Squashes the arc's work into one commit; the workflow-file edit it originally carried is excluded (see the end of this message). ## Lane 1 — Workbench recorded a proved answer as ungrounded With deduction_serving_enabled ratified ON (ADR-0256), workbench/api.py's live chat route builds a bare ChatRuntime(), so the deduction composer decides Workbench turns and stamps grounding_source="deduction" — but _coerce_grounding_source carried a hand-copied whitelist of the six pre-arc labels and silently rewrote anything else to "none". The runtime comment reasoned this was inert because "REPL turns do not flow through Workbench's CognitivePipelineRecord path". True, and irrelevant: the traffic flows the other way. Stale since 2026-07-24. Scope is one field. workbench/api.py:818 prefers TurnEvent.epistemic_state, which read epistemic_state_needed — honest. So the UNregistered path degraded honestly while the hand-copied whitelist asserted a falsehood; a second copy of a closed enum was worse than no copy. Hence registration AND derivation: GROUNDING_SOURCES exposes the Literal's members, and the coercion reads it. workbench-ui badges/tokens/snapshot follow; enumCoverage.test.ts forces atomicity. ## Lane 2 — the ratification ceremony The discovery loop was instrumented but not closed. teaching/ratification.py turns a reviewed decision into a chain record, a corpus commit, and a receipt. Its design turns on one observation: _ratified_rows DROPS unadmissible rows silently — correct when serving, a trap when ratifying, because the file grows, the commit lands, and the band count does not move. So the ceremony refuses to call an append a ratification until it has re-read the curriculum through the real loader and seen the chain arrive; a non-admitted append is rolled back. Validation is a pre-flight courtesy, admission is the proof. Arena queue entry and ledger reseal are deliberately NOT performed (bridge rule 1); the receipt names them. Front door: `core proposal-queue ratify`, a sibling of `review` rather than a flag on it. ## Lane 3 — structural closures - ADR-0263 gains rule 5: absence policy is DECLARED in CAPABILITY_LEDGERS, not passed at the call site. An AST-matched test fails if a serving path passes missing_ok again. - Deductive suite added WHOLE to the pre-push gate: 285 tests in 29s against smoke's 216 in 62s, so no coverage trade was needed. - Smoke/CI parity assertion made bidirectional. It was one-directional, and had drifted. - test_prior_surface_deduction_binding.py pins correction binding on the deduction path. The review's diagnosis did NOT reproduce — hash_surface moves in lockstep — so it pins what is there. Mutation-checked. - Domain-keyed ADR index over 312 flat-numbered files, explicitly partial. - Arc-close brief template, plus this arc's own brief filled in against it. ## Lanes 4 and 5 — two premises falsified by measurement, one of them mine Math 4.2: baseline reproduced (correct=5 wrong=0 refused=495); all four named cases traced to one seam with each gap isolated by one-variable probes. Then the number that changes the recommendation: the gap blocking case 0000 affects 1 case in 500, the 'than' gap blocking 0001 affects 2. ADR-0251's prohibition on per-case growth now rests on a count. No reader change made. CGA: versor_condition is 0.22% of a turn, not the "~10x proof latency" I claimed — that multiplied an isolated microbenchmark by a call count and compared it to a single verdict's latency. The real cost is geometric_product at 33,986 calls/turn (~73%) via cga_inner in search paths. The obvious closed form is NOT bit-exact (954/4000 in f32); backend.vault_recall's serial fold IS (3000/3000, worst-rel 0) and is the correct target. cargo test could not run — static.crates.io is denied by the sandbox network policy — so the Rust parity question stays open and the typestate lane is carried forward, not shipped uncompiled. ## Not landed: three lines owed to .github/workflows/smoke.yml The CI smoke gate is narrower than the local one — test_pack_draft_serve_boundary.py (ADR-0253 INV-33) has been local-only, unseen because the parity pin checked one direction. The edit was authored and rejected at push for lacking the `workflow` OAuth scope, so it is recorded as a named, dated PENDING_IN_CI exception rather than dropped: the assertion still fires on any new divergence, and a second guard fires once the three land. [Verification]: pre-push gates all green — smoke 236 passed, warmed_session 10 passed, deductive 285 passed. Ratification 14, ADR index 5, CLI suites 10. Grounding/epistemic sweep 741 passed 1 skipped. workbench-ui 598 passed across 73 files, tsc -b clean. capability index 11 passed, digest unchanged. Math holdout correct=5 wrong=0 refused=495. Committed chain corpora byte-unchanged after the tests that write to them. Environment caveat: the repo pins requires-python ==3.12.13, which uv cannot fetch for linux-x86_64, so `uv sync --locked` fails. All Python runs used a scratch venv on 3.12.11 with declared deps — not the locked universe, not the full ~12k suite. The pin was left untouched. Re-run on a 3.12.13 host before treating this as merge evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FduW6Krm3PPQv3P5iwBYtx
282 lines
10 KiB
Python
282 lines
10 KiB
Python
"""The curriculum ratification ceremony — a decision must produce artifacts.
|
|
|
|
The discovery loop was instrumented but not closed: proposals reached a human,
|
|
`review_log.jsonl` recorded that they looked, and nothing converted a reviewed
|
|
decision into a ratified chain. These pin the ceremony that closes it, and in
|
|
particular the property the whole design exists for — **an append is not a
|
|
ratification until the curriculum loader is observed to admit it.**
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from core.capability.domains import DOMAIN_CORPORA, DOMAIN_PACKS
|
|
from teaching.curriculum_premises import CONNECTIVE_FAMILY, load_curriculum
|
|
from teaching.ratification import (
|
|
ChainRecord,
|
|
RatificationError,
|
|
build_chain_record,
|
|
corpus_path_for,
|
|
existing_rows,
|
|
next_chain_id,
|
|
ratify_chain,
|
|
validate_admissible,
|
|
)
|
|
|
|
|
|
REVIEWER = "test-operator"
|
|
RATIONALE = "pinned by tests/test_ratification_ceremony.py"
|
|
|
|
|
|
@pytest.fixture
|
|
def physics_corpus_restored():
|
|
"""Any test that really writes must leave the committed corpus untouched."""
|
|
_, path = corpus_path_for("physics")
|
|
original = path.read_text(encoding="utf-8")
|
|
try:
|
|
yield path
|
|
finally:
|
|
path.write_text(original, encoding="utf-8")
|
|
load_curriculum.cache_clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Construction — a decision's provenance is not optional
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_reviewer_and_rationale_are_required() -> None:
|
|
"""A ratified chain carries who ratified it and why, or the corpus loses
|
|
the audit lineage that makes it reviewable as a diff."""
|
|
for reviewer, rationale in ((" ", RATIONALE), (REVIEWER, " ")):
|
|
with pytest.raises(RatificationError, match="requires a"):
|
|
build_chain_record(
|
|
domain="physics",
|
|
subject="force",
|
|
connective="causes",
|
|
obj="energy",
|
|
reviewer=reviewer,
|
|
rationale=rationale,
|
|
)
|
|
|
|
|
|
def test_chain_id_is_deterministic_and_next_in_sequence() -> None:
|
|
assert next_chain_id("physics", "causal") == next_chain_id("physics", "causal")
|
|
seq = int(next_chain_id("physics", "causal").rsplit("-", 1)[1])
|
|
committed = [
|
|
int(str(r["chain_id"]).rsplit("-", 1)[1])
|
|
for r in existing_rows("physics")
|
|
if str(r.get("chain_id", "")).startswith("physics-causal-")
|
|
]
|
|
assert seq == max(committed) + 1
|
|
|
|
|
|
def test_family_is_derived_from_the_connective_not_supplied() -> None:
|
|
record = build_chain_record(
|
|
domain="physics",
|
|
subject="force",
|
|
connective="requires",
|
|
obj="energy",
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
assert record.operator_family == CONNECTIVE_FAMILY["requires"] == "modal"
|
|
|
|
|
|
def test_row_is_byte_compatible_with_the_committed_corpus() -> None:
|
|
"""Chain corpora are reviewed as diffs, so key order and separators are
|
|
part of the artifact. A reformatting ratification would show every row as
|
|
changed and make the real change unreviewable."""
|
|
committed = corpus_path_for("physics")[1].read_text(encoding="utf-8").splitlines()[0]
|
|
row = existing_rows("physics")[0]
|
|
rebuilt = ChainRecord(**{k: row[k] for k in ChainRecord.__slots__}).as_jsonl_line()
|
|
assert rebuilt.rstrip("\n") == committed
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Validation — every rule the loader drops on, raised loudly
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_connective_outside_the_family_table_is_refused() -> None:
|
|
with pytest.raises(RatificationError, match="outside CONNECTIVE_FAMILY"):
|
|
build_chain_record(
|
|
domain="physics",
|
|
subject="force",
|
|
connective="influences", # not in CONNECTIVE_FAMILY
|
|
obj="energy",
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
|
|
|
|
def test_untaught_term_is_refused_with_the_loader_s_reason() -> None:
|
|
"""The anti-recall boundary, enforced at ratification: a chain about a term
|
|
no mounted pack teaches cannot route, so committing it only inflates the
|
|
file."""
|
|
with pytest.raises(RatificationError, match="taught by no pack"):
|
|
build_chain_record(
|
|
domain="physics",
|
|
subject="force",
|
|
connective="causes",
|
|
obj="photosynthesis", # biology; no physics pack teaches it
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
|
|
|
|
def test_duplicate_edge_is_refused() -> None:
|
|
"""Re-teaching an edge inflates the volume count without adding coverage —
|
|
exactly the repetition-padding ADR-0262 §5 rejected as dishonest."""
|
|
row = existing_rows("physics")[0]
|
|
dup = build_chain_record(
|
|
domain="physics",
|
|
subject=row["subject"],
|
|
connective=row["connective"],
|
|
obj=row["object"],
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
with pytest.raises(RatificationError, match="duplicate edge"):
|
|
validate_admissible(dup)
|
|
|
|
|
|
def test_unreviewed_status_is_refused() -> None:
|
|
row = existing_rows("physics")[0]
|
|
record = ChainRecord(**{**{k: row[k] for k in ChainRecord.__slots__},
|
|
"chain_id": "physics-causal-900",
|
|
"subject": "entropy", "object": "temperature",
|
|
"review_status": "pending"})
|
|
with pytest.raises(RatificationError, match="only 'reviewed' is admitted"):
|
|
validate_admissible(record)
|
|
|
|
|
|
def test_every_served_domain_has_exactly_one_writable_corpus() -> None:
|
|
"""philosophy_theology lists three corpora in ``DOMAIN_CORPORA``, but only
|
|
one is writable (present in ``DOMAIN_CAPABILITY_CORPORA``), so the append
|
|
target is unambiguous today. This pins that it stays that way: the day a
|
|
second writable corpus is registered for a domain, ratification has a real
|
|
choice to make and must not make it silently."""
|
|
for domain in DOMAIN_CORPORA:
|
|
if domain not in DOMAIN_PACKS:
|
|
continue
|
|
corpus_id, path = corpus_path_for(domain)
|
|
assert corpus_id, domain
|
|
assert path.name.endswith(".jsonl"), domain
|
|
|
|
|
|
def test_ambiguous_append_target_refuses_rather_than_guessing(monkeypatch) -> None:
|
|
"""The guard itself, exercised against an injected second writable corpus.
|
|
Picking one silently would scatter a subject's curriculum across files."""
|
|
import teaching.ratification as mod
|
|
|
|
monkeypatch.setitem(
|
|
mod.DOMAIN_CORPORA, "physics", ("physics_chains_v1", "physics_chains_v2")
|
|
)
|
|
monkeypatch.setitem(
|
|
mod.DOMAIN_CAPABILITY_CORPORA,
|
|
"physics_chains_v2",
|
|
"teaching/domain_chains/physics_chains_v2.jsonl",
|
|
)
|
|
with pytest.raises(RatificationError, match="exactly one writable"):
|
|
corpus_path_for("physics")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The ceremony itself — admission is the proof, not the append
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_dry_run_reports_the_delta_without_touching_the_corpus() -> None:
|
|
before = corpus_path_for("physics")[1].read_text(encoding="utf-8")
|
|
record = build_chain_record(
|
|
domain="physics",
|
|
subject="entropy",
|
|
connective="causes",
|
|
obj="temperature",
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
receipt = ratify_chain(record, dry_run=True)
|
|
assert receipt.admitted
|
|
assert receipt.chains_after == receipt.chains_before + 1
|
|
assert corpus_path_for("physics")[1].read_text(encoding="utf-8") == before
|
|
|
|
|
|
def test_ratification_moves_the_band_the_licence_is_scored_on(
|
|
physics_corpus_restored,
|
|
) -> None:
|
|
"""End to end on the real corpus. The family count is the number that
|
|
matters: licences are scored per (subject x family), so a ratification that
|
|
moves the total but not the family has not moved a band."""
|
|
record = build_chain_record(
|
|
domain="physics",
|
|
subject="entropy",
|
|
connective="causes",
|
|
obj="temperature",
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
receipt = ratify_chain(record)
|
|
|
|
assert receipt.admitted
|
|
assert receipt.family_chains_after == receipt.family_chains_before + 1
|
|
assert receipt.chain.chain_id in {
|
|
c.chain_id for c in load_curriculum("physics").chains
|
|
}, "the ceremony reported success but the chain is not routable"
|
|
|
|
|
|
def test_receipt_names_the_stages_it_did_not_perform(
|
|
physics_corpus_restored,
|
|
) -> None:
|
|
"""Bridge rule 1: nothing outside a sealed practice run writes a ledger.
|
|
The ceremony hands those stages to the operator rather than doing them."""
|
|
record = build_chain_record(
|
|
domain="physics",
|
|
subject="entropy",
|
|
connective="causes",
|
|
obj="temperature",
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
receipt = ratify_chain(record, dry_run=True)
|
|
assert receipt.pending_stages == ("arena_queue_entry", "ledger_reseal")
|
|
|
|
|
|
def test_append_that_the_loader_would_drop_is_rolled_back(
|
|
physics_corpus_restored, monkeypatch
|
|
) -> None:
|
|
"""**The property the design exists for.**
|
|
|
|
``_ratified_rows`` drops unadmissible rows silently, which is correct at
|
|
serving time and a trap at ratification time: the file grows, the commit
|
|
lands, the band count does not move, and nobody learns why. Simulated here
|
|
by making the loader refuse to see the new row.
|
|
"""
|
|
import teaching.ratification as mod
|
|
|
|
before = physics_corpus_restored.read_text(encoding="utf-8")
|
|
record = build_chain_record(
|
|
domain="physics",
|
|
subject="entropy",
|
|
connective="causes",
|
|
obj="temperature",
|
|
reviewer=REVIEWER,
|
|
rationale=RATIONALE,
|
|
)
|
|
calls = {"n": 0}
|
|
|
|
def stuck_counts(domain: str, family: str) -> tuple[int, int]:
|
|
calls["n"] += 1
|
|
return (16, 8) # never moves, whatever we append
|
|
|
|
monkeypatch.setattr(mod, "_count_chains", stuck_counts)
|
|
|
|
with pytest.raises(RatificationError, match="did not admit it"):
|
|
ratify_chain(record)
|
|
|
|
assert physics_corpus_restored.read_text(encoding="utf-8") == before, (
|
|
"a non-admitted append must not survive — a corpus carrying rows the "
|
|
"engine ignores makes the volume ledger lie"
|
|
)
|