The user's question — "shouldn't we be running it multiple times so
it can learn? or is that part broken?" — exposed that the math
teaching loop's `ratify → admit` closure had been structurally
broken at the connector between operator ratification and runtime
visibility. The handlers wrote source files (compositions/, frames/)
that the runtime loader never read because no compile step
regenerated the runtime artifacts.
This PR fixes the gap end-to-end AND fires the first live composition
admission on the canonical pack.
Modules
-------
- language_packs/compile_pack.py — unified compile step that
regenerates frames.jsonl + compositions.jsonl + updates
manifest.{frame,composition}_checksum atomically. Idempotent.
- teaching/math_composition_ratification.py — apply_composition_claim
now calls compile_pack at end of successful ratification. Closes
the source-file→runtime-artifact gap.
- teaching/math_frame_ratification.py — same auto-compile wire for
apply_frame_claim.
- generate/math_candidate_parser.py — CandidateInitial gains optional
composition_evidence Mapping field. When populated, signals the
candidate was produced by a registry-gated composition (ADR-0169);
the value/unit/entity are DERIVED arithmetic over grounded inputs.
- generate/math_candidate_graph.py — new _composed_initial_admissible
predicate that branches on composition_evidence. Wrong=0 preserved
by requiring each composition INPUT token (count, amount) to ground
in source_span literally; the derived value is admitted because the
arithmetic over grounded inputs is deterministic.
- generate/math_candidate_graph.py — discourse-level prior_subject
tracking: capture proper-noun subjects from ALL statement sentences
(including ADR-0136.S.0 context-filler sentences that get filtered
out before the candidate loop). Without this, "John adopts a dog"
(no numbers) is dropped and the cross-sentence subject resolver for
case 0019 sees prior_subject=None.
- generate/recognizer_match.py — all four composition matchers
(ME-1 currency-per-unit same-sentence, ME-2 cross-sentence, ME-3
additive, ME-4 subtractive) now populate composition_evidence in
CandidateInitial. Also added standalone " each " / " apiece " to
_PER_UNIT_TOKENS so currency_amount detection-only matcher refuses
per-item costs instead of swallowing them.
CLIs
----
- core teaching compile-pack — explicit operator surface for
regenerating runtime artifacts. JSON output for CI integration.
- core teaching seed-recognizer — operator surface for seeding a
RatifiedRecognizer entry in the proposal log for a given
(shape_category, anchor_kind). Writes created + transition(accepted)
events directly via ProposalLog._append.
Seeded artifacts (the actual loop closure)
------------------------------------------
- proposals.jsonl: new rat1-seed-48dd2673d6ad673d RatifiedRecognizer
entry for shape_category=rate_with_currency,
anchor_kind=currency_per_unit_composition.
- compositions/multiplicative_composition.jsonl: ratified
"bound(count) × bound(unit_cost)" affirms entry sourced from
case 0019 evidence.
- compositions.jsonl + manifest.composition_checksum: compiled
runtime artifact + manifest pin (RAT-1 auto-compile).
Live result on train_sample
---------------------------
- wrong == 0 preserved (3 correct / 47 refused / 0 wrong)
- Case 0050 hazard pin holds (refused)
- public split 150/150 preserved
- Case 0019 sentence 1 ("requires 3 vet appointments, which cost
$400 each") NOW ADMITS via composition. Previously refused with
"recognizer matched but produced no injection". The refusal moved
downstream to sentence 2 (a different currency_amount detection
bottleneck that is its own follow-up).
This is the first time a composition ratification on the canonical
pack actually reaches the runtime. The flywheel turned one
revolution.
Tests
-----
- tests/test_rat1_end_to_end_admission.py — 4 new live tests:
composition statement admits on isolated synthetic problem, case
0019 cross-sentence admission, wrong=0 preserved on train_sample,
case 0050 hazard pin.
- tests/test_consumption_empty_registry_no_op.py — refactored to use
isolated synthetic packs (the canonical pack may now carry ratified
entries).
- tests/test_math_{frame,composition}_ratification.py — updated
"manifest checksum unchanged" tests to "lexicon checksum
preserved" semantics: RAT-1 auto-compile may add the new optional
checksum fields; pre-existing lexicon checksum stays untouched.
Suite results: teaching 93, packs 131 (+4), runtime 20. All green.
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""RAT-1 — unified pack compile step.
|
|
|
|
Single entry point that regenerates compiled-artifact bytes for every
|
|
optional ratification surface (frames + compositions) AND updates the
|
|
pack manifest's checksum fields atomically.
|
|
|
|
This is the missing link between operator ratification (which writes
|
|
source files under ``{pack}/frames/`` or ``{pack}/compositions/``) and
|
|
runtime visibility (which loads from compiled artifacts +
|
|
manifest-declared checksums).
|
|
|
|
Doctrine:
|
|
|
|
- The compile step is **read-only over reviewed source files**.
|
|
- Manifest mutation is the only side effect besides the compiled
|
|
artifacts. The lexicon checksum (``checksum`` field) is preserved
|
|
byte-equal — only ``frame_checksum`` and ``composition_checksum``
|
|
fields are added/updated.
|
|
- Empty source directory produces a zero-byte compiled artifact AND
|
|
``manifest_checksum_field = sha256("")`` — the loader treats this
|
|
as an empty-registry no-op.
|
|
- Idempotent: running compile twice in a row is a no-op.
|
|
|
|
Trust boundary identical to the per-surface handlers (ADR-0168 §"Mutation
|
|
boundary", ADR-0169 §"Mutation boundary"): the compile step does NOT
|
|
touch solver, parser, decomposer, arithmetic operators, runtime graph
|
|
execution, or refusal logic. It writes ``compositions.jsonl`` +
|
|
``frames.jsonl`` + ``manifest.json``. Nothing else.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from language_packs.compile_compositions import compile_compositions
|
|
from language_packs.compile_frames import compile_frames
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CompilePackReceipt:
|
|
pack_path: Path
|
|
frame_checksum: str
|
|
composition_checksum: str
|
|
frame_bytes_written: int
|
|
composition_bytes_written: int
|
|
manifest_updated: bool
|
|
|
|
|
|
def compile_pack(pack_path: Path) -> CompilePackReceipt:
|
|
"""Compile all ratification surfaces for *pack_path* + update manifest.
|
|
|
|
Returns the receipt with computed checksums + I/O counts.
|
|
"""
|
|
pack_path = Path(pack_path).resolve()
|
|
manifest_path = pack_path / "manifest.json"
|
|
if not manifest_path.exists():
|
|
raise FileNotFoundError(f"Pack manifest missing: {manifest_path}")
|
|
|
|
frame_bytes, frame_sha = compile_frames(pack_path)
|
|
comp_bytes, comp_sha = compile_compositions(pack_path)
|
|
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
before = dict(manifest)
|
|
manifest["frame_checksum"] = frame_sha
|
|
manifest["composition_checksum"] = comp_sha
|
|
updated = manifest != before
|
|
if updated:
|
|
manifest_path.write_text(
|
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
return CompilePackReceipt(
|
|
pack_path=pack_path,
|
|
frame_checksum=frame_sha,
|
|
composition_checksum=comp_sha,
|
|
frame_bytes_written=len(frame_bytes),
|
|
composition_bytes_written=len(comp_bytes),
|
|
manifest_updated=updated,
|
|
)
|
|
|
|
|
|
__all__ = ["compile_pack", "CompilePackReceipt"]
|