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).
14 KiB
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}.jsonlreach the runtime frame-opener registry consumed by the comprehension reader. - CW-2 — Composition consumption. Make ratified entries in
compositions/{category}.jsonlreach the runtime injector path (generate/recognizer_anchor_inject.pyand 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:
# 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.
- 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.jsonchecksum to include the new compiled bytes per CLAUDE.md "Semantic Pack Discipline"
- reads
- Runtime consumer. New function (or extension of an existing
loader) —
load_frame_registry(pack_path)— analogous toload_lexicon, with the same caching + checksum-verification discipline. - 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) andmulti-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. - 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_sampleeval, 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.
- Compile step. Same shape as CW-1 but for
compositions/*.jsonl→compositions.jsonl(or runtime index). Manifest checksum extended again. - Runtime consumer.
load_composition_registry(pack_path)— analog ofload_lexiconfor composition patterns. - 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. - Empty-registry safe behavior. Same as CW-1.
Hard requirements.
- Allowlist enforced at consumption. Even though the handler
enforces
SAFE_COMPOSITION_CATEGORIESat write time, the consumer must also enforce: any entry with a category outside the allowlist is rejected at load withWrongCompositionCategory— 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 asaffirms. 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=0preserved. Fulltrain_sampleeval green; fullcore eval gsm8k_mathgreen.
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:
apply_composition_claim()on case 0019 (the canary I ratified 2026-05-27) undermultiplicative_compositionwith the samesurface_patterncauses case 0019 to admit intrain_sampleeval (verdict transitions fromrefusedtocorrect).- Case 0050 still refuses (mandatory hazard pin).
train_sampleeval count moves from 3 correct / 47 refused to at least 4 correct / 46 refused (admitting at least case 0019; better if more of the 12quantity_extractioncases also admit under the same multiplicative pattern).wrong == 0invariant unchanged.core eval gsm8k_math(publicsplit) 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 raisestests/test_composition_registry_load.py— sametests/test_composition_load_allowlist.py— unsafe category at load raisesWrongCompositionCategorytests/test_composition_polarity_falsifies.py— falsifying entries suppress injectiontests/test_composition_case_0019_admits.py— the truth test: ratify case 0019, re-run train_sample reader, assert verdict flips fromrefusedtocorrecttests/test_composition_case_0050_hazard_pin.py— mandatorytests/test_consumption_empty_registry_no_op.py— eval byte-identical when bothframes/andcompositions/emptytests/test_pack_manifest_checksum_includes_compiled_frames.pytests/test_pack_manifest_checksum_includes_compiled_compositions.py- end-to-end:
core eval math-contemplation→apply_composition_claim()→train_samplere-run → assert verdict delta
Anti-regression invariants
wrong == 0oncore eval gsm8k_mathpreserved- 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_CATEGORIESallowlist enforced at both write and load (defense in depth)- Polarity semantics (
affirmsvsfalsifies) 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/andcompositions/ generate/comprehension/frame_registry.py(or analogous) — loader + cache + checksum verificationgenerate/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 -qgreencore test --suite runtime -qgreencore test --suite packs -qgreencore eval gsm8k_mathgreentrain_sampleeval 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"asaffirms - Reading from cognition corpus or any unsigned path
- New eval lanes (ADR-0166)
- New corpus families
Sequencing
- 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.
- 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.
- 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.