core/core/ratified_ledger.py
Claude 71d6366fa3
feat(ledger,tests,adr): PR-14 — the outcome-mix rule, and the four expected licences dissolve under it (R-8)
Rank 6, the last docket item before the posture statements, and the throughput
frontier N-5 called "one ruling wide". The ruling landed. It closed the frontier
rather than opening it, and that is the correct outcome.

R-8 ruled C: committing to an entailment and correctly declining to commit are
DIFFERENT capabilities, licensed on DIFFERENT evidence, and may not be pooled.

THE FINDING: split, ZERO bands license — not four.

  band                              unknown   floor      entailed  floor
  philosophy_theology_contrast          652   0.989925          8   0.000000
  philosophy_theology_modal             652   0.989925          8   0.000000
  physics_causal                        653   0.989940          7   0.000000
  systems_software_causal               653   0.989940          7   0.000000
  (pooled, the old basis)               660   0.990046  -- clears by 0.000046

N-5 recorded that "four bands would earn SERVE the moment a ledger is sealed."
That is true on the POOLED basis and false on the ruled one. Those four cleared
theta_SERVE=0.99 only because 7-8 entailments were counted alongside 652-653
correct refusals to reach 660. The licence was manufactured by the pooling, not
earned by the evidence. conservative_floor(9,9) is 0.000000 — the Wilson lower
bound at nine trials is not merely below theta, it is zero.

This is exactly why the plan blocked PR-14 on R-8 instead of sealing first and
ruling after. A ledger sealed on the pooled basis would have granted four
licences that the mix rule then had to revoke — and revoking a granted licence
is the expensive direction, as PR-12 had just demonstrated across 21 deduction
bands in this same session.

DELEGATED RULING — R-8 C's entailed floor N: no new constant.
The packet recommended "C, with a floor from A applied to the entailed
capability only," and N was never named. It does not need naming. theta_SERVE
=0.99 through conservative_floor — the bar every other capability already meets,
657 distinct correct decisions — applied to each separated capability on its own
evidence licenses nothing, by a factor of 73. Inventing a second, weaker
constant for the capability that most needs the strong one would institutionalise
two standards, which is precisely the objection that sank option B. Recorded
under the standing delegation with its reasoning, so it can be overturned.

NO LEDGER IS SEALED, AND THAT IS THE DELIVERABLE. Under the rule there is
nothing to license, and the registered missing_ok=True absence already says so
once. An artifact whose only content is its own emptiness would be a second
statement of the same fact — the defect class registered as G-23 this same week.

THE USEFUL RESULT IS THE SHAPE OF THE GAP, WHICH POOLING HAD HIDDEN.
Non-commitment serving is FOUR TO FIVE distinct query atoms short per band.
Entailed serving is ~648 short. Those are content tasks of completely different
size, and a single pooled figure could not distinguish "almost there" from "two
orders of magnitude away". Four cases is a morning's work; 648 is a program.
Nobody could see that before the split.

