Skip to content
This repository was archived by the owner on Oct 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

- **Trained inference (Spec 007):** `hyperlexical.infer --model-dir` runs a local
checkpoint (lazy torch, local trunk, fail-closed exit 2) and emits a
`MODEL_EMBEDDING` inference packet; only the approved pin carries
`hyperlex-structure-149m`. Default CLI stays the offline stub.

- **Card rename (Spec 007):** Hub card `hyperlex-structure-149m` for `seed-morph78`.
`name_gate.py` pins the approved checkpoint; `eval_unbind` sets `name_gate`
true only for a trunk-forward eval of that pin; eval schema field is boolean.
Expand Down
10 changes: 6 additions & 4 deletions NEXT_MOVES_007.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- morph78 named phrases settled · force/hard **236/277** · soft_ceiling train complete.
- **PROMOTE_BEST** (ceiling_escape): broad **0.9883** > PRIOR morph65 **0.8867** n=256 + E2 PASS.
- Card rename: Hub card `hyperlex-structure-149m`; eval packets set `name_gate` true only for a trunk-forward eval of `seed-morph78`.
- Real inference: `hyperlexical.infer --model-dir ~/.hyperlex/models/BEST` → `MODEL_EMBEDDING` packet (`hyperlex-structure-149m`), verified on Spark.
- Danny **`flip name_gate`** → `seed-morph78` may be called **Hyperlexical** (receipt `specs/007-hyperlexical-model/receipts/20260924-name-gate-yes-morph78.md`).

## Gate
Expand All @@ -15,9 +16,10 @@ soft_ceiling **spent** for this climb (morph78 now PRIOR/BEST). Further climbs n

## Next (each a separate operator sentence)

1. Real `seed-morph78` inference path (trunk-forward infer emitting the inference packet) — in progress.
2. Hub upload — still not authorized.
3. Optional T13 (promote `scripts/shadow/hyperlexical/` into `src/hyperlex/`).
4. HOLD morph78; no new climb without a new card.
1. Hub upload — still not authorized (decide publish vs explicit local-only).
2. Optional T13 (promote `scripts/shadow/hyperlexical/` into `src/hyperlex/`).
3. HOLD morph78; no new climb without a new card.

Open findings (receipt `receipts/20260924-infer-model-morph78.md`): classify overconfident on val-settled phrases; trunk+heads = 150.55M vs A1 150M (trunk 149.0M).

Qwen stays stopped unless re-enabled.
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ The skill remains the operator surface while the encoder stays SHADOW.
See [SPEC.md](SPEC.md) and [DESIGN.md](DESIGN.md) for the historical spine. Current operator snapshot: [STATUS.md](STATUS.md).

## Recent
- 2026-09-24: Real inference path — `infer --model-dir` on `seed-morph78` emits `MODEL_EMBEDDING` packets as `hyperlex-structure-149m` (Spark-verified).
- 2026-09-24: Card rename — Hub card `hyperlex-structure-149m`; eval packets set `name_gate` true only for trunk-forward eval of `seed-morph78`.
- 2026-09-24: Danny `flip name_gate` — `seed-morph78` named **Hyperlexical** (amendment A6). Card rename, Hub, T13 still separate.
- 2026-09-24: morph78 PROMOTE_BEST (soft_ceiling ceiling_escape; broad 0.9883 > 0.8867 n=256 + E2) after named-phrase settle. `name_gate` still false.
Expand Down
1 change: 1 addition & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ The skill remains the operator surface while the encoder stays SHADOW.
See [SPEC.md](SPEC.md) and [DESIGN.md](DESIGN.md) for the historical spine. Current operator snapshot: [STATUS.md](STATUS.md).

## Recent
- 2026-09-24: Real inference path — `infer --model-dir` on `seed-morph78` emits `MODEL_EMBEDDING` packets as `hyperlex-structure-149m` (Spark-verified).
- 2026-09-24: Card rename — Hub card `hyperlex-structure-149m`; eval packets set `name_gate` true only for trunk-forward eval of `seed-morph78`.
- 2026-09-24: Danny `flip name_gate` — `seed-morph78` named **Hyperlexical** (amendment A6). Card rename, Hub, T13 still separate.
- 2026-09-24: morph78 PROMOTE_BEST (soft_ceiling ceiling_escape; broad 0.9883 > 0.8867 n=256 + E2) after named-phrase settle. `name_gate` still false.
Expand Down
2 changes: 2 additions & 0 deletions docs/shadow-hyperlexical.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Danny ~2500 candidate bar: **met**. Hermes 913 / “gap to 2500” is **supersed

