feat(workbench): calibration + serving-metrics readers — the gold-tether loop, visible (Wave M B1)

First Wave M / Phase B piece (GATING): read-only backend that makes the
calibrated-learning / serving-discipline loop inspectable — 'the engine
earns the right to guess', ADR-0175.

The workbench computes NONE of these numbers:
- GET /calibration/classes — per-class gold-tether view from the persisted
  practice arena ledger (evals/gsm8k_math/practice/v1/report.json per_class).
  Each class's reliability_floor is the engine's own one-sided Wilson
  conservative_floor (via ClassTally.reliability); PROPOSE (θ=0.85) / SERVE
  (θ=0.99) license verdicts come from core.reliability_gate.license_for.
  Failures-first ordering. A test proves the reader's floor equals a direct
  conservative_floor() call — no re-implementation.
- GET /serving/metrics — the live correct/refused/wrong counts read unchanged
  from the committed train_sample + holdout_dev report.json (currently
  4/46/0 and 5/495/0 — wrong=0). Never re-runs a lane.

Honest current state: the committed practice ledger's three classes
(additive/divisive/multiplicative) are all below N_MIN=10, so none has
earned a license yet — the reader shows exactly that, no fake green light.

- workbench/calibration.py: pure readers; imports core.reliability_gate;
  EvidenceUnavailableError -> 501 (fail-closed) when the artifact is absent.
- schemas + TS mirrors (CalibrationClass, ServingMetrics); both snapshots
  regenerated (deterministic); both drift gates pass.
- trust boundary: read-only over committed artifacts + engine-owned
  derivation; no execution, no mutation, no license ever changed.

Verified: 30 Python tests (incl. the no-reimplementation proof + fail-closed),
390 vitest, both schema drift gates, snapshots deterministic.
This commit is contained in:
Shay 2026-06-13 00:38:16 -07:00
parent 4e2adcc0a6
commit 1fe56e9b6f
7 changed files with 340 additions and 3 deletions

View file