Delivered:
  - curriculum_serve_entailed registered in CAPABILITY_LEDGERS, missing_ok=True
    (absent = nothing licensed, the honest state), with the rule declared in the
    manifest table rather than at a call site (ADR-0263 rule 5)
  - an audit source for it — demanded immediately by the manifest's own pin
    (test_every_licensed_capability_has_an_audit_source went red the moment the
    capability was declared, which is PR-5's declared-table discipline working)
  - ADR-0264 §5 amendment recording the ruling and correcting §4.1's "four bands
    would earn SERVE" expectation. Changes no decision in that ADR;
    curriculum_serving_enabled stays False
  - tests/test_curriculum_outcome_mix.py on the gate: both capabilities pinned,
    the shortfall pinned exactly, AND the counterfactual pinned — pooling
    licenses 4 — so the argument for the rule cannot drift away from the number
    it rests on

The serving path needs no change, and that is stated rather than left implicit:
under the rule nothing is licensed, so there is no licensed-vs-disclosed branch
to route. Building that machinery now would be building for a case that cannot
occur yet.

Closes G-10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wcw2pnMBwyvmNyQg4uPEt4
2026-07-28 07:59:01 +00:00

265 lines
11 KiB
Python

"""The ratified-ledger bridge — seal → ratify → SHA-verify → serve-gate.
ADR-0175 Phase 5's consumption bridge (generalization plan Phase 3.3),
extracted from three working instances rather than designed ahead of them:
- ``generate/determine/estimation_license.py`` (ADR-0175, the first)
- ``chat/deduction_serve_license.py`` (ADR-0256, the second)
- ``chat/curriculum_serve_license.py`` (ADR-0262, the third)
All three had converged on the same artifact and the same four rules, which is
what makes this an extraction and not a speculation. The rules, stated once:
1. **The engine reads; only sealed practice writes.** A ledger is the output
of a practice run over a gold corpus, never of a serving turn. Nothing in a
serving path may call :func:`write_sealed_ledger`.
2. **Tamper-evidence is structural.** The artifact carries
``content_sha256`` over its ``classes`` table; a load that cannot reproduce
it REFUSES. A hand-edited ledger is not a slightly-wrong ledger, it is an
unratified one.
3. **Ceilings are not negotiable at the call site.** The gate always runs at
the safe defaults unless a caller passes ceilings explicitly, and no
production path does — ADR-0175 invariant #4: an engine cannot raise its own
bar.
4. **Absent evidence is never a license.** A class missing from the ledger
yields ``None``, and every caller's ``None`` branch serves the disclosed
(hedged) surface. A capability with no track record is served honestly, not
withheld and not asserted.
Byte-compatibility is deliberate: :func:`seal_artifact` and
:func:`write_sealed_ledger` reproduce the exact bytes the three existing
sealers wrote, so adopting the bridge re-seals every committed ledger
identically and no lane pin moves.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.reliability_gate import (
Action,
Ceilings,
ClassTally,
LicenseDecision,
license_for,
)
from formation.hashing import sha256_of
class RatifiedLedgerError(ValueError):
"""A committed ledger is malformed or does not verify against its own hash."""
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
@dataclass(frozen=True, slots=True)
class LedgerSpec:
"""One capability's committed ledger, and whether its absence is an error."""
capability: str
path: Path
#: ``False`` — this capability SHIPS with a sealed ledger, so a missing file
#: means a broken deployment and the load must refuse.
#: ``True`` — this capability's practice volume is still being built, so a
#: missing file honestly means "nothing earned yet" and serves disclosed.
missing_ok: bool
note: str
#: Rule 5 of the bridge: **absence policy is declared, not passed.**
#:
#: Rules 1-4 (docstring above) are all enforced structurally; this one was not.
#: ``missing_ok`` began life as a ``load_sealed_ledger`` keyword, which meant
#: each adapter chose its own answer to "is a missing ledger a broken
#: deployment, or an unearned capability?" — a question about the capability,
#: not about the call. Any new subject onboarding through the bridge could pass
#: ``missing_ok=True`` and silently downgrade a should-be-hard-refuse into a
#: disclosed hedge, and nothing would catch it.
#:
#: Declaring it here means adding a capability is a manifest edit that a
#: reviewer reads as a policy change, which is what it is. The keyword survives
#: on the primitive for tests and one-off tooling; no production adapter passes
#: it.
CAPABILITY_LEDGERS: dict[str, LedgerSpec] = {
"estimation": LedgerSpec(
capability="estimation",
path=_PROJECT_ROOT / "generate" / "determine" / "data" / "estimation_ledger.json",
missing_ok=False,
note="ADR-0175 — ships sealed with the converse-estimation gate.",
),
"deduction_serve": LedgerSpec(
capability="deduction_serve",
path=_PROJECT_ROOT / "chat" / "data" / "deduction_serve_ledger.json",
missing_ok=False,
note=(
"ADR-0256 — ships sealed; 25 bands, wrong=0. RE-COUNTED 2026-07-28 "
"(R-13): committed == DISTINCT evidence, so 4 bands hold a SERVE "
"licence and 21 decide with disclosure. The prior note said "
"'25 bands at 720/720', which counted replays as independent trials."
),
),
"curriculum_serve": LedgerSpec(
capability="curriculum_serve",
path=_PROJECT_ROOT / "chat" / "data" / "curriculum_serve_ledger.json",
missing_ok=True,
note=(
"ADR-0262 §5 — no band has earned a license from present curriculum "
"volume; every served band is DISCLOSED. Flips to False when the "
"first band earns SERVE and the ledger is committed. R-8 (ruled C, "
"2026-07-28) splits the earning basis: see 'curriculum_serve_entailed' "
"below. This entry now governs NON-COMMITMENT (correct UNKNOWN) "
"serving only."
),
),
"curriculum_serve_entailed": LedgerSpec(
capability="curriculum_serve_entailed",
path=_PROJECT_ROOT / "chat" / "data" / "curriculum_serve_entailed_ledger.json",
missing_ok=True,
note=(
"R-8, ruled C (2026-07-28) — the outcome-mix rule. Committing to an "
"entailment and correctly declining to are DIFFERENT capabilities and "
"are licensed on DIFFERENT evidence. Pooling them let a band clear "
"theta_SERVE=0.99 on correct refusals alone: the four leading bands "
"held 652-653 correct UNKNOWNs and 7-8 entailments, and only the "
"pooled 660 cleared. Split, NEITHER capability licenses anything "
"today — entailed evidence tops out at 9 (conservative_floor(9,9)=0.0 "
"against a required 657), and unknown evidence tops out at 653, four "
"short (five for two of the four bands). Absent = nothing licensed, which is the honest state."
),
),
}
def ledger_spec(capability: str) -> LedgerSpec:
"""The declared spec for *capability*, or a refusal naming the manifest."""
try:
return CAPABILITY_LEDGERS[capability]
except KeyError:
raise RatifiedLedgerError(
f"unregistered ledger capability: {capability!r} — declare it in "
"core.ratified_ledger.CAPABILITY_LEDGERS before consuming it"
) from None
def load_capability_ledger(capability: str) -> dict[str, ClassTally]:
"""Load the committed ledger for *capability* under its declared policy.
The production entry point. A caller names what it is, not how absence
should be treated — so no call site can grant itself a softer failure mode
than the capability was registered with.
"""
spec = ledger_spec(capability)
return load_sealed_ledger(spec.path, missing_ok=spec.missing_ok)
def tally_dict(tally: ClassTally) -> dict[str, Any]:
"""The committed per-class row. The field set is the contract — a reader
of an older ledger must be able to name every field it finds."""
return {
"correct": tally.correct,
"wrong": tally.wrong,
"refused": tally.refused,
"t2_verified": tally.t2_verified,
"t2_agrees_gold": tally.t2_agrees_gold,
}
def seal_artifact(
ledger: dict[str, ClassTally], *, schema: str, note: str, provenance: str
) -> dict[str, Any]:
"""The self-verifying sealed-ledger dict for *ledger*.
Classes are sorted, so the artifact is a pure function of the practice
result: the same corpus and solver seal byte-identically, which is what
makes a committed ledger reviewable as a diff.
"""
classes = {name: tally_dict(tally) for name, tally in sorted(ledger.items())}
return {
"schema": schema,
"classes": classes,
"content_sha256": sha256_of(classes),
"note": note,
"provenance": provenance,
}
def write_sealed_ledger(path: Path, artifact: dict[str, Any]) -> dict[str, Any]:
"""Write *artifact* to *path* in the committed formatting."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(artifact, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return artifact
def load_sealed_ledger(path: Path, *, missing_ok: bool = False) -> dict[str, ClassTally]:
"""Load + verify a sealed ledger → per-class ``ClassTally``.
``missing_ok`` distinguishes two genuinely different situations. A ledger
that a capability *ships with* is required: its absence means the
deployment is broken, and refusing is right. A ledger for a capability
whose practice volume is still being built is legitimately absent, and the
honest reading of "no file" is "no class has earned anything yet" — an
empty table, every answer disclosed. Neither case may be answered by
guessing a license.
"""
if not path.exists():
if missing_ok:
return {}
raise RatifiedLedgerError(f"ratified ledger not found: {path}")
try:
artifact = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RatifiedLedgerError(f"cannot read ratified ledger: {exc}") from exc
classes = artifact.get("classes") if isinstance(artifact, dict) else None
if not isinstance(classes, dict):
raise RatifiedLedgerError("ratified ledger has no 'classes' table")
if sha256_of(classes) != artifact.get("content_sha256"):
raise RatifiedLedgerError(
"ratified ledger content_sha256 mismatch — not the sealed-practice output"
)
return {
name: ClassTally(
class_name=name,
correct=int(counts.get("correct", 0)),
wrong=int(counts.get("wrong", 0)),
refused=int(counts.get("refused", 0)),
t2_verified=int(counts.get("t2_verified", 0)),
t2_agrees_gold=int(counts.get("t2_agrees_gold", 0)),
)
for name, counts in classes.items()
}
def serve_license(
class_name: str,
ledger: dict[str, ClassTally],
*,
ceilings: Ceilings | None = None,
) -> LicenseDecision | None:
"""The ``Action.SERVE`` verdict for *class_name*, or ``None`` when the
class has no committed evidence (never a license — rule 4)."""
tally = ledger.get(class_name)
if tally is None:
return None
return license_for(tally, Action.SERVE, ceilings or Ceilings.default())
__all__ = [
"CAPABILITY_LEDGERS",
"LedgerSpec",
"RatifiedLedgerError",
"ledger_spec",
"load_capability_ledger",
"load_sealed_ledger",
"seal_artifact",
"serve_license",
"tally_dict",
"write_sealed_ledger",
]