core/benchmarks/teaching_loop.py
Shay 82dac4b16f feat(adr-0055-0057): teaching-loop determinism benchmark — replayable learning
`core bench --suite teaching-loop [--runs N]` runs the full reviewed-
corpus extension pipeline (propose → real replay-equivalence gate →
operator accept) N times against an identical input and asserts
byte-identical artifacts every run:

  - proposal_id          (SHA-256 of canonical-JSON payload)
  - replay_baseline      (cognition lane metrics on active corpus)
  - replay_candidate     (cognition lane metrics on transient corpus)
  - regressed_metrics    (sorted tuple)
  - chain_id_written

Also reports per-iteration latency (mean / p50 / p95) and total wall.

100-run result against today's main:
  unique(proposal_id)=1  unique(baseline)=1  unique(candidate)=1
  unique(chain_id)=1     active_corpus_byte_eq=True
  mean=1.849s  p50=1.838s  p95=1.851s

The full learning loop is replayable bit-identically across N
independent invocations.  Pairs naturally with ADR-0045's 100% exact-
NIAH recall numbers — same epistemic class of guarantee, applied to
the *learning loop* itself rather than only to retrieval.  No LLM
provider can publish equivalent numbers on a learning path.

- benchmarks/teaching_loop.py — `run_teaching_loop_determinism(runs)`
  returns a typed `TeachingLoopBenchReport` with uniqueness counts,
  determinism flag, byte-identical-active-corpus flag, and latency
  distribution (mean / p50 / p95 / total).  Pure-stdlib percentile —
  no numpy dep on this path.
- benchmarks/run_benchmarks.py — `bench_teaching_loop_determinism`
  shim + `_SUITES["teaching-loop"]` registration + runs= passthrough.
- core/cli.py — `--suite teaching-loop` choice added to bench parser.
- tests/test_teaching_loop_bench.py — 5 tests pin determinism at
  small N, proposal_id SHA-256 shape, canonical chain_id layout,
  latency stats well-formedness, JSON serialisation.

Trust boundary: every write is confined to a tempdir created inside
the bench loop; the active corpus is read once at start, once at end,
and any byte difference would fail the bench.
2026-05-18 11:03:48 -07:00

222 lines
7.8 KiB
Python

