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
134 lines
4.7 KiB
Python
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
|