core/evals/curriculum_serve/practice/runner.py
Shay c1d08ff9d5 feat(curriculum): sealed-practice producer for curriculum serving (Phase C)
Builds the producer `chat/curriculum_serve_license.py` already named as the
only writer of the ledger it reads — `evals.curriculum_serve.practice.runner
.seal_ledger` — which did not exist. Until now, authoring curriculum could
not earn a license at any volume.

A band's case space is ENUMERATED, not authored: every exam question that
routes to that subject (resolve_domain) whose relation is in that family, one
committed case per distinct query atom. So `committed == distinct` is not a
property the generator maintains, it is the same fact twice — the structural
form of ADR-0264 R9, and why this producer cannot develop the deduction
producer's exposure (CASES_PER_BAND=720 filled from spaces as small as 28).
CASES_PER_BAND=660 follows `evals/determination_estimation` per
DIVISION-OF-WORK §4: a ceiling on committed volume, not a quota.

11 bands, wrong=0, inflation 1.0 everywhere.

THE LEDGER IS DELIBERATELY NOT COMMITTED. The plan's Phase-1 exit criterion
asked for "a real (still-unearned) ledger"; that state is unreachable.
Reliability is commitment precision and a correct UNKNOWN is a commitment, so
conservative_floor(660,660)=0.990046 clears theta_SERVE on non-commitments
alone. Sealing today licenses FOUR bands on evidence that is 99.0%-99.98%
correct non-commitment — which ADR-0262 §5.1 rules unacceptable and which
ClassTally structurally cannot see (no verdict axis). Writing the artifact is
therefore a ratification and belongs to Phase D's explicit human-run verb.
The open outcome-mix ruling is now blocking, not academic.

Two corrections to my own earlier records, both measured:

- ADR-0264 §4.2 sized bands from per-TERM exclusivity; the router's predicate
  is per-PAIR, which is looser. Every ceiling was understated, and
  systems_software_causal is 720 not 630 — flipping it from "cannot reach
  657" to "can". 4 of 11 bands can reach the floor, not 3. Now pinned as
  measured values in BAND_ATOM_SPACE.
- `then` and `therefore` are taught philosophy_theology lemmas AND control
  words of the argument reader (`therefore` is the conclusion marker). 164
  routable atoms refuse compiled_premises_unreadable — coverage misses, never
  wrongs — in the band Phase F retargets to. The vocabulary boundary is not
  screened against the reader's grammar; a pack teaching and/or/not/if would
  open a much larger hole the same way. Recorded, not fixed (§6.5), pinned as
  a two-lemma bound.

Registers curriculum_serve in AUDIT_SOURCES, retiring the declared `None`
that was Phase C's forcing function, and adds the producer to `deductive`.

[Verification]: in-worktree on canonical CPython 3.12.13 with `uv sync
--locked`: smoke 571 (569 + 1 test + 1 parametrization), deductive 310
(291 + 19). Mutation-checked: injecting the deduction padding pattern into
band_cases turns 5 tests red including the R9 audit and the seal guard.
curriculum_serve lane unchanged at n=32 correct=32 wrong=0 anti_recall=5;
all 11 lane SHA pins match; no ledger written, so no serving change.
2026-07-26 09:52:54 -07:00

