docs(consumption-wiring): brief for closing Frame + Composition loop halves (#396)
Names the structural gap discovered in the first end-to-end
CompositionClaim ratification (2026-05-27 post-#393):
ratification handler writes JSONL artifacts cleanly, but no runtime
code reads compositions/*.jsonl or frames/*.jsonl. Two of three
sub-types ship the ratification half of the loop without the
consumption half.
State:
lexicon/ writer ✓ reader ✓ (LexicalClaim — closed)
frames/ writer ✓ reader ✗ (FrameClaim — half-open)
compositions/ writer ✓ reader ✗ (CompositionClaim — half-open)
Proposes one bundled PR (CW-1 + CW-2) mirroring the proven
generate/comprehension/lexicon.py::load_lexicon pattern:
CW-1 — Frame consumption: pack-compile frames/*.jsonl into a
runtime-loadable artifact; new load_frame_registry()
loader; reader wire.
CW-2 — Composition consumption: pack-compile compositions/*.jsonl;
new load_composition_registry(); injector wire in
generate/recognizer_anchor_inject.py.
Hard requirements: SAFE_COMPOSITION_CATEGORIES allowlist enforced
at both write and load (defense in depth); polarity "falsifies"
honored; manifest checksum extended per CLAUDE.md "Semantic Pack
Discipline"; empty-registry runtime byte-identical to today.
Truth-test pinned: success is the EVAL DELTA, not the artifact
append. PR is "done" when case 0019 (the canary I ratified) admits
under train_sample, train_sample moves from 3 correct / 47 refused
to ≥4 correct / 46 refused, case 0050 stays refused, wrong==0 holds.
This brief is orthogonal to the workbench UI wave (W1..W4); both
can ship in parallel. Recommended operator: Opus (load-bearing
wrong=0 surface; same rigor as CC-2).
This commit is contained in:
parent
3c9e7e2ce3
commit
a105eb5f20
1 changed files with 359 additions and 0 deletions
359
docs/handoff/COMPOSITION-FRAME-CONSUMPTION-WIRING-BRIEF.md
Normal file
359
docs/handoff/COMPOSITION-FRAME-CONSUMPTION-WIRING-BRIEF.md
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
# Composition + Frame Consumption Wiring — Follow-Up Brief
|
||||
|
||||
**Date:** 2026-05-27
|
||||
**Author:** Shay
|
||||
**Parent:** ADR-0167, ADR-0168 / 0168.1, ADR-0169 / 0169.1, ADR-0172
|
||||
**Related:** [[project-ratification-consumption-gap-2026-05-27]], [[feedback-ratify-vs-consume-loop-closure]]
|
||||
**Type:** Implementation brief (one or two PRs); not a doctrine ADR
|
||||
|
||||
---
|
||||
|
||||
## Why this brief exists
|
||||
|
||||
The end-to-end CompositionClaim ratification ran cleanly on 2026-05-27
|
||||
post-#393 merge. The handler appended one entry to
|
||||
`language_packs/data/en_core_math_v1/compositions/multiplicative_composition.jsonl`
|
||||
with provenance `adr_0169_composition_ratified_shay_2026-05-27`.
|
||||
|
||||
**The eval did not change.** `train_sample` re-ran with the reader on
|
||||
returned the same baseline: **3 correct / 47 refused / 0 wrong**. Case
|
||||
0019 (the one I ratified) still refused with `recognized-but-uninjectable`.
|
||||
`wrong=0` held — no regression — but also no admission.
|
||||
|
||||
Why: **no runtime code reads `compositions/*.jsonl` or `frames/*.jsonl`**.
|
||||
|
||||
| Directory | Has handler (writes) | Has consumer (reads) |
|
||||
|---|---|---|
|
||||
| `language_packs/data/en_core_math_v1/lexicon/` | ✓ LexicalClaim (W2-D) | ✓ `generate/comprehension/lexicon.py::load_lexicon` |
|
||||
| `language_packs/data/en_core_math_v1/frames/` | ✓ FrameClaim (#389 / ADR-0168) | ✗ none |
|
||||
| `language_packs/data/en_core_math_v1/compositions/` | ✓ CompositionClaim (#393 / ADR-0169) | ✗ none |
|
||||
|
||||
Two of three sub-types ship the **ratification half** of the loop
|
||||
but not the **consumption half**. The handler-side compounding loop
|
||||
is closed; the runtime-effect compounding loop is not.
|
||||
|
||||
This brief names the work to close the consumption half.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
Two parallel-safe wirings:
|
||||
|
||||
- **CW-1 — Frame consumption.** Make ratified entries in
|
||||
`frames/{category}.jsonl` reach the runtime frame-opener
|
||||
registry consumed by the comprehension reader.
|
||||
- **CW-2 — Composition consumption.** Make ratified entries in
|
||||
`compositions/{category}.jsonl` reach the runtime injector path
|
||||
(`generate/recognizer_anchor_inject.py` and adjacent) that
|
||||
composes bound quantities into a single operand.
|
||||
|
||||
Recommendation: ship as **one PR** because (a) they share the
|
||||
pack-compile + manifest-checksum mechanism, and (b) the eval-delta
|
||||
truth-test only registers if both halves are wired (the audit
|
||||
refusals span both frame and composition gaps).
|
||||
|
||||
Optionally split into PR-α (compile + frame consumption) and PR-β
|
||||
(composition consumption) if CI cycle time argues for it, but
|
||||
prefer bundled.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- ReferenceClaim and SlotClaim ratification handlers (ADR-0167
|
||||
FOLLOWUPS §1) — those need their own sub-type ADRs first.
|
||||
- Workbench UI wave (W1..W4) — orthogonal; can run in parallel.
|
||||
- New eval lanes (ADR-0166).
|
||||
- Pack-mutation autonomy — runtime still reads packs only at load
|
||||
time; no live-running registry edits.
|
||||
|
||||
---
|
||||
|
||||
## Proven pattern (template)
|
||||
|
||||
The lexicon consumer in `generate/comprehension/lexicon.py::load_lexicon`
|
||||
is the working reference:
|
||||
|
||||
```python
|
||||
# pseudocode of the proven pattern
|
||||
def load_lexicon(pack_path):
|
||||
compiled = pack_path / "lexicon.jsonl"
|
||||
manifest = json.loads((pack_path / "manifest.json").read_text())
|
||||
actual_sha = sha256(compiled.read_bytes())
|
||||
if manifest["checksum"] != actual_sha:
|
||||
raise LexiconLoadError(...)
|
||||
entries = []
|
||||
for src in sorted((pack_path / "lexicon").glob("*.jsonl")):
|
||||
for line in src.read_text().splitlines():
|
||||
entries.append(parse_entry(line))
|
||||
return Lexicon(entries, ...)
|
||||
```
|
||||
|
||||
Key invariants from CLAUDE.md ("Semantic Pack Discipline"):
|
||||
|
||||
- Manifest checksum hashes the bytes actually written to disk
|
||||
(`hashlib.sha256(Path(lexicon_path).read_bytes()).hexdigest()`).
|
||||
- Deterministic ordering: per-category files sorted alphabetically,
|
||||
entries within each file sorted by key.
|
||||
- Cache by (resolved_path, mtime, sha256) to avoid re-reading on
|
||||
every turn.
|
||||
|
||||
Both CW-1 and CW-2 mirror this pattern.
|
||||
|
||||
---
|
||||
|
||||
## CW-1 — Frame consumption wiring
|
||||
|
||||
**Branch:** `feat/composition-frame-consumption-wiring`
|
||||
(bundled with CW-2; same branch)
|
||||
**Reads required FIRST:**
|
||||
|
||||
- `generate/comprehension/lexicon.py::load_lexicon` (the template)
|
||||
- `teaching/math_frame_ratification.py` (write side — entry schema)
|
||||
- `docs/decisions/ADR-0168-frameclaim-ratification.md` §"Mutation boundary"
|
||||
- `language_packs/data/en_core_math_v1/manifest.json` (current shape)
|
||||
- The empty `language_packs/data/en_core_math_v1/frames/` directory
|
||||
|
||||
**Outcome.**
|
||||
|
||||
1. **Compile step.** Extend the pack-compile mechanism (or add a
|
||||
new sibling) that:
|
||||
- reads `frames/*.jsonl` (sorted)
|
||||
- produces a deterministic compiled artifact `frames.jsonl`
|
||||
(or folds entries into a runtime-friendly index — choose one
|
||||
and pin it)
|
||||
- regenerates `manifest.json` checksum to include the new
|
||||
compiled bytes per CLAUDE.md "Semantic Pack Discipline"
|
||||
2. **Runtime consumer.** New function (or extension of an existing
|
||||
loader) — `load_frame_registry(pack_path)` — analogous to
|
||||
`load_lexicon`, with the same caching + checksum-verification
|
||||
discipline.
|
||||
3. **Reader wire.** Plumb the loaded frame registry into the
|
||||
comprehension reader's frame-opener decision path. The current
|
||||
refusal `frame closed with no quantity` (`quantity_extraction`)
|
||||
and `multi-quantity operations are Phase-2.1 scope`
|
||||
(`multi_quantity_composition`) are the relevant refusal sites —
|
||||
identify where the reader decides those and route through the
|
||||
ratified registry.
|
||||
4. **Empty-registry safe behavior.** When `frames/` is empty
|
||||
(current state), the runtime behaves exactly as today — no
|
||||
regression. Verified by an empty-frames test.
|
||||
|
||||
**Hard requirements.**
|
||||
|
||||
- **No solver / parser / decomposer mutation.** ADR-0168 §"Mutation
|
||||
boundary" forbids it. The consumer reads only; it must not
|
||||
rewrite arithmetic semantics.
|
||||
- **Manifest checksum.** New compiled bytes participate in
|
||||
`manifest.json::checksum`; loader rejects mismatch (see lexicon
|
||||
template).
|
||||
- **Deterministic order.** Per-category files sorted; entries
|
||||
sorted within file.
|
||||
- **Empty-registry no-op.** Eval is byte-identical when `frames/`
|
||||
has zero entries.
|
||||
- **Case 0050 hazard pin.** A new test ratifies a synthetic
|
||||
FrameClaim, runs `train_sample` eval, asserts case 0050 stays
|
||||
refused.
|
||||
|
||||
---
|
||||
|
||||
## CW-2 — Composition consumption wiring
|
||||
|
||||
**Branch:** same as CW-1 (`feat/composition-frame-consumption-wiring`)
|
||||
**Reads required FIRST:**
|
||||
|
||||
- All CW-1 reads
|
||||
- `teaching/math_composition_ratification.py` (write side — entry schema)
|
||||
- `docs/decisions/ADR-0169-compositionclaim-ratification.md` §"Mutation boundary", §"Initial safe category scope"
|
||||
- `generate/recognizer_anchor_inject.py` — current injector entry points (`inject_from_match`, `inject_discrete_count_statement`)
|
||||
- `generate/math_candidate_graph.py` — where injector outputs feed the graph
|
||||
|
||||
**Outcome.**
|
||||
|
||||
1. **Compile step.** Same shape as CW-1 but for
|
||||
`compositions/*.jsonl` → `compositions.jsonl` (or runtime index).
|
||||
Manifest checksum extended again.
|
||||
2. **Runtime consumer.** `load_composition_registry(pack_path)` —
|
||||
analog of `load_lexicon` for composition patterns.
|
||||
3. **Injector wire.** Wire the composition registry into the
|
||||
injector path. Specifically: when the recognizer matches a
|
||||
structure that the existing injector currently emits as
|
||||
`recognized-but-uninjectable`, the injector consults the
|
||||
composition registry to determine if a SAFE_COMPOSITION_CATEGORIES
|
||||
pattern applies, and if so emits the composed operand.
|
||||
4. **Empty-registry safe behavior.** Same as CW-1.
|
||||
|
||||
**Hard requirements.**
|
||||
|
||||
- **Allowlist enforced at consumption.** Even though the handler
|
||||
enforces `SAFE_COMPOSITION_CATEGORIES` at write time, the
|
||||
consumer must also enforce: any entry with a category outside
|
||||
the allowlist is **rejected at load** with `WrongCompositionCategory`
|
||||
— not silently skipped. (Defense in depth; protects against a
|
||||
pack edit that bypasses the handler.)
|
||||
- **Polarity respected.** `polarity: "falsifies"` entries must
|
||||
suppress an injection that would otherwise have fired — not be
|
||||
silently treated as `affirms`. Pinned by a parametrized test.
|
||||
- **No corpus-laundering at load.** The consumer reads from the
|
||||
reviewed math pack only; it must never read cognition corpus or
|
||||
any other unsigned path.
|
||||
- **Case 0050 hazard pin.** Synthetic CompositionClaim under
|
||||
every entry in `SAFE_COMPOSITION_CATEGORIES`; assert case 0050
|
||||
remains refused after each one.
|
||||
- **`wrong=0` preserved.** Full `train_sample` eval green; full
|
||||
`core eval gsm8k_math` green.
|
||||
|
||||
---
|
||||
|
||||
## Eval-delta truth test (the real success criterion)
|
||||
|
||||
[[feedback-ratify-vs-consume-loop-closure]] — the artifact append
|
||||
is **not** the success signal. The eval delta is.
|
||||
|
||||
The PR is "done" when:
|
||||
|
||||
1. `apply_composition_claim()` on case 0019 (the canary I ratified
|
||||
2026-05-27) under `multiplicative_composition` with the same
|
||||
`surface_pattern` causes **case 0019 to admit** in
|
||||
`train_sample` eval (verdict transitions from `refused` to
|
||||
`correct`).
|
||||
2. Case 0050 still refuses (mandatory hazard pin).
|
||||
3. `train_sample` eval count moves from **3 correct / 47 refused**
|
||||
to at least **4 correct / 46 refused** (admitting at least case
|
||||
0019; better if more of the 12 `quantity_extraction` cases
|
||||
also admit under the same multiplicative pattern).
|
||||
4. `wrong == 0` invariant unchanged.
|
||||
5. `core eval gsm8k_math` (`public` split) unchanged: **150/150**.
|
||||
|
||||
A PR that lands the consumer code but doesn't move the eval needle
|
||||
is **not done** — it has the same shape as the bug this brief is
|
||||
fixing (handler writes, runtime ignores).
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/test_frame_registry_load.py` — empty-registry no-op;
|
||||
deterministic order; manifest mismatch raises
|
||||
- `tests/test_composition_registry_load.py` — same
|
||||
- `tests/test_composition_load_allowlist.py` — unsafe category at
|
||||
load raises `WrongCompositionCategory`
|
||||
- `tests/test_composition_polarity_falsifies.py` — falsifying
|
||||
entries suppress injection
|
||||
- `tests/test_composition_case_0019_admits.py` — the truth test:
|
||||
ratify case 0019, re-run train_sample reader, assert verdict
|
||||
flips from `refused` to `correct`
|
||||
- `tests/test_composition_case_0050_hazard_pin.py` — mandatory
|
||||
- `tests/test_consumption_empty_registry_no_op.py` — eval
|
||||
byte-identical when both `frames/` and `compositions/` empty
|
||||
- `tests/test_pack_manifest_checksum_includes_compiled_frames.py`
|
||||
- `tests/test_pack_manifest_checksum_includes_compiled_compositions.py`
|
||||
- end-to-end: `core eval math-contemplation` → `apply_composition_claim()`
|
||||
→ `train_sample` re-run → assert verdict delta
|
||||
|
||||
---
|
||||
|
||||
## Anti-regression invariants
|
||||
|
||||
- `wrong == 0` on `core eval gsm8k_math` preserved
|
||||
- Case 0050 stays refused after any synthetic ratification
|
||||
- ADR-0166 — no new eval lanes
|
||||
- ADR-0057 replay-equivalence inherited
|
||||
- ADR-0167 partition (math/cognition) preserved
|
||||
- Empty-registry runtime byte-identical to today
|
||||
- `SAFE_COMPOSITION_CATEGORIES` allowlist enforced at both write
|
||||
and load (defense in depth)
|
||||
- Polarity semantics (`affirms` vs `falsifies`) honored at consumer
|
||||
- `engine_state/*` never committed
|
||||
- Pinned-lane SHAs may update (intentional eval delta); call out
|
||||
the move in the PR body
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
- pack-compile mechanism extended (or new sibling) for `frames/`
|
||||
and `compositions/`
|
||||
- `generate/comprehension/frame_registry.py` (or analogous) —
|
||||
loader + cache + checksum verification
|
||||
- `generate/comprehension/composition_registry.py` (or analogous) —
|
||||
same
|
||||
- reader wire (CW-1) and injector wire (CW-2)
|
||||
- manifest schema updated to carry the new compiled-bytes checksums
|
||||
- tests above, all green
|
||||
- `core test --suite teaching -q` green
|
||||
- `core test --suite runtime -q` green
|
||||
- `core test --suite packs -q` green
|
||||
- `core eval gsm8k_math` green
|
||||
- `train_sample` eval count improved (truth test)
|
||||
|
||||
---
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Mutating solver, parser, decomposer, or arithmetic operators
|
||||
(ADR-0168 / 0169 mutation boundary)
|
||||
- Dynamic category synthesis at load time
|
||||
- Nearest-pattern guessing
|
||||
- Embedding-based composition selection
|
||||
- Silently skipping unsafe categories at load (must raise)
|
||||
- Treating `polarity: "falsifies"` as `affirms`
|
||||
- Reading from cognition corpus or any unsigned path
|
||||
- New eval lanes (ADR-0166)
|
||||
- New corpus families
|
||||
|
||||
---
|
||||
|
||||
## Sequencing
|
||||
|
||||
1. **Dispatch:** after the workbench UI W0 (ADR-0173, PR #394) does
|
||||
**not** need to land first — this brief is orthogonal to the UI
|
||||
wave and can ship anytime.
|
||||
2. **Operator profile:** Opus (load-bearing wrong=0 surface;
|
||||
case 0050 pin; same rigor as CC-2). Sonnet is feasible for
|
||||
CW-1/CW-2 if treated as tight-scope mechanical wiring with the
|
||||
lexicon template explicitly referenced — but the injector wire
|
||||
in CW-2 has enough judgment surface that Opus is the safer
|
||||
default.
|
||||
3. **Bundle vs split.** Default: one PR bundling both CW-1 and
|
||||
CW-2. Split only if CI cycle time forces it.
|
||||
|
||||
---
|
||||
|
||||
## Memory pointers
|
||||
|
||||
- [[project-ratification-consumption-gap-2026-05-27]] — the
|
||||
finding that motivated this brief
|
||||
- [[feedback-ratify-vs-consume-loop-closure]] — the general pattern
|
||||
- [[feedback-wrong-zero-hazard-case-0050]] — mandatory pin
|
||||
- [[milestone-adr-0172-tier1-2026-05-27]] — wave context
|
||||
- [[adr-0167-audit-as-evidence-wave]] — parent corridor
|
||||
- [[feedback-batch-during-research]] — bundling rule
|
||||
|
||||
---
|
||||
|
||||
## What ships when this PR lands
|
||||
|
||||
The compounding loop's **consumption half** closes for Frame and
|
||||
Composition. End-to-end:
|
||||
|
||||
```
|
||||
audit refusal
|
||||
→ core eval math-contemplation (8 proposals; CC-3 dispatch)
|
||||
→ MathFrameClaimProposal / MathCompositionClaimProposal
|
||||
→ HITL ratify via apply_*_claim()
|
||||
→ frames/{cat}.jsonl or compositions/{cat}.jsonl append
|
||||
→ pack-compile (NEW) folds entries into runtime-loadable form
|
||||
→ manifest.json checksum updated
|
||||
→ next runtime turn loads new entries
|
||||
→ reader/injector consumes ratified patterns
|
||||
→ previously-refused cases admit (verified by train_sample eval delta)
|
||||
```
|
||||
|
||||
This is the first PR where the loop **runs in full** for math
|
||||
sub-types beyond Lexical.
|
||||
|
||||
The 20 composition cases + however-many frame cases the empty
|
||||
frame registry now serves become live admission events on
|
||||
ratification. The compounding flywheel is operational.
|
||||
Loading…
Reference in a new issue