"""Teaching-loop determinism benchmark.
Run the full reviewed-corpus extension pipeline (propose → replay-
equivalence gate → operator accept) N times against the same input.
Assert byte-identical artifacts every run:
- proposal_id (SHA-256 of canonical-JSON payload)
- replay baseline (cognition lane metrics on active corpus)
- replay candidate (cognition lane metrics on transient corpus)
- regressed_metrics (sorted tuple)
- corpus_append_chain_id
Also report latency:
- per-iteration wall-time (mean / p50 / p95)
- total wall-time
Trust boundary: the benchmark writes ONLY to tempdir-scoped paths.
The active teaching corpus on disk is byte-identical pre/post.
Asserted in the report and in the test.
"""
from __future__ import annotations
import shutil
import statistics
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from chat import teaching_grounding as _tg
from teaching.discovery import DiscoveryCandidate, EvidencePointer
from teaching.proposals import (
ProposalLog,
accept_proposal,
propose_from_candidate,
)
# Canonical demo candidate — identical to the learning-loop demo's
# operator-augmented payload. Same input → same artifacts on every
# iteration; that's the entire benchmark thesis.
def _canonical_candidate() -> DiscoveryCandidate:
return DiscoveryCandidate(
candidate_id="bench_canonical_001",
proposed_chain={
"subject": "thought", "intent": "cause",
"connective": "reveals", "object": "meaning",
},
trigger="would_have_grounded",
source_turn_trace="",
pack_consistent=True,
boundary_clean=True,
polarity="affirms",
claim_domain="factual",
evidence=(
EvidencePointer(
source="corpus",
ref="cause_creation_reveals_meaning",
polarity="affirms",
epistemic_status="coherent",
),
),
)
@dataclass(frozen=True, slots=True)
class _IterationArtifact:
proposal_id: str
replay_baseline: tuple[tuple[str, float], ...]
replay_candidate: tuple[tuple[str, float], ...]
regressed_metrics: tuple[str, ...]
chain_id_written: str
elapsed_s: float
@dataclass(frozen=True, slots=True)
class TeachingLoopBenchReport:
runs: int
unique_proposal_ids: int
unique_replay_baselines: int
unique_replay_candidates: int
unique_regressed_metrics: int
unique_chain_ids: int
deterministic: bool
active_corpus_byte_identical: bool
elapsed_mean_s: float
elapsed_p50_s: float
elapsed_p95_s: float
elapsed_total_s: float
sample_proposal_id: str
sample_chain_id: str
def as_dict(self) -> dict[str, Any]:
return {
"runs": self.runs,
"unique_proposal_ids": self.unique_proposal_ids,
"unique_replay_baselines": self.unique_replay_baselines,
"unique_replay_candidates": self.unique_replay_candidates,
"unique_regressed_metrics": self.unique_regressed_metrics,
"unique_chain_ids": self.unique_chain_ids,
"deterministic": self.deterministic,
"active_corpus_byte_identical": self.active_corpus_byte_identical,
"elapsed_mean_s": round(self.elapsed_mean_s, 4),
"elapsed_p50_s": round(self.elapsed_p50_s, 4),
"elapsed_p95_s": round(self.elapsed_p95_s, 4),
"elapsed_total_s": round(self.elapsed_total_s, 4),
"sample_proposal_id": self.sample_proposal_id,
"sample_chain_id": self.sample_chain_id,
}
def _freeze_metrics(d: dict[str, float]) -> tuple[tuple[str, float], ...]:
"""Convert a metrics dict to a sorted tuple-of-pairs (hashable, ordered)."""
return tuple(sorted((k, round(float(v), 6)) for k, v in d.items()))
def _percentile(values: list[float], pct: float) -> float:
"""Inclusive percentile via linear interpolation. Pure stdlib so
the bench has no numpy dependency on this path."""
if not values:
return 0.0
s = sorted(values)
if len(s) == 1:
return s[0]
k = (len(s) - 1) * pct / 100.0
lo, hi = int(k), min(int(k) + 1, len(s) - 1)
if lo == hi:
return s[lo]
return s[lo] + (s[hi] - s[lo]) * (k - lo)
def run_teaching_loop_determinism(runs: int = 10) -> TeachingLoopBenchReport:
"""Execute the full propose → replay → accept loop ``runs`` times
against the same candidate, then assert byte-identical artifacts.
Trust boundary: the active corpus is read once at the start and
once at the end; any byte difference is a defect. All writes
are confined to tempdirs created inside this function.
"""
active_path = _tg._CORPUS_PATH
active_bytes_before = active_path.read_bytes() if active_path.exists() else b""
artifacts: list[_IterationArtifact] = []
total_t0 = time.perf_counter()
for _ in range(runs):
with tempfile.TemporaryDirectory() as tmpdir:
log_path = Path(tmpdir) / "proposals.jsonl"
transient = Path(tmpdir) / "cognition_chains_v1.jsonl"
if active_path.exists():
shutil.copyfile(active_path, transient)
else:
transient.write_text("", encoding="utf-8")
log = ProposalLog(log_path)
candidate = _canonical_candidate()
t0 = time.perf_counter()
proposal = propose_from_candidate(candidate, log=log)
rec = log.find(proposal.proposal_id) or {}
ev = rec.get("replay_evidence") or {}
chain_id = accept_proposal(
proposal.proposal_id, log=log,
corpus_path=transient,
review_date="2026-05-18",
operator_note="bench",
)
elapsed = time.perf_counter() - t0
artifacts.append(_IterationArtifact(
proposal_id=proposal.proposal_id,
replay_baseline=_freeze_metrics(ev.get("baseline", {})),
replay_candidate=_freeze_metrics(ev.get("candidate", {})),
regressed_metrics=tuple(ev.get("regressed_metrics") or ()),
chain_id_written=chain_id,
elapsed_s=elapsed,
))
elapsed_total = time.perf_counter() - total_t0
elapsed_values = [a.elapsed_s for a in artifacts]
active_bytes_after = active_path.read_bytes() if active_path.exists() else b""
unique_pids = len({a.proposal_id for a in artifacts})
unique_baselines = len({a.replay_baseline for a in artifacts})
unique_candidates = len({a.replay_candidate for a in artifacts})
unique_regressed = len({a.regressed_metrics for a in artifacts})
unique_chain_ids = len({a.chain_id_written for a in artifacts})
deterministic = (
unique_pids == 1
and unique_baselines == 1
and unique_candidates == 1
and unique_regressed == 1
and unique_chain_ids == 1
)
return TeachingLoopBenchReport(
runs=runs,
unique_proposal_ids=unique_pids,
unique_replay_baselines=unique_baselines,
unique_replay_candidates=unique_candidates,
unique_regressed_metrics=unique_regressed,
unique_chain_ids=unique_chain_ids,
deterministic=deterministic,
active_corpus_byte_identical=(active_bytes_before == active_bytes_after),
elapsed_mean_s=statistics.mean(elapsed_values) if elapsed_values else 0.0,
elapsed_p50_s=_percentile(elapsed_values, 50.0),
elapsed_p95_s=_percentile(elapsed_values, 95.0),
elapsed_total_s=elapsed_total,
sample_proposal_id=artifacts[0].proposal_id if artifacts else "",
sample_chain_id=artifacts[0].chain_id_written if artifacts else "",
)
__all__ = [
"TeachingLoopBenchReport",
"run_teaching_loop_determinism",
]