From 667790a0771381a165421e9e010529de8f276220 Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Thu, 24 Sep 2026 12:31:36 -0700 Subject: [PATCH 1/3] feat(007): trained inference path for seed-morph78 --- CHANGELOG.md | 5 +++++ NEXT_MOVES_007.md | 10 +++++---- ROADMAP.md | 1 + docs/ROADMAP.md | 1 + docs/shadow-hyperlexical.md | 2 ++ scripts/shadow/hyperlexical/infer.py | 31 ++++++++++++++++++++++------ 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91810e1d..12f03c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/NEXT_MOVES_007.md b/NEXT_MOVES_007.md index d0f4ab7f..063e6dc4 100644 --- a/NEXT_MOVES_007.md +++ b/NEXT_MOVES_007.md @@ -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 @@ -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. diff --git a/ROADMAP.md b/ROADMAP.md index e998d4fe..21c15e0f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e998d4fe..21c15e0f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. diff --git a/docs/shadow-hyperlexical.md b/docs/shadow-hyperlexical.md index 63844061..89205812 100644 --- a/docs/shadow-hyperlexical.md +++ b/docs/shadow-hyperlexical.md @@ -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 diff --git a/scripts/shadow/hyperlexical/infer.py b/scripts/shadow/hyperlexical/infer.py index 23fb6642..12d62c95 100644 --- a/scripts/shadow/hyperlexical/infer.py +++ b/scripts/shadow/hyperlexical/infer.py @@ -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 @@ -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: From b4c29738af379980a86033736652a9efc0e77faa Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Thu, 24 Sep 2026 12:34:19 -0700 Subject: [PATCH 2/3] feat(007): trained inference path for seed-morph78 --- scripts/shadow/hyperlexical/infer_model.py | 159 ++++++++++++++++++ .../HYPERLEXICAL-PRODUCT-PLAN.md | 6 +- .../007-hyperlexical-model/NEXT_MOVES_007.md | 10 +- .../receipts/20260924-infer-model-morph78.md | 28 +++ .../infer-model-morph78-20260924/e2-best.json | 32 ++++ .../infer-bet_that_up.json | 49 ++++++ .../infer-have_fun_staying_poor.json | 55 ++++++ .../infer-morph65-rizz.json | 37 ++++ .../infer-no_cap_fr.json | 49 ++++++ .../infer-rizz.json | 37 ++++ .../infer-touch_grass.json | 43 +++++ specs/007-hyperlexical-model/status.md | 2 +- 12 files changed, 499 insertions(+), 8 deletions(-) create mode 100644 scripts/shadow/hyperlexical/infer_model.py create mode 100644 specs/007-hyperlexical-model/receipts/20260924-infer-model-morph78.md create mode 100644 specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/e2-best.json create mode 100644 specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-bet_that_up.json create mode 100644 specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-have_fun_staying_poor.json create mode 100644 specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-morph65-rizz.json create mode 100644 specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-no_cap_fr.json create mode 100644 specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-rizz.json create mode 100644 specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-touch_grass.json diff --git a/scripts/shadow/hyperlexical/infer_model.py b/scripts/shadow/hyperlexical/infer_model.py new file mode 100644 index 00000000..57c45ffc --- /dev/null +++ b/scripts/shadow/hyperlexical/infer_model.py @@ -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) + 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}") + 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") + 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)) + 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, + ) diff --git a/specs/007-hyperlexical-model/HYPERLEXICAL-PRODUCT-PLAN.md b/specs/007-hyperlexical-model/HYPERLEXICAL-PRODUCT-PLAN.md index 5cbb1ecd..ae02a9e6 100644 --- a/specs/007-hyperlexical-model/HYPERLEXICAL-PRODUCT-PLAN.md +++ b/specs/007-hyperlexical-model/HYPERLEXICAL-PRODUCT-PLAN.md @@ -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 @@ -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 @@ -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 diff --git a/specs/007-hyperlexical-model/NEXT_MOVES_007.md b/specs/007-hyperlexical-model/NEXT_MOVES_007.md index d0f4ab7f..063e6dc4 100644 --- a/specs/007-hyperlexical-model/NEXT_MOVES_007.md +++ b/specs/007-hyperlexical-model/NEXT_MOVES_007.md @@ -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 @@ -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. diff --git a/specs/007-hyperlexical-model/receipts/20260924-infer-model-morph78.md b/specs/007-hyperlexical-model/receipts/20260924-infer-model-morph78.md new file mode 100644 index 00000000..af5df20b --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/20260924-infer-model-morph78.md @@ -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). diff --git a/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/e2-best.json b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/e2-best.json new file mode 100644 index 00000000..916e265f --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/e2-best.json @@ -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" +} diff --git a/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-bet_that_up.json b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-bet_that_up.json new file mode 100644 index 00000000..6bca3ca8 --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-bet_that_up.json @@ -0,0 +1,49 @@ +{ + "auto_fire": false, + "brier": null, + "class": "INFERRED", + "embed_mode": "MODEL_EMBEDDING", + "forecast_eligible": false, + "input_hash": "4df92ccb02f4fbd0bdea30ce8f9f5b975e5359dad3c7db19d846f5b32176a879", + "lineage_confidence": 1.0, + "lineage_family": "none", + "model_id": "hyperlex-structure-149m", + "model_version": "seed-morph78", + "param_count": 150546889, + "payload_ref": null, + "restricted_intent_suspected": false, + "role_scheme": "positional", + "routes_claimed": [ + "form", + "lexical" + ], + "schema": "hyperlex.hyperlexical.inference.v0.1", + "selection_proxy": "unspecified", + "stage": null, + "surface": "bet that up", + "typology": [], + "unbind": { + "atoms": [ + { + "filler": "bet", + "filler_pred": "bet", + "role": "pos_0", + "role_pred": "pos_0" + }, + { + "filler": "that", + "filler_pred": "that", + "role": "pos_1", + "role_pred": "pos_1" + }, + { + "filler": "up", + "filler_pred": "up", + "role": "pos_2", + "role_pred": "pos_2" + } + ] + }, + "unbind_ok": true, + "vector_hash": "ec1a71eddb9782fd606f374ce7b4ca0af1b5564fc97715be9e16fbf613f1e146" +} diff --git a/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-have_fun_staying_poor.json b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-have_fun_staying_poor.json new file mode 100644 index 00000000..22ba0b36 --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-have_fun_staying_poor.json @@ -0,0 +1,55 @@ +{ + "auto_fire": false, + "brier": null, + "class": "INFERRED", + "embed_mode": "MODEL_EMBEDDING", + "forecast_eligible": false, + "input_hash": "c4c648b19dfd0c567fd2e60022d871e70cf50370e126a0eac839e16126152455", + "lineage_confidence": 0.999999, + "lineage_family": "none", + "model_id": "hyperlex-structure-149m", + "model_version": "seed-morph78", + "param_count": 150546889, + "payload_ref": null, + "restricted_intent_suspected": false, + "role_scheme": "positional", + "routes_claimed": [ + "form", + "lexical" + ], + "schema": "hyperlex.hyperlexical.inference.v0.1", + "selection_proxy": "unspecified", + "stage": null, + "surface": "have fun staying poor", + "typology": [], + "unbind": { + "atoms": [ + { + "filler": "have", + "filler_pred": "have", + "role": "pos_0", + "role_pred": "pos_0" + }, + { + "filler": "fun", + "filler_pred": "fun", + "role": "pos_1", + "role_pred": "pos_1" + }, + { + "filler": "staying", + "filler_pred": "staying", + "role": "pos_2", + "role_pred": "pos_2" + }, + { + "filler": "poor", + "filler_pred": "poor", + "role": "pos_3", + "role_pred": "pos_3" + } + ] + }, + "unbind_ok": true, + "vector_hash": "2d5c4541a0ae677c4c91f9180359691eb29d15956e79a67bdbf4f7f2e4084fda" +} diff --git a/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-morph65-rizz.json b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-morph65-rizz.json new file mode 100644 index 00000000..995e5619 --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-morph65-rizz.json @@ -0,0 +1,37 @@ +{ + "auto_fire": false, + "brier": null, + "class": "INFERRED", + "embed_mode": "MODEL_EMBEDDING", + "forecast_eligible": false, + "input_hash": "95ef5a31d084e89425c9e30a864d6cba7fb5a4d1c0e031029cd58c1a433adb5d", + "lineage_confidence": 0.98396, + "lineage_family": "brainrot-aura", + "model_id": "hyperlex-encoder-modernbert-base-seed-morph65", + "model_version": "seed-morph65", + "param_count": 150551503, + "payload_ref": null, + "restricted_intent_suspected": false, + "role_scheme": "positional", + "routes_claimed": [ + "form", + "lexical" + ], + "schema": "hyperlex.hyperlexical.inference.v0.1", + "selection_proxy": "unspecified", + "stage": null, + "surface": "rizz", + "typology": [], + "unbind": { + "atoms": [ + { + "filler": "rizz", + "filler_pred": "rizz", + "role": "pos_0", + "role_pred": "pos_0" + } + ] + }, + "unbind_ok": true, + "vector_hash": "06a317074d378b8fc91986341cac1f1a404721d51748c67328144350556ef915" +} diff --git a/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-no_cap_fr.json b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-no_cap_fr.json new file mode 100644 index 00000000..e90652d2 --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-no_cap_fr.json @@ -0,0 +1,49 @@ +{ + "auto_fire": false, + "brier": null, + "class": "INFERRED", + "embed_mode": "MODEL_EMBEDDING", + "forecast_eligible": false, + "input_hash": "1e50b5001350fbabbdfe1f499764dfa34bc25198235d0feebf773700244a29f8", + "lineage_confidence": 0.879764, + "lineage_family": "brainrot-aura", + "model_id": "hyperlex-structure-149m", + "model_version": "seed-morph78", + "param_count": 150546889, + "payload_ref": null, + "restricted_intent_suspected": false, + "role_scheme": "positional", + "routes_claimed": [ + "form", + "lexical" + ], + "schema": "hyperlex.hyperlexical.inference.v0.1", + "selection_proxy": "unspecified", + "stage": null, + "surface": "no cap fr", + "typology": [], + "unbind": { + "atoms": [ + { + "filler": "no", + "filler_pred": "no", + "role": "pos_0", + "role_pred": "pos_0" + }, + { + "filler": "cap", + "filler_pred": "cap", + "role": "pos_1", + "role_pred": "pos_1" + }, + { + "filler": "fr", + "filler_pred": "fr", + "role": "pos_2", + "role_pred": "pos_2" + } + ] + }, + "unbind_ok": true, + "vector_hash": "80853a08e9ef7fdb319921c460d8a032cdec285457e8b49c98dd41874ebc1963" +} diff --git a/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-rizz.json b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-rizz.json new file mode 100644 index 00000000..161b8d8d --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-rizz.json @@ -0,0 +1,37 @@ +{ + "auto_fire": false, + "brier": null, + "class": "INFERRED", + "embed_mode": "MODEL_EMBEDDING", + "forecast_eligible": false, + "input_hash": "95ef5a31d084e89425c9e30a864d6cba7fb5a4d1c0e031029cd58c1a433adb5d", + "lineage_confidence": 0.936404, + "lineage_family": "brainrot-aura", + "model_id": "hyperlex-structure-149m", + "model_version": "seed-morph78", + "param_count": 150546889, + "payload_ref": null, + "restricted_intent_suspected": false, + "role_scheme": "positional", + "routes_claimed": [ + "form", + "lexical" + ], + "schema": "hyperlex.hyperlexical.inference.v0.1", + "selection_proxy": "unspecified", + "stage": null, + "surface": "rizz", + "typology": [], + "unbind": { + "atoms": [ + { + "filler": "rizz", + "filler_pred": "rizz", + "role": "pos_0", + "role_pred": "pos_0" + } + ] + }, + "unbind_ok": true, + "vector_hash": "f3eb252857fe27d5e2eccf1194cfa9c1c0f02ea4612c56a5a4f921cb72664151" +} diff --git a/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-touch_grass.json b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-touch_grass.json new file mode 100644 index 00000000..8d0b5097 --- /dev/null +++ b/specs/007-hyperlexical-model/receipts/infer-model-morph78-20260924/infer-touch_grass.json @@ -0,0 +1,43 @@ +{ + "auto_fire": false, + "brier": null, + "class": "INFERRED", + "embed_mode": "MODEL_EMBEDDING", + "forecast_eligible": false, + "input_hash": "fc403ee9d4d380573defc9817ebf7e312fe278bc1c127ab7ad1ab62afa945212", + "lineage_confidence": 0.9783, + "lineage_family": "gaming-meta", + "model_id": "hyperlex-structure-149m", + "model_version": "seed-morph78", + "param_count": 150546889, + "payload_ref": null, + "restricted_intent_suspected": false, + "role_scheme": "positional", + "routes_claimed": [ + "form", + "lexical" + ], + "schema": "hyperlex.hyperlexical.inference.v0.1", + "selection_proxy": "unspecified", + "stage": null, + "surface": "touch grass", + "typology": [], + "unbind": { + "atoms": [ + { + "filler": "touch", + "filler_pred": "touch", + "role": "pos_0", + "role_pred": "pos_0" + }, + { + "filler": "grass", + "filler_pred": "grass", + "role": "pos_1", + "role_pred": "pos_1" + } + ] + }, + "unbind_ok": true, + "vector_hash": "88ed068b3c8e4a62e5fe4fd1e473f260fe94c24a56402cb3a382574fe8e939ae" +} diff --git a/specs/007-hyperlexical-model/status.md b/specs/007-hyperlexical-model/status.md index 164815d1..f19d4d08 100644 --- a/specs/007-hyperlexical-model/status.md +++ b/specs/007-hyperlexical-model/status.md @@ -29,7 +29,7 @@ Operator scoreboard (Danny-locked harvest snapshot **2026-09-10**; climb pin **2 Next operator sentences that do work: - Default: **HOLD** `seed-morph78`; operator product review of `HYPERLEXICAL-PRODUCT-PLAN.md` -- Real inference path; Hub decision; optional T13 decision (each separate). `name_gate` + card rename done. +- Hub decision; optional T13 decision (each separate). `name_gate`, card rename, and real inference path done. - A new climb only on a new named-phrase acquire card vs morph78 PRIOR — see `NAME-GATE-AND-NAMED-PHRASES.md` - `open 006 IsA` — separate spec From e028323fb1f709d4d4d5f8421bae92b1f27583fb Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Thu, 24 Sep 2026 12:34:47 -0700 Subject: [PATCH 3/3] feat(007): trained inference path for seed-morph78 --- tests/shadow/test_hyperlexical_infer_model.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/shadow/test_hyperlexical_infer_model.py diff --git a/tests/shadow/test_hyperlexical_infer_model.py b/tests/shadow/test_hyperlexical_infer_model.py new file mode 100644 index 00000000..69a6d83e --- /dev/null +++ b/tests/shadow/test_hyperlexical_infer_model.py @@ -0,0 +1,89 @@ +import json +import pathlib +import sys + +import jsonschema +import pytest + +ROOT = pathlib.Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts" / "shadow")) + +from hyperlexical.infer import main as infer_main +from hyperlexical.infer_model import InferModelError, infer, model_identity, model_packet +from hyperlexical.layout import FAMILIES, MODEL_ID_SEED + +SCHEMA = json.loads( + (ROOT / "specs" / "007-hyperlexical-model" / "schemas" / "hyperlexical_inference.v0.1.schema.json").read_text() +) + + +def _packet(**over): + kw = dict( + model_id="hyperlex-structure-149m", + model_version="seed-morph78", + vector=[0.1, -0.2, 0.3], + param_count=149_000_000, + families=list(FAMILIES), + family_probs=[0.05] * (len(FAMILIES) - 1) + [0.6], + atoms=[{"role": "pos_0", "role_pred": "pos_0", "filler": "rizz", "filler_pred": "rizz"}], + ) + kw.update(over) + return model_packet("rizz", **kw) + + +def test_model_packet_matches_schema(): + pkt = _packet() + jsonschema.validate(pkt, SCHEMA) + assert pkt["embed_mode"] == "MODEL_EMBEDDING" + assert pkt["brier"] is None + assert pkt["forecast_eligible"] is False + assert pkt["lineage_family"] == FAMILIES[-1] + assert pkt["lineage_confidence"] == 0.6 + assert pkt["unbind_ok"] is True + assert pkt["class"] == "INFERRED" + assert len(pkt["vector_hash"]) == 64 + + +def test_unbind_ok_false_on_filler_miss(): + pkt = _packet(atoms=[{"role": "pos_0", "role_pred": "pos_0", "filler": "rizz", "filler_pred": ""}]) + assert pkt["unbind_ok"] is False + jsonschema.validate(pkt, SCHEMA) + + +def test_family_mismatch_fails_closed(): + with pytest.raises(InferModelError): + _packet(family_probs=[1.0]) + + +def test_identity_names_only_the_pin(tmp_path): + assert model_identity(tmp_path / f"{MODEL_ID_SEED}-morph78") == ("hyperlex-structure-149m", "seed-morph78") + assert model_identity(tmp_path / f"{MODEL_ID_SEED}-morph65") == (f"{MODEL_ID_SEED}-morph65", "seed-morph65") + + +def test_restricted_never_loads_model(tmp_path): + pkt = infer("__RESTRICTED_FIXTURE__ x", model_dir=tmp_path / "missing") + assert pkt["surface"] is None + assert pkt["restricted_intent_suspected"] is True + + +def test_missing_weights_fail_closed(tmp_path): + with pytest.raises(InferModelError): + infer("rizz", model_dir=tmp_path) + + +def test_cli_model_dir_fails_closed_exit_2(tmp_path, capsys): + rc = infer_main(["--text", "rizz", "--model-dir", str(tmp_path)]) + err = json.loads(capsys.readouterr().err) + assert rc == 2 + assert err["abort"] is True + assert err["brier"] is None + + +def test_cli_rejects_type_slot_for_model(tmp_path): + assert infer_main(["--text", "rizz", "--model-dir", str(tmp_path), "--role-scheme", "type_slot"]) == 2 + + +def test_cli_default_is_still_stub(tmp_path): + out = tmp_path / "p.json" + assert infer_main(["--text", "rizz", "--out", str(out)]) == 0 + assert json.loads(out.read_text())["model_id"] == "stub"