Phase 3.3 of the generalization arc. Estimation (ADR-0175), deduction serving (ADR-0256) and curriculum serving (ADR-0262) had each written the same seal -> ratify -> SHA-verify -> serve-gate machinery. core/ratified_ledger.py now owns it and states the four rules once: only sealed practice writes; tamper-evidence is structural (a load that cannot reproduce content_sha256 REFUSES); ceilings are not negotiable at the call site; absent evidence is never a license. Each capability keeps a thin adapter that names its artifact and preserves its public API. One real difference is now declared rather than implied: missing_ok distinguishes a ledger a capability SHIPS with (absence = broken deployment, refuse) from one whose practice volume is still being built (absence = nothing earned yet, serve disclosed) — curriculum serving is the second kind today. Safety property is byte-identity, asserted not assumed: re-sealing the committed 25-band deduction ledger through the bridge reproduces it byte-for-byte, so no artifact and no lane pin moves. Effect: a new subject arena now needs a gold corpus and a band key, not a re-implementation of ratification — which is what §3 meant by sequencing the bridge ahead of the second subject. [Verification]: tests/test_ratified_ledger_bridge.py 8 passed; core test --suite deductive 252 passed; estimation/license test set 355 passed; committed deduction ledger byte-identical after reseal.
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""The ratified-ledger bridge (ADR-0263) — seal → ratify → SHA-verify → gate.
|
|
|
|
These tests pin the four rules the three adapters depend on, and the property
|
|
that made the extraction safe: re-sealing through the bridge reproduces the
|
|
committed artifacts byte-for-byte.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from core.ratified_ledger import (
|
|
RatifiedLedgerError,
|
|
load_sealed_ledger,
|
|
seal_artifact,
|
|
serve_license,
|
|
write_sealed_ledger,
|
|
)
|
|
from core.reliability_gate import ClassTally
|
|
|
|
|
|
def _tally(name: str, correct: int, wrong: int = 0) -> ClassTally:
|
|
return ClassTally(class_name=name, correct=correct, wrong=wrong, refused=0)
|
|
|
|
|
|
def test_seal_then_load_round_trips(tmp_path) -> None:
|
|
ledger = {"alpha": _tally("alpha", 700), "beta": _tally("beta", 12)}
|
|
path = tmp_path / "ledger.json"
|
|
write_sealed_ledger(
|
|
path, seal_artifact(ledger, schema="t_v1", note="n", provenance="p")
|
|
)
|
|
loaded = load_sealed_ledger(path)
|
|
assert set(loaded) == {"alpha", "beta"}
|
|
assert loaded["alpha"].correct == 700
|
|
|
|
|
|
def test_a_hand_edited_ledger_is_rejected(tmp_path) -> None:
|
|
"""Rule 2 — tamper-evidence is structural. Editing a tally without
|
|
re-sealing does not produce a slightly-wrong ledger; it produces an
|
|
unratified one, and loading REFUSES."""
|
|
path = tmp_path / "ledger.json"
|
|
write_sealed_ledger(
|
|
path,
|
|
seal_artifact({"alpha": _tally("alpha", 10)}, schema="t_v1", note="n", provenance="p"),
|
|
)
|
|
artifact = json.loads(path.read_text())
|
|
artifact["classes"]["alpha"]["correct"] = 9_999
|
|
path.write_text(json.dumps(artifact, indent=2, sort_keys=True) + "\n")
|
|
with pytest.raises(RatifiedLedgerError):
|
|
load_sealed_ledger(path)
|
|
|
|
|
|
def test_missing_ledger_refuses_unless_declared_optional(tmp_path) -> None:
|
|
"""A capability that SHIPS with a ledger is broken without it; one whose
|
|
practice volume is still being built legitimately has none. Neither is
|
|
answered by guessing a license."""
|
|
path = tmp_path / "absent.json"
|
|
with pytest.raises(RatifiedLedgerError):
|
|
load_sealed_ledger(path)
|
|
assert load_sealed_ledger(path, missing_ok=True) == {}
|
|
|
|
|
|
def test_absent_class_is_never_licensed() -> None:
|
|
"""Rule 4 — no evidence is not a license; the caller's ``None`` branch is
|
|
what serves the disclosed surface."""
|
|
assert serve_license("nobody", {"alpha": _tally("alpha", 700)}) is None
|
|
|
|
|
|
def test_volume_floor_is_enforced_by_the_gate() -> None:
|
|
"""A perfect but small record does not clear θ_SERVE=0.99 — the Wilson
|
|
floor is what makes a license *earned* rather than merely clean."""
|
|
small = serve_license("alpha", {"alpha": _tally("alpha", 12)})
|
|
large = serve_license("alpha", {"alpha": _tally("alpha", 720)})
|
|
assert small is not None and not small.licensed
|
|
assert large is not None and large.licensed
|
|
|
|
|
|
def test_a_single_wrong_costs_the_license() -> None:
|
|
dirty = serve_license("alpha", {"alpha": _tally("alpha", 720, wrong=1)})
|
|
assert dirty is not None and not dirty.licensed
|
|
|
|
|
|
def test_committed_deduction_ledger_reseals_byte_identically() -> None:
|
|
"""The extraction's safety property: the bridge writes what the bespoke
|
|
sealers wrote, so adopting it moves no committed artifact and no lane pin."""
|
|
from pathlib import Path
|
|
|
|
from evals.deduction_serve.practice.runner import build_sealed_artifact
|
|
|
|
committed = Path("chat/data/deduction_serve_ledger.json")
|
|
expected = json.dumps(build_sealed_artifact(), indent=2, sort_keys=True) + "\n"
|
|
assert committed.read_text(encoding="utf-8") == expected
|
|
|
|
|
|
def test_every_adapter_reads_through_the_bridge() -> None:
|
|
"""All three instances now share one loader — the property that makes a
|
|
future change to the ratification rule land everywhere at once."""
|
|
import chat.curriculum_serve_license as curriculum
|
|
import chat.deduction_serve_license as deduction
|
|
import generate.determine.estimation_license as estimation
|
|
|
|
for module in (deduction, estimation, curriculum):
|
|
source = module.__file__ or ""
|
|
assert source
|
|
text = open(source, encoding="utf-8").read()
|
|
assert "core.ratified_ledger" in text
|
|
# The signal that an adapter still verifies for itself is that it
|
|
# hashes for itself; docstrings may (and do) still explain the rule.
|
|
assert "formation.hashing" not in text, (
|
|
f"{module.__name__} still re-implements verification"
|
|
)
|