```bash
PYTHONPATH=scripts/shadow python3 -m hyperlexical.infer --text rizz --offline
# trained pin (Spark; needs torch + local trunk):
PYTHONPATH=scripts/shadow python3 -m hyperlexical.infer --text rizz --model-dir ~/.hyperlex/models/BEST
PYTHONPATH=scripts/shadow python3 -m hyperlexical.ingest_tap --dry-run
PYTHONPATH=scripts/shadow python3 -m hyperlexical.export
PYTHONPATH=scripts/shadow python3 -m hyperlexical.export --include-live
Expand Down
31 changes: 25 additions & 6 deletions scripts/shadow/hyperlexical/infer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""SHADOW CLI. Offline stub. No torch. No Hub."""
"""SHADOW CLI. Offline stub by default. ``--model-dir`` runs a local trained checkpoint. No Hub."""

from __future__ import annotations

Expand All @@ -17,15 +17,34 @@ def main(argv=None) -> int:
p.add_argument("--restricted", action="store_true")
p.add_argument("--role-scheme", default="positional", choices=("positional", "type_slot"))
p.add_argument("--out", default="")
p.add_argument("--model-dir", default="", help="local train-out dir (e.g. ~/.hyperlex/models/BEST); needs torch + trunk")
p.add_argument("--trunk-dir", default="", help="local ModernBERT-base snapshot (default HYPERLEX_TRUNK_DIR)")
args = p.parse_args(argv)
if os.environ.get("HYPERLEX_OFFLINE") == "0" and not args.offline:
print("abort: online infer is out of U1", file=sys.stderr)
return 2
packet = build_packet(
args.text,
restricted=args.restricted,
role_scheme=args.role_scheme,
)
if args.model_dir:
if args.role_scheme != "positional":
print("abort: trained infer supports role_scheme=positional only", file=sys.stderr)
return 2
from .infer_model import InferModelError, infer

try:
packet = infer(
args.text,
model_dir=args.model_dir,
trunk_dir=args.trunk_dir or None,
restricted=args.restricted,
)
except InferModelError as exc:
print(json.dumps({"abort": True, "error": str(exc), "brier": None}), file=sys.stderr)
return 2
else:
packet = build_packet(
args.text,
restricted=args.restricted,
role_scheme=args.role_scheme,
)
text = json.dumps(packet, indent=2, sort_keys=True)
if args.out:
with open(args.out, "w", encoding="utf-8") as fh:
Expand Down
159 changes: 159 additions & 0 deletions scripts/shadow/hyperlexical/infer_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Trained-model inference for a pinned checkpoint. Lazy torch. Local files only.

Emits ``hyperlex.hyperlexical.inference.v0.1`` with ``MODEL_EMBEDDING``. Lineage
is the classify head on the CLS state, as in train. Unbind treats each
whitespace token as a positional atom and reads role/filler heads on its pooled
token states, as in train val. Brier stays null; this is not a forecast.
"""

from __future__ import annotations

import hashlib
import os
from pathlib import Path

from .layout import HIDDEN, MAX_LEN
from .name_gate import PUBLIC_CARD_NAME, name_gate_for, pin_of
from .packet import SCHEMA, build_packet, sha256_hex, validate_packet

WEIGHT_FILE = "model.safetensors"


class InferModelError(RuntimeError):
pass


def default_trunk_dir() -> Path:
return Path(
os.environ.get("HYPERLEX_TRUNK_DIR")
or Path.home() / ".hyperlex" / "models" / "trunks" / "ModernBERT-base"
)


def model_identity(model_dir: Path) -> tuple[str, str | None]:
"""(model_id, model_version). Only an approved pin carries the public card name."""
pin = pin_of(model_dir)
if name_gate_for(model_dir):
return PUBLIC_CARD_NAME, pin
return Path(os.path.realpath(model_dir)).name, pin


def model_packet(
text: str,
*,
model_id: str,
model_version: str | None,
vector: list[float],
param_count: int,
families: list[str],
family_probs: list[float],
atoms: list[dict],
) -> dict:
"""Pure packet assembly from model outputs. No torch."""
if len(families) != len(family_probs) or not families:
raise InferModelError("family probabilities do not match families")
best = max(range(len(families)), key=lambda i: family_probs[i])
vector_blob = ",".join(f"{v:.6f}" for v in vector).encode("utf-8")
unbind_ok = bool(atoms) and all(a["filler_pred"] == a["filler"] for a in atoms)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include role predictions in the unbind result

When the filler head predicts every token correctly but the role head predicts any wrong positional role, this expression still reports unbind_ok: true. Spec 007 defines unbinding as role-and-filler exactness, so downstream consumers can accept a structurally incorrect result; require both role_pred == role and filler_pred == filler for every atom.

Useful? React with 👍 / 👎.

packet = {
"schema": SCHEMA,
"model_id": model_id,
"model_version": model_version,
"embed_mode": "MODEL_EMBEDDING",
"input_hash": sha256_hex(text),
"vector_hash": hashlib.sha256(vector_blob).hexdigest(),
"selection_proxy": "unspecified",
"param_count": int(param_count),
"surface": text,
"payload_ref": None,
"lineage_family": families[best],
"lineage_confidence": round(float(family_probs[best]), 6),
"typology": [],
"stage": None,
"role_scheme": "positional",
"unbind_ok": unbind_ok,
"unbind": {"atoms": atoms},
"routes_claimed": ["form", "lexical"],
"restricted_intent_suspected": False,
"brier": None,
"forecast_eligible": False,
"auto_fire": False,
"class": "INFERRED",
}
return validate_packet(packet)


def infer(text: str, *, model_dir: Path, trunk_dir: Path | None = None, restricted: bool = False) -> dict:
if restricted or "__RESTRICTED_FIXTURE__" in (text or ""):
return build_packet(text, restricted=True)
if not (text or "").strip():
raise InferModelError("empty text")
model_dir = Path(model_dir)
trunk_dir = Path(trunk_dir) if trunk_dir else default_trunk_dir()
weight = model_dir / WEIGHT_FILE
if not weight.is_file():
raise InferModelError(f"missing {weight}")
Comment on lines +93 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load the supported heads.pt checkpoint format

When safetensors is unavailable or saving it fails, save_heads intentionally produces heads.pt, and the checkpoint contract declares both formats valid. This hardcoded lookup therefore rejects a valid output from the repository's own trainer as missing; inference should select and load either artifact, as the existing trunk-forward evaluator does.

Useful? React with 👍 / 👎.

if not trunk_dir.is_dir():
raise InferModelError(f"missing trunk snapshot {trunk_dir}")

from .eval_unbind import TrunkForwardError
from .eval_forward import _import_torch, _load_maps, _load_trunk, _offsets, apply_encoder_trainable
from .align import atom_token_index, pool_indices
from .save_pretrained import split_weight_tensors

try:
torch, nn = _import_torch()
from safetensors.torch import load_file

split = split_weight_tensors(load_file(str(weight), device="cpu"))
maps = _load_maps(model_dir, None)
tok, encoder = _load_trunk(trunk_dir, model_dir)
except (TrunkForwardError, ImportError) as exc:
raise InferModelError(str(exc)) from exc
applied = apply_encoder_trainable(encoder, split["encoder"])
if applied["present"] and applied["loaded"] == 0:
raise InferModelError("encoder tensors present but none matched trunk keys")
heads = {}
for name, n_out in (
("classify", len(maps["families"])),
("role_head", len(maps["role_vocab"])),
("filler_head", len(maps["filler_vocab"])),
):
state = split.get(name) or {}
if "weight" not in state or tuple(state["weight"].shape) != (n_out, HIDDEN):
raise InferModelError(f"{name} shape does not match layout")
head = nn.Linear(HIDDEN, n_out)
head.load_state_dict(state)
heads[name] = head.eval()
encoder.eval()

tokens = text.split()
with torch.no_grad():
enc = tok([text], truncation=True, max_length=MAX_LEN, return_tensors="pt")
Comment on lines +130 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not emit predictions for truncated-away atoms

For inputs exceeding the tokenizer's MAX_LEN, tokens retains every whitespace token while the encoder truncates the sequence. Atoms beyond the retained offset mapping then get no matching indices and pool_indices silently substitutes token state 1, so the packet fabricates predictions for text the model never encoded and can even report a false successful unbind; reject, omit, or explicitly mark those truncated atoms unsuccessful.

Useful? React with 👍 / 👎.

states = encoder(**enc).last_hidden_state[0]
cls = states[0]
probs = torch.softmax(heads["classify"](cls), dim=-1).tolist()
offs = _offsets(tok, text)
atoms = []
for i, token in enumerate(tokens):
idxs = pool_indices(states.size(0), atom_token_index(text, token, offs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align each atom to its actual occurrence

For repeated tokens such as go go, or a token that first appears as a substring such as the a in cat a, atom_token_index ultimately uses text.find(token) and resolves the first matching substring every time. Multiple positional atoms consequently pool the same or an unrelated hidden state while being labeled as different pos_i roles, corrupting their role and filler predictions; use the character span of the enumerated whitespace occurrence instead.

Useful? React with 👍 / 👎.

h = states[idxs].mean(0)
role = maps["role_vocab"][int(heads["role_head"](h).argmax())]
filler = maps["filler_vocab"][int(heads["filler_head"](h).argmax())]
atoms.append(
{"role": f"pos_{i}", "role_pred": role, "filler": token.lower(), "filler_pred": filler}
)
param_count = sum(p.numel() for p in encoder.parameters()) + sum(
p.numel() for h in heads.values() for p in h.parameters()
)
model_id, model_version = model_identity(model_dir)
return model_packet(
text,
model_id=model_id,
model_version=model_version,
vector=cls.tolist(),
param_count=param_count,
families=list(maps["families"]),
family_probs=probs,
atoms=atoms,
)
6 changes: 3 additions & 3 deletions specs/007-hyperlexical-model/HYPERLEXICAL-PRODUCT-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Item 4 is done. Hub card name is `hyperlex-structure-149m`; local train-out path
- [ ] operator reviews this reconciled plan
- [x] Danny explicit `flip name_gate` (2026-09-24)
- [x] card rename → `hyperlex-structure-149m` + eval packet `name_gate` true only for trunk-forward eval of `seed-morph78`
- [ ] real `seed-morph78` inference path (trunk-forward infer → inference packet)
- [x] real `seed-morph78` inference path (`infer --model-dir`; receipt `receipts/20260924-infer-model-morph78.md`)
- [ ] finalize public model card from pinned receipts
- [ ] separately authorize Hub upload, or explicitly close the cycle with no Hub
- [ ] optional T13 in a separate PR with tests
Expand All @@ -97,7 +97,7 @@ Q1 status is not a substitute for Spec 007 product completion, and Spec 007 prog

## Default recommendation

**HOLD morph78.** NAME is done. Resolve the inference path and PUBLISH before authorizing another training climb.
**HOLD morph78.** NAME is done. Resolve PUBLISH (and the two inference findings) before authorizing another training climb.

## Explicit non-goals

Expand All @@ -119,7 +119,7 @@ Q1 status is not a substitute for Spec 007 product completion, and Spec 007 prog
- [ ] operator product review
- [x] Danny `name_gate` decision — yes for `seed-morph78`
- [x] card rename
- [ ] real inference path
- [x] real inference path
- [ ] Hub decision: upload authorized, or explicit no-Hub this cycle
- [ ] optional T13 decision

Expand Down
10 changes: 6 additions & 4 deletions specs/007-hyperlexical-model/NEXT_MOVES_007.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- morph78 named phrases settled · force/hard **236/277** · soft_ceiling train complete.
- **PROMOTE_BEST** (ceiling_escape): broad **0.9883** > PRIOR morph65 **0.8867** n=256 + E2 PASS.
- Card rename: Hub card `hyperlex-structure-149m`; eval packets set `name_gate` true only for a trunk-forward eval of `seed-morph78`.
- Real inference: `hyperlexical.infer --model-dir ~/.hyperlex/models/BEST` → `MODEL_EMBEDDING` packet (`hyperlex-structure-149m`), verified on Spark.
- Danny **`flip name_gate`** → `seed-morph78` may be called **Hyperlexical** (receipt `specs/007-hyperlexical-model/receipts/20260924-name-gate-yes-morph78.md`).

## Gate
Expand All @@ -15,9 +16,10 @@ soft_ceiling **spent** for this climb (morph78 now PRIOR/BEST). Further climbs n

## Next (each a separate operator sentence)

1. Real `seed-morph78` inference path (trunk-forward infer emitting the inference packet) — in progress.
2. Hub upload — still not authorized.
3. Optional T13 (promote `scripts/shadow/hyperlexical/` into `src/hyperlex/`).
4. HOLD morph78; no new climb without a new card.
1. Hub upload — still not authorized (decide publish vs explicit local-only).
2. Optional T13 (promote `scripts/shadow/hyperlexical/` into `src/hyperlex/`).
3. HOLD morph78; no new climb without a new card.

Open findings (receipt `receipts/20260924-infer-model-morph78.md`): classify overconfident on val-settled phrases; trunk+heads = 150.55M vs A1 150M (trunk 149.0M).

Qwen stays stopped unless re-enabled.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Trained inference path — `seed-morph78` (2026-09-24)

**Scope:** first real (non-stub) inference for Hyperlexical. `hyperlexical.infer --model-dir` loads a local trained checkpoint and emits `hyperlex.hyperlexical.inference.v0.1` with `MODEL_EMBEDDING`. No Hub, no T13, no training. Default CLI stays the offline stub.

**Run:** Spark, container `lmsysorg/sglang:dev-qwen38-27b-dflash2`, branch code staged in `~/hlx-infer-test` (the Spark `~/Hyperlex` checkout was not touched). Model `~/.hyperlex/models/BEST` → `seed-morph78`. Outputs in `infer-model-morph78-20260924/`.

## OBSERVED

| Input | model_id / version | lineage (conf) | unbind_ok |
|---|---|---|---|
| `rizz` | `hyperlex-structure-149m` / `seed-morph78` | brainrot-aura (0.936) | true |
| `no cap fr` | same | brainrot-aura (0.880) | true |
| `touch grass` | same | gaming-meta (0.978) | true |
| `bet that up` | same | none (1.000) | true |
| `have fun staying poor` | same | none (1.000) | true |
| `rizz` on morph65 dir | `hyperlex-encoder-modernbert-base-seed-morph65` / `seed-morph65` | brainrot-aura (0.984) | true |

Trunk-forward E2 on `BEST` with this branch: `e2_pass: true`, unbind_exact 1.0, n_unbind_eval 24, **`name_gate: true`** (card-rename PR #104 behaviour confirmed on the real pin).

## Findings (recorded, not acted on)

1. **Classify overconfidence on val-settled phrases.** `bet that up` and `have fun staying poor` were settled OBSERVED `split=val` for unbind; the classify head returns `none` at confidence ≈1.0 instead of their card lineages (kinship-address, crypto-degen). Lineage confidence is not calibrated; treat it as INFERRED only.
2. **Parameter count vs A1.** Inference counts **150,546,889** parameters: trunk 149,014,272 + heads 1,532,617 (filler vocab 1,972). A1 says "T1 ≤ 150M". The trunk is within the ceiling; trunk + heads exceeds it by ~0.55M. Whether A1 counts heads is an operator reading; the card name `hyperlex-structure-149m` matches the trunk.

## NOT_COMPUTABLE

- Calibrated lineage accuracy of the inference path (no held-out classify receipt for this pin; E1 still NOT_COMPUTABLE).
- `type_slot` inference (CLI refuses it for `--model-dir` in this cut).
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"brier": null,
"device": "cuda",
"e2_pass": true,
"encoder_trainable_loaded": 48,
"encoder_trainable_present": 48,
"forecast_eligible": false,
"heads_loaded": true,
"model_dir": "/home/morpheus/.hyperlex/models/BEST",
"model_id": "hyperlex-encoder-modernbert-base-seed",
"model_swap": 1.0,
"n_test": 12,
"n_unbind_eval": 24,
"name_gate": true,
"note": "Trunk-forward unbind_exact + token/slot F1 from /home/morpheus/.hyperlex/models/BEST/model.safetensors vs 004 probe_swap_min. Stub/digest leaves F1 null (no civilian filler lists). name_gate is true only for an approved pin (A6). Applied 48 saved encoder trainable tensors.",
"probe_positional_swap": 1.0,
"probe_schema": "abraxas.recoverable_structure.probe.v0.1",
"probe_swap_min": 0.5,
"probe_type_slot_swap": 0.5,
"schema": "hyperlex.hyperlexical.eval_unbind.v0.1",
"stub_swap": 0.0,
"trunk": "answerdotai/ModernBERT-base",
"trunk_dir": "/home/morpheus/.hyperlex/models/trunks/ModernBERT-base",
"trunk_forward": true,
"trunk_loaded": true,
"unbind_exact": 1.0,
"unbind_slot_f1": 1.0,
"unbind_token_f1": 1.0,
"unbind_token_precision": 1.0,
"unbind_token_recall": 1.0,
"weight_file": "model.safetensors"
}
Loading
Loading