core/tests/test_ratification_ceremony.py
Claude 71bf04fb44
feat(provenance,teaching): close the lateral gaps the assessment actually found
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
2026-07-25 04:51:15 +00:00

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"
)