187 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Sealed-practice runner for curriculum serving (ADR-0262/0264, Phase C).
Folds the ADR-0199 ``run_practice`` engine over the enumerated atom corpus
(``generator.py``) and reads each band's ``ClassTally`` through the reliability
gate. Two outputs, exactly mirroring ``evals/deduction_serve/practice/runner.py``:
- ``run()`` — the falsifiable report: which *(subject × relation family)* bands
earned SERVE, at what committed volume, and with what verdict MIX.
- ``seal_ledger()`` — writes the SHA-sealed artifact the serving reader
(``chat/curriculum_serve_license.py``) trusts. That module's docstring already
names this function as the only writer; this is the function it named.
**The artifact is NOT committed by this arc, and that is a finding, not an
omission.** Every band with a routable space ≥657 reaches θ_SERVE on the UNKNOWN
class alone: ``physics · causal`` commits 7 taught edges and 713 correct
non-commitments, and ``conservative_floor(660, 660) = 0.990046 ≥ 0.99``. So
sealing this ledger today would flip four bands from disclosed to authoritative on
a mix that is ~99% non-entailed — which ADR-0262 §5.1 explicitly rules
unacceptable, and which the still-open outcome-mix ruling (ADR-0264 R9's recorded
non-decision; ``docs/research/distinct-evidence-audit-2026-07-25.md``) exists to
settle. The plan of record's Phase-1 exit criterion asked for a ledger that is
"real (still-unearned)"; building the producer shows that state is unreachable.
Committing the artifact is therefore a ratification, and it is left to the
explicit ``core proposal-queue reseal`` verb a human runs (Phase D).
Determinism: the corpus is enumerated + sorted (no clock, no RNG) and
``run_practice`` is a pure fold, so the sealed ledger is byte-identical across
runs — safe to commit and SHA-verify on load, whenever it is committed.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Any
from core.learning_arena.engine import run_practice
from core.ratified_ledger import seal_artifact, write_sealed_ledger
from core.reliability_gate import Action, Ceilings, ClassTally, license_for
from evals.curriculum_serve.practice.generator import (
CASES_PER_BAND,
CurriculumOracleTether,
CurriculumSolver,
all_gold_problems,
assert_practice_atoms_distinct,
)
#: The committed sealed ledger lives next to its serving READER (chat/), the
#: topology the estimation and deduction ledgers already use.
_SEALED_LEDGER_PATH = (
Path(__file__).resolve().parents[3] / "chat" / "data" / "curriculum_serve_ledger.json"
)
def build_ledger(cap: int = CASES_PER_BAND) -> dict[str, ClassTally]:
"""Run sealed practice over the enumerated corpus → per-band ledger."""
report = run_practice(
all_gold_problems(cap), CurriculumSolver(), CurriculumOracleTether()
)
return dict(report.ledger)
def build_mix(cap: int = CASES_PER_BAND) -> dict[str, dict[str, int]]:
"""Per-band GOLD verdict mix — the figure the reliability gate cannot see.
``ClassTally`` carries ``correct``/``wrong``/``refused`` and no verdict axis, so
a correct UNKNOWN is indistinguishable from a correct ENTAILED once tallied.
That is precisely why a band can clear θ_SERVE on non-commitments alone, and
why the mix has to be reported alongside the license rather than inferred from
it.
"""
tether = CurriculumOracleTether()
mix: dict[str, dict[str, int]] = {}
for problem in all_gold_problems(cap):
band = mix.setdefault(problem.class_name, {})
gold = tether.gold_answer(problem)
band[gold] = band.get(gold, 0) + 1
return mix
def run(ceilings: Ceilings | None = None, cap: int = CASES_PER_BAND) -> dict[str, Any]:
"""Build the ledger and report the SERVE verdict + mix per band."""
ceilings = ceilings if ceilings is not None else Ceilings.default()
ledger = build_ledger(cap)
mix = build_mix(cap)
classes: dict[str, Any] = {}
for band, tally in sorted(ledger.items()):
serve = license_for(tally, Action.SERVE, ceilings)
band_mix = mix.get(band, {})
entailed = band_mix.get("entailed", 0)
classes[band] = {
"correct": tally.correct,
"wrong": tally.wrong,
"refused": tally.refused,
"committed": tally.committed,
"reliability": tally.reliability,
"coverage": tally.coverage,
"serve_licensed": serve.licensed,
"serve_ratio": serve.ratio,
"gold_mix": dict(sorted(band_mix.items())),
"entailed_share": (
round(entailed / tally.committed, 6) if tally.committed else 0.0
),
}
return {
"lane": "curriculum-serve-practice",
"classes": classes,
"any_band_serve_licensed": any(c["serve_licensed"] for c in classes.values()),
"wrong_is_zero": all(c["wrong"] == 0 for c in classes.values()),
}
def build_sealed_artifact(cap: int = CASES_PER_BAND) -> dict[str, Any]:
"""The sealed-ledger dict (self-verifying ``content_sha256``)."""
return seal_artifact(
build_ledger(cap),
schema="curriculum_serve_ledger_v1",
note=(
"Sealed-practice committed ledger for curriculum-grounded serving "
"(ADR-0262, sized per ADR-0264 R9). Engine reads, never writes. "
"Ceilings stay at safe defaults (theta_SERVE=0.99). One committed case "
"per DISTINCT routable query atom, so committed == distinct evidence by "
"construction. NOTE: reliability here is dominated by correct "
"non-commitments; read `gold_mix` from the runner report before "
"treating a licensed band as demonstrated curriculum competence."
),
provenance="evals.curriculum_serve.practice.runner.seal_ledger",
)
def seal_ledger(
path: Path = _SEALED_LEDGER_PATH, cap: int = CASES_PER_BAND
) -> dict[str, Any]:
"""Regenerate + write the committed sealed ledger.
Verifies the R9 distinct-evidence invariant first: a producer that padded
could never seal, rather than sealing and being caught by the audit later.
"""
assert_practice_atoms_distinct(cap)
return write_sealed_ledger(path, build_sealed_artifact(cap))
def _full_sweep() -> dict[str, Any]:
"""Every routable atom in every band — the exhaustive agreement measurement.
Not the sealed corpus (that is capped at :data:`CASES_PER_BAND`); this is the
evidence that the cap is a sampling choice and not a hiding place. Runs the
whole ~70k-atom space, so it is a deliberate CLI invocation and not a test.
"""
return run(cap=10**9)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--seal", action="store_true",
help="regenerate + write chat/data/curriculum_serve_ledger.json "
"(a RATIFICATION — see the module docstring before running it)",
)
parser.add_argument(
"--full", action="store_true",
help="report over EVERY routable atom instead of the capped corpus (slow)",
)
args = parser.parse_args(argv)
if args.seal:
artifact = seal_ledger()
print(f"sealed {len(artifact['classes'])} bands -> {_SEALED_LEDGER_PATH}")
return 0
report = _full_sweep() if args.full else run()
for band, c in report["classes"].items():
print(
f" {band:44s} committed={c['committed']:6d} wrong={c['wrong']} "
f"refused={c['refused']:4d} reliability={c['reliability']:.5f} "
f"SERVE={str(c['serve_licensed']):5s} entailed={c['gold_mix'].get('entailed', 0):3d} "
f"({c['entailed_share']:.4%})"
)
print(
f"any_band_serve_licensed={report['any_band_serve_licensed']} "
f"wrong_is_zero={report['wrong_is_zero']}"
)
return 0 if report["wrong_is_zero"] else 1
if __name__ == "__main__":
raise SystemExit(main())