@ -0,0 +1,100 @@
"""Wave M Phase B — calibration / serving-discipline readers (ADR-0175).
The load-bearing obligation: the workbench re-implements none of the engine's
calibration math. These tests prove the reader's numbers come from
``core.reliability_gate`` (``conservative_floor`` / ``license_for``), and that
the serving counts are read from the committed reports unchanged.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from core.reliability_gate import conservative_floor
from workbench import calibration
from workbench.api import WorkbenchApi
from workbench.readers import EvidenceUnavailableError
def _write_practice_report(tmp_path: Path, per_class: dict) -> Path:
path = tmp_path / "report.json"
path.write_text(
json.dumps({"adr": "0175", "regime": "practice", "per_class": per_class}),
encoding="utf-8",
)
return path
def test_serving_metrics_read_committed_counts_unchanged() -> None:
metrics = {m.lane: m for m in calibration.read_serving_metrics()}
assert "train_sample" in metrics
# The live invariant: the committed serving lane commits zero wrong answers.
assert metrics["train_sample"].wrong == 0
assert metrics["train_sample"].correct >= 0
assert metrics["train_sample"].source_digest.startswith("sha256:")
def test_calibration_classes_over_committed_report_are_honest() -> None:
# The committed practice report's classes are all below N_MIN today, so
# none has earned a license — the reader must show exactly that, not fake
# a green light.
rows = calibration.read_calibration_classes()
assert rows, "expected the committed per_class ledger to yield rows"
for row in rows:
if row.committed < 10: # N_MIN
assert row.reliability_floor == 0.0
assert row.propose_licensed is False
assert row.serve_licensed is False
def test_reader_uses_the_engine_math_not_its_own(tmp_path) -> None:
# A class that has earned PROPOSE (0.86 >= 0.85) but not SERVE (< 0.99).
report = _write_practice_report(
tmp_path,
{
"additive": {"correct": 95, "wrong": 5, "refused": 50},
"novice": {"correct": 0, "wrong": 0, "refused": 4},
},
)
rows = {r.class_name: r for r in calibration.read_calibration_classes(report)}
earned = rows["additive"]
# The reader's reliability is the engine's own Wilson floor, to the digit.
assert earned.reliability_floor == round(conservative_floor(95, 100), 9)
assert earned.committed == 100
assert earned.propose_required == 0.85 and earned.propose_licensed is True
assert earned.serve_required == 0.99 and earned.serve_licensed is False
novice = rows["novice"]
assert novice.reliability_floor == 0.0 # below N_MIN
assert novice.propose_licensed is False
def test_calibration_classes_are_failures_first(tmp_path) -> None:
report = _write_practice_report(
tmp_path,
{
"earned": {"correct": 95, "wrong": 5, "refused": 0},
"unearned": {"correct": 0, "wrong": 0, "refused": 9},
},
)
rows = calibration.read_calibration_classes(report)
# Un-licensed / lowest-reliability comes first.
assert rows[0].class_name == "unearned"
assert rows[-1].class_name == "earned"
def test_endpoints_return_items() -> None:
api = WorkbenchApi()
r1 = api.handle("GET", "/calibration/classes", b"")
assert r1.status == 200 and isinstance(r1.payload["data"]["items"], list)
r2 = api.handle("GET", "/serving/metrics", b"")
assert r2.status == 200 and isinstance(r2.payload["data"]["items"], list)
def test_missing_practice_report_is_evidence_unavailable(tmp_path) -> None:
with pytest.raises(EvidenceUnavailableError):
calibration.read_calibration_classes(tmp_path / "nope.json")

View file

@ -322,6 +322,32 @@
"metadata": "dict[str, Any]",
"versor_digest": "str | None"
}
},
"CalibrationClass": {
"fields": {
"class_name": "str",
"correct": "int",
"wrong": "int",
"refused": "int",
"committed": "int",
"reliability_floor": "float",
"coverage": "float",
"propose_required": "float",
"propose_licensed": "bool",
"serve_required": "float",
"serve_licensed": "bool"
}
},
"ServingMetrics": {
"fields": {
"lane": "str",
"correct": "int",
"refused": "int",
"wrong": "int",
"sample_count": "int",
"source_path": "str",
"source_digest": "str"
}
}
}
}

View file

@ -22,6 +22,19 @@
"payload_digest",
"payload"
],
"CalibrationClass": [
"class_name",
"correct",
"wrong",
"refused",
"committed",
"reliability_floor",
"coverage",
"propose_required",
"propose_licensed",
"serve_required",
"serve_licensed"
],
"ChatTurnResult": [
"prompt",
"surface",
@ -186,7 +199,8 @@
"trace_hash",
"timestamp",
"trace_path",
"surface_excerpt"
"surface_excerpt",
"trace_integrity"
],
"RuntimeStatus": [
"backend",
@ -197,6 +211,15 @@
"active_session_id",
"mutation_mode"
],
"ServingMetrics": [
"lane",
"correct",
"refused",
"wrong",
"sample_count",
"source_path",
"source_digest"
],
"TurnJournalEntrySchema": [
"turn_id",
"timestamp",
@ -214,6 +237,7 @@
"proposal_candidates",
"turn_cost_ms",
"checkpoint_emitted",
"trace_integrity",
"journal_digest"
],
"TurnJournalSummarySchema": [
@ -222,7 +246,8 @@
"prompt_excerpt",
"surface_excerpt",
"trace_hash",
"grounding_source"
"grounding_source",
"trace_integrity"
],
"TurnReplayComparison": [
"turn_id",

View file

@ -338,6 +338,33 @@ export interface VaultEntry {
versor_digest: string | null;
}
// Wave M Phase B — calibrated-learning / serving-discipline read views.
// reliability_floor + the license verdicts are computed by the engine
// (core.reliability_gate), never the workbench.
export interface CalibrationClass {
class_name: string;
correct: number;
wrong: number;
refused: number;
committed: number;
reliability_floor: number;
coverage: number;
propose_required: number;
propose_licensed: boolean;
serve_required: number;
serve_licensed: boolean;
}
export interface ServingMetrics {
lane: string;
correct: number;
refused: number;
wrong: number;
sample_count: number;
source_path: string;
source_digest: string;
}
// API envelope types
export interface ApiOk<T> {
ok: true;

View file

@ -18,7 +18,7 @@ from core.epistemic_state import (
epistemic_state_for_grounding_source,
normative_detail_from_verdicts,
)
from workbench import readers
from workbench import calibration, readers
from workbench.journal import DEFAULT_JOURNAL_DIR, TurnJournal, TurnJournalEntry
from workbench.readers import ArtifactTooLargeError, EvidenceUnavailableError
from workbench.replay import replay_turn
@ -205,6 +205,10 @@ class WorkbenchApi:
)
),
)
if method == "GET" and path == "/calibration/classes":
return ApiResponse(200, ok({"items": calibration.read_calibration_classes()}))
if method == "GET" and path == "/serving/metrics":
return ApiResponse(200, ok({"items": calibration.read_serving_metrics()}))
if method == "GET" and path == "/vault/summary":
return ApiResponse(200, ok(readers.read_vault_summary()))
if method == "GET" and path == "/vault/entries":

119
workbench/calibration.py Normal file
View file

@ -0,0 +1,119 @@
"""Read-only views over the calibrated-learning / serving-discipline loop.
ADR-0175. This is where "the engine earns the right to guess" becomes
inspectable. The workbench computes nothing the engine owns:
- per-class **reliability** is the engine's own one-sided Wilson
``conservative_floor`` (via ``ClassTally.reliability``), and the
**license** verdicts come from ``core.reliability_gate.license_for``
never re-implemented here;
- **serving counts** are read from the committed ``report.json`` artifacts;
no lane is ever re-run.
Trust boundary: read-only over committed artifacts + engine-owned
derivation. No execution, no mutation, no license is ever changed.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Mapping, Sequence
from core.reliability_gate import Action, Ceilings, ClassTally, license_for
from workbench.readers import (
REPO_ROOT,
EvidenceUnavailableError,
_display_path,
_read_json_object,
_sha256_file,
)
from workbench.schemas import CalibrationClass, ServingMetrics
# The persisted per-class arena ledger (sealed practice, ADR-0175).
PRACTICE_REPORT = REPO_ROOT / "evals" / "gsm8k_math" / "practice" / "v1" / "report.json"
# Committed serving lanes — their counts are the live wrong=0 evidence.
SERVING_LANES: tuple[tuple[str, Path], ...] = (
("train_sample", REPO_ROOT / "evals" / "gsm8k_math" / "train_sample" / "v1" / "report.json"),
("holdout_dev", REPO_ROOT / "evals" / "gsm8k_math" / "holdout_dev" / "v1" / "report.json"),
)
_CEILINGS = Ceilings()
def _calibration_class(class_name: str, counts: Mapping[str, Any]) -> CalibrationClass:
tally = ClassTally(
class_name,
correct=int(counts.get("correct", 0)),
wrong=int(counts.get("wrong", 0)),
refused=int(counts.get("refused", 0)),
)
propose = license_for(tally, Action.PROPOSE, _CEILINGS)
serve = license_for(tally, Action.SERVE, _CEILINGS)
return CalibrationClass(
class_name=class_name,
correct=tally.correct,
wrong=tally.wrong,
refused=tally.refused,
committed=tally.committed,
reliability_floor=round(tally.reliability, 9),
coverage=round(tally.coverage, 9),
propose_required=propose.required,
propose_licensed=propose.licensed,
serve_required=serve.required,
serve_licensed=serve.licensed,
)
def read_calibration_classes(report_path: Path = PRACTICE_REPORT) -> list[CalibrationClass]:
"""Per-class gold-tether view: what each class has earned, by the real gate."""
if not report_path.exists():
raise EvidenceUnavailableError(
"calibration evidence unavailable: practice report.json is absent "
"(run the sealed practice lane to populate the arena ledger)"
)
report = _read_json_object(report_path)
per_class = report.get("per_class")
if not isinstance(per_class, dict):
raise EvidenceUnavailableError(
"calibration evidence unavailable: report has no per_class ledger"
)
rows = [
_calibration_class(name, counts)
for name, counts in per_class.items()
if isinstance(counts, dict)
]
# Failures-first: un-licensed / lowest-reliability at the top; stable by name.
rows.sort(
key=lambda r: (r.serve_licensed, r.propose_licensed, r.reliability_floor, r.class_name)
)
return rows
def read_serving_metrics(lanes: Sequence[tuple[str, Path]] = SERVING_LANES) -> list[ServingMetrics]:
"""The live serving counts (correct / refused / wrong) from committed reports."""
out: list[ServingMetrics] = []
for lane, path in lanes:
if not path.exists():
continue
report = _read_json_object(path)
counts = report.get("counts") or {}
correct = int(counts.get("correct", 0))
refused = int(counts.get("refused", 0))
wrong = int(counts.get("wrong", 0))
out.append(
ServingMetrics(
lane=lane,
correct=correct,
refused=refused,
wrong=wrong,
sample_count=int(report.get("sample_count", correct + refused + wrong)),
source_path=_display_path(path),
source_digest=_sha256_file(path),
)
)
if not out:
raise EvidenceUnavailableError(
"serving metrics unavailable: no committed report.json found"
)
return out

View file

@ -448,3 +448,39 @@ class VaultEntry:
epistemic_state: str
metadata: dict[str, Any]
versor_digest: str | None
# ---------------------------------------------------------------------------
# Wave M Phase B — calibrated-learning / serving-discipline read views.
# The workbench computes none of these numbers: reliability_floor and the
# license verdicts come from core.reliability_gate's own conservative_floor /
# license_for; serving counts come from committed eval report.json artifacts.
# Read-only — no lane is re-run, no license is changed.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class CalibrationClass:
class_name: str
correct: int
wrong: int
refused: int
committed: int
# One-sided Wilson conservative floor (0.0 below N_MIN committed trials).
reliability_floor: float
coverage: float
propose_required: float # θ for PROPOSE (0.85)
propose_licensed: bool
serve_required: float # θ for SERVE (0.99)
serve_licensed: bool
@dataclass(frozen=True, slots=True)
class ServingMetrics:
lane: str
correct: int
refused: int
wrong: int
sample_count: int
source_path: str
source_digest: str