core/workbench/calibration.py
Shay bdb294eac3 feat(workbench): land B3.5-b/c/d/e — calibration evidence subject, B4a leeway gate, docs; runner-reproducible practice artifact
Completes the Wave M B3.5 consolidation slice (b–e), built on #728.

B3.5-b — calibration as a first-class evidence subject (`calibration_class`,
address `calibration:<class_name>`): RightInspector projection + Evidence
Chain Rail semantics (serving-discipline evidence, not runtime truth).

B3.5-c / B4a — nullable `LeewayEvidence` read model threaded through turn,
replay, cognition-proposal, and math-proposal surfaces, with a shared
absence-honest card. B4 is gated correctly: the tuple exists in typed data but
no producer populates it, so the card renders absence (verified: no non-null
producer in workbench/core/chat).

B3.5-d/e — UI-UX-GUIDE.md, b4-leeway-feasibility-gate.md, phase-a-residue-ledger.md.

Practice artifact — earn-it-for-real (runner-reproducible). The committed
`report.json` (additive earns PROPOSE @0.861, 95/5/50) is now emitted by a
deterministic runner rather than copied from the queue. `propose_runner`
gains `regenerate_practice_artifacts()`, which runs ONE sealed `resolve_pooled`
practice pass and writes BOTH report.json (the per-class ledger the calibration
reader consumes) and ratification_queue.json — two projections of one ledger,
coherent by construction and byte-reproducible. `runner.main()` delegates to
it (lazy import, no cycle), so both entry points produce the identical pair.
This closes the gap where a hand-copied report.json agreed with the queue but
no runner produced it. `resolve_pooled` is the aggressive sealed PROPOSE-regime
scorer (proposal-only/HITL, unsafe for serving, legitimate for
attempt-and-eliminate); wrong=5 is the sealed-practice learning signal, NOT the
serving wrong=0. No serving/derivation/reliability_gate source touched; the
practice lane is not in the serving-frozen SHA gate.

Validated:
- python -m pytest tests/test_workbench_{calibration,journal,replay,schemas}.py -> 31 passed
- python -m pytest tests/ -k "workbench or propose or learning_arena or practice"
  -> 190 passed (3 failing tests in test_adr_0175_phase2_practice_lane.py are
  PRE-EXISTING reds on clean origin/main: stale 4/0/46 assertions on build_report,
  which this change does not touch)
- report.json + ratification_queue.json: deterministic (run1==run2) and
  reproduced byte-identically by both `python -m ...runner` and `...propose_runner`
- pnpm build green; 144 UI tests across calibration/leeway/evidence/replay/
  doctrine-gates/routes-docs-drift all pass
2026-06-13 07:36:44 -07:00

134 lines
4.7 KiB
Python

"""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],
*,
source_path: str,
source_digest: str,
) -> 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,
source_path=source_path,
source_digest=source_digest,
)
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"
)
source_path = _display_path(report_path)
source_digest = _sha256_file(report_path)
rows = [
_calibration_class(
name,
counts,
source_path=source_path,
source_digest=source_digest,
)
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