core/evals/anti_regression/run_demo.py
Shay 8f1903e8e7
chore(evals): contracts + bench json + Lane B viewer + chart + audit + demo schema (#62)
* chore(evals, cli): contract standardization + bench --json stdout cleanliness

End-of-session shippability pass.  Three concrete fixes:

1. core/cli.py — bench --json no longer pollutes stdout
   Several bench paths call scripts.run_pulse.run_pulse which prints
   verbose [pulse] traces unconditionally to stdout, breaking jq /
   programmatic consumers of --json output.

   New _bench_stdout_guard() redirects stdout → stderr for the
   duration of the bench run when --json is set.  Operator still sees
   the pulse trace (on stderr), but --json consumers get a clean JSON
   document on stdout.  Applied to all four bench paths: cost,
   articulation, default suite, and --suite all.

   Verified: core bench --suite determinism --json now produces
   parseable JSON; human path still shows 1140 [pulse] lines.

2. evals/{frontier_compare,realizer_guard}/contract.md (new)
   core/contemplation/contract.md (new)

   Each new contract follows the established pattern (37 contracts
   already exist under evals/<lane>/contract.md):

     - What it measures
     - Why it matters (structural win)
     - How to run
     - How to read the output
     - Pass criteria table
     - When it has failed and why
     - Runner / module layout

   Coverage:
     - frontier_compare: both Lane A (CORE-only suites) and Lane B
       (cross-provider prompt_battery) with explicit guardrails
       against mixing — operator asks for the wrong lane combination,
       runner exits 2 with helpful error.
     - realizer_guard: C1/C2 articulation safety boundary — synthetic
       illegal candidates rejected directly by check_surface AND
       former-bug runtime prompts now produce legal articulations.
     - contemplation (ADR-0080): not under evals/ since it's runtime
       infrastructure that consumes eval reports — contract lives at
       core/contemplation/contract.md.  Documents the read-only +
       SPECULATIVE-only + deterministic-replay invariants and the
       shared DiscoveryCandidateSink plumbing convergence (ADR-0080).

3. evals/CLAIMS.md — Tier 2 rows added

   - frontier_compare Lane A: determinism.primary_score, max_versor_condition
   - frontier_compare Lane B: prompt_battery.primary_score (CORE adapter),
     cross-provider artifact persistence
   - realizer_guard: all_claims_supported
   - contemplation: SPECULATIVE-only invariant, deterministic replay,
     additive sink path, no pack mutation (all CI-pinned by tests)

Verification
------------
$ core test --suite smoke -q
67 passed in 27.22s    (no regression)

$ uv run pytest -q tests/test_contemplation_loop.py \
    tests/test_contemplation_pipeline_convergence.py \
    tests/test_frontier_compare_cross_provider.py
27 passed in 4.87s

$ core bench --suite determinism --json 2>/dev/null | jq .results[0].passed
true        (was: JSONDecodeError on prior [pulse] pollution)

* feat(evals/ui): report viewer renders Lane B cross-provider + pass-rate chart

Stop-hook caught that #62 only covered contracts — the 929-line
report_viewer.html was never audited against the new cross-provider
report shape from #61.  Two real gaps:

1. Lane-aware observation drawer
   The drawer hardcoded Lane A (CORE-native) fields: surface,
   grounding_source, anchor_lens_mode_label, versor_condition.
   Lane B (cross-provider) observations carry different fields:
   provider, model, elapsed_ms, error_type, error_message.

   Loading a cross-provider report rendered only the surface row
   with empty `grounding` — the provider + model + timing data
   was unreachable without expanding "Show raw JSON".

   Fix: detect Lane B (presence of `obs.provider`) and render the
   appropriate field set.  Lane A still renders identically (now
   also surfaces trace_hash + register_id when present, which were
   silently buried in the raw JSON before).

2. Pass-rate chart per suite
   The summary strip showed one aggregate Primary % across all
   suites, with no way to see WHICH suite is dragging the score.
   Multi-suite runs (e.g. --suite all) had to expand each panel
   individually to find the failing one.

   Fix: new .passrate-chart element below the summary strip,
   one horizontal bar per suite showing passed/total.  All-pass =
   solid green, all-fail = solid red, partial = green/red split
   at the pass fraction.  CSS only — no new dependencies.

3. SUITE_PREAMBLES gains the prompt_battery entry so the sidebar
   shows the "side-by-side surface evidence across providers"
   description when loading a Lane B report.

Verified
--------
- Brace/paren/div balance unchanged (308/308 / 380/380 / 54/54)
- One <script> tag pair preserved
- Generated a real Lane B report via
  `python -m evals.frontier_compare --provider core --suite prompt_battery`
  for visual confirmation

Out of scope (noted for future PR)
----------------------------------
Sampled 3 `core demo` targets:
- register-tour: clean schema (all_claims_supported, claims, grid)
- audit-tour: both scene_1_* keys AND an empty scenes:[] array — inconsistent
- anti-regression: no all_claims_supported key, uses all_gates_held instead

Demo schema standardization deserves its own PR — operator tooling
would benefit from a uniform top-level success field across demos.

* docs(evals) + chore(demos): systematic audit + uniform success field

Stop-hook caught two real gaps after the contract+UI PR:
- demos had divergent success-field names (all_gates_held vs
  learning_loop_closed vs claim_supported vs nested claims_supported)
- no systematic look at the 48 eval directories had been done

Both addressed concretely; remaining work captured in audit doc
rather than vaguely deferred.

1. Demo schema standardization — uniform all_claims_supported field
----------------------------------------------------------------------
All 9 ``core demo`` targets now emit a top-level
``all_claims_supported: bool`` field.  Existing per-demo fields
(``all_gates_held``, ``learning_loop_closed``, ``claim_supported``,
nested ``claims_supported``) are preserved for backwards compat —
the new field is an alias derived from the demo's existing success
signal, not a replacement.

Operator tooling and the CI gate can now target
``all_claims_supported`` without knowing each demo's idiomatic
field name.

Files touched:
- evals/anti_regression/run_demo.py — adds AND of all_gates_held +
  active_corpus_byte_identical
- evals/learning_loop/run_demo.py — adds AND of learning_loop_closed +
  active_corpus_byte_identical
- scripts/publish_pack_measurements.py — adds AND of the three
  entries in the nested claims_supported dict
- evals/long_context_cost/comparison_runner.py — adds alias for
  claim_supported (singular)

The 5 demos already using ``all_claims_supported`` (audit-tour,
register-tour, anchor-lens-tour, orthogonality-tour, articulation)
are unchanged.

Verified across all 9 demos:
  audit-tour              : True
  register-tour           : True
  anchor-lens-tour        : True
  orthogonality-tour      : True
  pack-measurements       : True   ← new alias
  anti-regression         : True   ← new alias
  learning-loop           : True   ← new alias
  articulation            : True
  long-context-comparison : True   ← new alias

2. docs/EVAL_AUDIT_2026-05-20.md — systematic 48-lane audit
------------------------------------------------------------
Replaces the "future PR" deferral with a concrete document.

Contains:
- Method (what was inspected for each lane).
- Summary (40/48 have contract.md; 18/48 have saved results;
  empty results/ ≠ broken — most lanes regenerate on demand).
- Cross-provider relevance triage:
    * 9 lanes are cross-provider-relevant and could benefit
      from the prompt_battery-style adapter pattern (cognition,
      english_fluency_ood, hebrew_fluency, koine_greek_fluency,
      grammatical_coverage, inference_closure, multi_step_reasoning,
      discourse_paragraph, foundational_*_ood, etc.).
    * 29 lanes are CORE-only by design (versor closure, anchor
      lens, identity divergence, provenance, etc.) — wiring
      providers would be category-erroneous.
- Demo schema standardization status (this PR closes that).
- UI/UX coverage matrix.
- 5 concrete follow-up items, each focused enough for a single
  PR, none requiring architectural change.

Regenerated reports
-------------------
evals/long_context_cost/results/comparison_v1.json and
evals/results/phase2_pack_measurements.json now contain the new
all_claims_supported field (auto-regenerated when validating the
schema change).

evals/frontier_compare/results/sample_core_promptbattery.json
added as a reference Lane B report so the new viewer always has
something to load on first open.
2026-05-20 13:53:13 -07:00

437 lines
15 KiB
Python

"""Anti-regression demo — three scenes showing how CORE refuses to learn
something that would make it worse.
The thesis: when a system extends its own knowledge, **the gate that
decides what to admit is the load-bearing part** — not the proposer.
CORE's reviewed-corpus extension path (ADR-0057) has three independent
gates that must each pass before any byte is written:
S1. Eligibility predicate (mechanical, pre-replay).
Five mechanical checks on the candidate's shape (polarity,
evidence-floor, claim-domain, boundary-clean, chain-complete).
Ineligible candidates raise ``ProposalError`` and never enter
the proposal log.
S2. Replay-equivalence gate (mechanical, post-eligibility).
The full cognition lane runs against the active corpus AND
against a transient copy with the proposed chain appended.
Any strict-decrease in a watched metric auto-rejects the
proposal with the metrics named in the operator note.
Active corpus file bytes are byte-identical pre/post.
S3. Operator review (manual, post-replay).
Even a replay-equivalent proposal only reaches the *pending*
state — explicit ``core teaching review <id> --accept`` is
required to write to the active corpus.
This demo runs each scene end-to-end against the real ``ProposalLog``
in an isolated temp directory. No active corpus or production log is
touched.
Scenes 1 and 3 use the **real** ``teaching.replay.run_replay_equivalence``
function. Scene 2 injects a controlled replay function (via the
documented ``run_replay=`` kwarg of ``propose_from_candidate``) that
returns a regressed ``ReplayEvidence`` of the same shape the real gate
produces — demonstrating the auto-rejection lifecycle on a synthetic
regression deterministically. In production the real gate produces
this same shape when a real regression is detected.
"""
from __future__ import annotations
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from teaching.discovery import DiscoveryCandidate, EvidencePointer
from teaching.proposals import (
ProposalError,
ProposalLog,
ReplayEvidence,
propose_from_candidate,
)
_VERBOSE = True
def _say(*args: Any, **kwargs: Any) -> None:
if _VERBOSE:
print(*args, **kwargs)
def _print_header(title: str, claim: str) -> None:
_say()
_say("" * 72)
_say(f" {title}")
_say("" * 72)
_say(f" CLAIM: {claim}")
_say()
# ---------------------------------------------------------------------------
# Synthetic ReplayEvidence builder for Scene 2
# ---------------------------------------------------------------------------
def _make_regressed_replay(
*, regressed_metrics: tuple[str, ...]
) -> Any:
"""Return a ``run_replay`` function that emits a regressed
``ReplayEvidence`` with the same shape the real gate produces.
"""
baseline = {
"intent_accuracy": 1.0,
"surface_groundedness": 1.0,
"term_capture_rate": 0.9167,
"versor_closure_rate": 1.0,
}
candidate = dict(baseline)
for m in regressed_metrics:
candidate[m] = round(candidate[m] - 0.0833, 4)
def _fn(chain: dict[str, Any]) -> ReplayEvidence: # noqa: ARG001
return ReplayEvidence(
baseline=baseline,
candidate=candidate,
regressed_metrics=tuple(sorted(regressed_metrics)),
replay_equivalent=False,
)
return _fn
# ---------------------------------------------------------------------------
# Candidate builders
# ---------------------------------------------------------------------------
def _candidate_undetermined() -> DiscoveryCandidate:
"""A candidate that fails the eligibility predicate at the polarity
gate. Used for Scene 1."""
return DiscoveryCandidate(
candidate_id="demo_undetermined_001",
proposed_chain={
"subject": "wisdom", "intent": "cause",
"connective": "informs", "object": "judgment",
},
trigger="would_have_grounded",
source_turn_trace="demo_trace_001",
pack_consistent=True,
boundary_clean=True,
polarity="undetermined",
claim_domain="factual",
evidence=(
EvidencePointer(
source="corpus",
ref="cause_wisdom_orders_judgment",
polarity="affirms",
epistemic_status="reviewed",
),
),
)
def _candidate_for_regression() -> DiscoveryCandidate:
"""A candidate that passes eligibility but (under the injected
regression replay) is auto-rejected for regressing
``surface_groundedness`` and ``term_capture_rate``."""
return DiscoveryCandidate(
candidate_id="demo_regression_002",
proposed_chain={
"subject": "knowledge", "intent": "cause",
"connective": "obscures", "object": "wisdom",
},
trigger="would_have_grounded",
source_turn_trace="demo_trace_002",
pack_consistent=True,
boundary_clean=True,
polarity="affirms",
claim_domain="factual",
evidence=(
EvidencePointer(
source="corpus",
ref="cause_knowledge_requires_evidence",
polarity="affirms",
epistemic_status="reviewed",
),
),
)
def _candidate_pass_through() -> DiscoveryCandidate:
"""A candidate that passes both eligibility and the real
replay-equivalence gate. Lands in ``pending`` awaiting
operator review."""
return DiscoveryCandidate(
candidate_id="demo_pass_003",
proposed_chain={
"subject": "judgment", "intent": "verification",
"connective": "requires", "object": "evidence",
},
trigger="would_have_grounded",
source_turn_trace="demo_trace_003",
pack_consistent=True,
boundary_clean=True,
polarity="affirms",
claim_domain="factual",
evidence=(
EvidencePointer(
source="corpus",
ref="verification_truth_requires_evidence",
polarity="affirms",
epistemic_status="reviewed",
),
),
)
# ---------------------------------------------------------------------------
# Scene results
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class SceneResult:
scene: str
claim: str
outcome: str
candidate_id: str
proposed_chain: dict[str, Any]
proposal_id: str | None
review_state: str
replay_evidence: dict[str, Any] | None
operator_note: str
error: str | None
corpus_byte_identical: bool
def as_dict(self) -> dict[str, Any]:
return {
"scene": self.scene,
"claim": self.claim,
"outcome": self.outcome,
"candidate_id": self.candidate_id,
"proposed_chain": self.proposed_chain,
"proposal_id": self.proposal_id,
"review_state": self.review_state,
"replay_evidence": self.replay_evidence,
"operator_note": self.operator_note,
"error": self.error,
"corpus_byte_identical": self.corpus_byte_identical,
}
@dataclass(frozen=True, slots=True)
class DemoReport:
scenes: tuple[SceneResult, ...]
all_gates_held: bool
active_corpus_byte_identical: bool
def as_dict(self) -> dict[str, Any]:
# ``all_claims_supported`` is the canonical cross-demo success
# field — added as an alias so operator tooling (and the CI gate)
# can rely on one uniform boolean key across every ``core demo``
# target. Existing fields are preserved for backwards compat.
return {
"scenes": [s.as_dict() for s in self.scenes],
"all_gates_held": self.all_gates_held,
"active_corpus_byte_identical": self.active_corpus_byte_identical,
"all_claims_supported": (
self.all_gates_held and self.active_corpus_byte_identical
),
}
# ---------------------------------------------------------------------------
# Scenes
# ---------------------------------------------------------------------------
def _read_active_corpus_bytes() -> bytes:
from chat.teaching_grounding import _CORPUS_PATH
return _CORPUS_PATH.read_bytes() if _CORPUS_PATH.exists() else b""
def _scene1_eligibility_gate(log_path: Path) -> SceneResult:
_print_header(
"S1. Eligibility predicate refuses ineligible candidates",
"An undetermined-polarity candidate never enters the proposal "
"log. ProposalError raised; no log row; no replay invocation.",
)
log = ProposalLog(log_path)
candidate = _candidate_undetermined()
bytes_before = _read_active_corpus_bytes()
error: str | None = None
try:
propose_from_candidate(candidate, log=log)
except ProposalError as exc:
error = str(exc)
bytes_after = _read_active_corpus_bytes()
_say(f" candidate.polarity : {candidate.polarity}")
_say(f" outcome : ProposalError raised")
_say(f" error : {error}")
_say(f" proposal log rows : {len(log.current_state())}")
_say(f" active corpus byte-eq : {bytes_before == bytes_after}")
return SceneResult(
scene="S1_eligibility_gate",
claim=(
"Five mechanical eligibility gates fire before any replay "
"is invoked. Undetermined-polarity candidates never enter "
"the proposal log."
),
outcome="rejected_pre_replay",
candidate_id=candidate.candidate_id,
proposed_chain=candidate.proposed_chain,
proposal_id=None,
review_state="(not in log)",
replay_evidence=None,
operator_note="",
error=error,
corpus_byte_identical=(bytes_before == bytes_after),
)
def _scene2_replay_auto_reject(log_path: Path) -> SceneResult:
_print_header(
"S2. Replay-equivalence gate auto-rejects a regressing chain",
"An eligible candidate whose append would regress the cognition "
"lane is auto-rejected with the named regressed metrics in the "
"operator note. Active corpus byte-identical pre/post.",
)
log = ProposalLog(log_path)
candidate = _candidate_for_regression()
bytes_before = _read_active_corpus_bytes()
proposal = propose_from_candidate(
candidate,
log=log,
run_replay=_make_regressed_replay(
regressed_metrics=("surface_groundedness", "term_capture_rate"),
),
)
bytes_after = _read_active_corpus_bytes()
rec = log.find(proposal.proposal_id) or {}
ev = rec.get("replay_evidence") or {}
_say(f" proposal_id : {proposal.proposal_id}")
_say(f" baseline metrics : {ev.get('baseline')}")
_say(f" candidate metrics : {ev.get('candidate')}")
_say(f" regressed_metrics : {ev.get('regressed_metrics')}")
_say(f" replay_equivalent : {ev.get('replay_equivalent')}")
_say(f" state : {rec.get('state')}")
_say(f" operator_note : {rec.get('operator_note')}")
_say(f" active corpus byte-eq : {bytes_before == bytes_after}")
return SceneResult(
scene="S2_replay_auto_reject",
claim=(
"Replay-equivalence gate compares the full cognition lane "
"metrics; any strict-decrease auto-rejects with the regressed "
"metric names in the operator note. Active corpus untouched."
),
outcome="auto_rejected_on_regression",
candidate_id=candidate.candidate_id,
proposed_chain=candidate.proposed_chain,
proposal_id=proposal.proposal_id,
review_state=str(rec.get("state")),
replay_evidence=ev,
operator_note=str(rec.get("operator_note") or ""),
error=None,
corpus_byte_identical=(bytes_before == bytes_after),
)
def _scene3_real_gate_pass_through(log_path: Path) -> SceneResult:
_print_header(
"S3. Real replay gate runs cognition lane; pass → pending",
"An eligible candidate whose append does not regress reaches "
"'pending' state. Operator --accept is still required to write "
"to the active corpus; the gate is a precondition, not a "
"permission.",
)
log = ProposalLog(log_path)
candidate = _candidate_pass_through()
bytes_before = _read_active_corpus_bytes()
proposal = propose_from_candidate(candidate, log=log)
bytes_after = _read_active_corpus_bytes()
rec = log.find(proposal.proposal_id) or {}
ev = rec.get("replay_evidence") or {}
_say(f" proposal_id : {proposal.proposal_id}")
_say(f" baseline metrics : {ev.get('baseline')}")
_say(f" candidate metrics : {ev.get('candidate')}")
_say(f" regressed_metrics : {ev.get('regressed_metrics')}")
_say(f" replay_equivalent : {ev.get('replay_equivalent')}")
_say(f" state : {rec.get('state')}")
_say(f" next step : core teaching review {proposal.proposal_id} "
"--accept --review-date YYYY-MM-DD")
_say(f" active corpus byte-eq : {bytes_before == bytes_after}")
return SceneResult(
scene="S3_real_gate_pass_through",
claim=(
"A replay-equivalent candidate reaches 'pending' but is "
"not auto-applied. Operator --accept is the third gate."
),
outcome="pending_awaiting_operator",
candidate_id=candidate.candidate_id,
proposed_chain=candidate.proposed_chain,
proposal_id=proposal.proposal_id,
review_state=str(rec.get("state")),
replay_evidence=ev,
operator_note="",
error=None,
corpus_byte_identical=(bytes_before == bytes_after),
)
# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------
def run_demo(*, emit_json: bool = False) -> dict[str, Any]:
"""Run all three scenes and return a structured report."""
global _VERBOSE
_VERBOSE = not emit_json
active_bytes_before = _read_active_corpus_bytes()
with tempfile.TemporaryDirectory() as tmpdir:
log_path = Path(tmpdir) / "demo_proposals.jsonl"
s1 = _scene1_eligibility_gate(log_path)
s2 = _scene2_replay_auto_reject(log_path)
s3 = _scene3_real_gate_pass_through(log_path)
active_bytes_after = _read_active_corpus_bytes()
scenes = (s1, s2, s3)
all_gates_held = (
s1.outcome == "rejected_pre_replay"
and s2.outcome == "auto_rejected_on_regression"
and s3.outcome == "pending_awaiting_operator"
)
report = DemoReport(
scenes=scenes,
all_gates_held=all_gates_held,
active_corpus_byte_identical=(active_bytes_before == active_bytes_after),
)
if _VERBOSE:
_say()
_say("" * 72)
_say(" RESULT")
_say("" * 72)
_say(f" all three gates held : {report.all_gates_held}")
_say(f" active corpus byte-eq : {report.active_corpus_byte_identical}")
_say()
_say(
" Each gate is independent and fails closed. Bad proposals "
"stop at the cheapest applicable gate. The active corpus is "
"never written to anywhere in this demo."
)
_say()
return report.as_dict()
__all__ = ["run_demo"]