Closes audit Finding 3 (2026-05-20).
Pre-fix ``ratify_intent`` defaulted to ``threshold=0.0``, which admits
anything with non-negative ``cga_inner(prompt, anchor)`` — the field
gate (ADR-0022 §TBD-1) was structurally live but semantically
transparent. RATIFIED was logged on essentially every turn because
the CGA inner product over conformal space is not sign-symmetric.
Measurement (``scripts/calibrate_ratification_threshold.py``):
* Runs every cognition eval prompt (45 cases = 13 public + 13 dev +
19 holdout) through a primed ``CognitiveTurnPipeline``.
* Captures the actual ``cga_inner(prompt, anchor)`` score from the
pipeline's own ``_ratify_intent`` via a temporary spy on the
imported ``ratify_intent`` binding.
Observed distribution:
* 34 RATIFIED: min=+1.1039 p10=+1.1039 median=+2.6820 max=+5.7508
* 11 PASSTHROUGH (no vocab-grounded anchor available; score=0.0)
* 0 DEMOTED at any threshold ≤ 1.10
Threshold = 0.5 chosen as the calibrated default:
* Well below the empirical floor of 1.10 — every currently-passing
case stays RATIFIED, byte-identically.
* Clearly non-trivially positive — random Cl(4,1) inner products
fluctuate around zero, so 0.5 demands genuine correlation with
the anchor rather than passive non-negativity.
* Leaves headroom for the gate to actually demote weakly-aligned
off-corpus / adversarial prompts to UNKNOWN and route them
through the honest-refusal surface.
Verification:
* ``core eval cognition`` — public 100/100/91.7/100, holdout
100/100/83.3/100, dev 100/100/78.6/100 — byte-identical to
MEMORY baselines.
* ``core test --suite cognition`` — 120/0/1
* ``core test --suite smoke`` — 67/0
* ``core test --suite runtime`` — 19/0
* 2 new tests in ``tests/test_ratification_threshold_default.py``
pin both the constant and the signature default so a future
change cannot silently regress to ``0.0``.
141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
"""Measure the ratification score distribution across cognition eval cases.
|
|
|
|
Finding 3 (audit 2026-05-20). The current default ``threshold=0.0`` in
|
|
``generate.intent_ratifier.ratify_intent`` admits anything with non-
|
|
negative projection — the field gate is structurally live but
|
|
semantically inactive.
|
|
|
|
This script runs every cognition eval prompt through a fresh
|
|
``ChatRuntime``, captures the raw ``cga_inner(prompt, anchor)`` score
|
|
emitted by ``ratify_intent``, and prints the distribution by split and
|
|
intent. Use the output to choose a calibrated threshold (the audit
|
|
suggests the ~10th percentile of the RATIFIED distribution as a
|
|
starting point so the bottom decile of weakly-aligned transitions
|
|
demotes to UNKNOWN without breaking any case that currently passes).
|
|
|
|
Run::
|
|
|
|
uv run python scripts/calibrate_ratification_threshold.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import statistics
|
|
from pathlib import Path
|
|
|
|
from chat.runtime import ChatRuntime
|
|
from core.cognition import CognitiveTurnPipeline
|
|
from generate.intent import classify_intent
|
|
from generate.intent_ratifier import ratify_intent
|
|
|
|
|
|
SPLITS = {
|
|
"public": Path("evals/cognition/public/v1/cases.jsonl"),
|
|
"dev": Path("evals/cognition/dev/cases.jsonl"),
|
|
"holdout": Path("evals/cognition/holdouts/cases_plaintext.jsonl"),
|
|
}
|
|
|
|
|
|
def _load_cases(path: Path) -> list[dict]:
|
|
if not path.exists():
|
|
return []
|
|
with path.open() as fh:
|
|
return [json.loads(line) for line in fh if line.strip()]
|
|
|
|
|
|
_capture_buffer: list[dict] = []
|
|
|
|
|
|
def _capture_score(prompt: str) -> dict:
|
|
"""Run the prompt through the pipeline and capture the ratification score.
|
|
|
|
The pipeline's own ``_ratify_intent`` runs inside ``run()`` after the
|
|
chat path has primed the session state. We monkeypatch
|
|
``ratify_intent`` for the duration of the call so the captured score
|
|
reflects the exact same field-versor / anchor pair the production
|
|
path uses — no parallel state-priming logic to drift.
|
|
|
|
The pipeline captures ``field_state_before`` ahead of any ingest
|
|
for this prompt, so a fresh runtime returns None on turn 1 and the
|
|
ratifier short-circuits to PASSTHROUGH. We run a no-op prime turn
|
|
first so the score-bearing call reflects a real session.
|
|
"""
|
|
rt = ChatRuntime()
|
|
pipeline = CognitiveTurnPipeline(runtime=rt)
|
|
# Prime the field so turn N+1 has a real ``field_state_before``.
|
|
pipeline.run("prime", max_tokens=2)
|
|
|
|
import generate.intent_ratifier as _ir
|
|
original = _ir.ratify_intent
|
|
captured: list[dict] = []
|
|
|
|
def _spy(intent, prompt_versor, *, vocab, threshold=0.0):
|
|
result = original(intent, prompt_versor, vocab=vocab, threshold=threshold)
|
|
captured.append({
|
|
"seed_tag": intent.tag.value,
|
|
"outcome": result.outcome.value,
|
|
"score": result.score,
|
|
})
|
|
return result
|
|
|
|
# core/cognition/pipeline.py imports ``ratify_intent`` at module load;
|
|
# patch both the source module and the imported reference.
|
|
import core.cognition.pipeline as _pl
|
|
_ir.ratify_intent = _spy
|
|
_pl.ratify_intent = _spy
|
|
try:
|
|
pipeline.run(prompt, max_tokens=8)
|
|
finally:
|
|
_ir.ratify_intent = original
|
|
_pl.ratify_intent = original
|
|
|
|
if not captured:
|
|
return {"prompt": prompt, "outcome": "no_ratification_fired"}
|
|
rec = captured[-1]
|
|
rec["prompt"] = prompt
|
|
return rec
|
|
|
|
|
|
def _summarize(rows: list[dict]) -> None:
|
|
by_outcome: dict[str, list[float]] = {}
|
|
skipped: dict[str, int] = {}
|
|
for r in rows:
|
|
if "score" not in r:
|
|
skipped[r.get("outcome", "unknown")] = skipped.get(r.get("outcome", "unknown"), 0) + 1
|
|
continue
|
|
by_outcome.setdefault(r["outcome"], []).append(r["score"])
|
|
if skipped:
|
|
for reason, n in sorted(skipped.items()):
|
|
print(f" skipped[{reason}] = {n}")
|
|
for outcome, scores in sorted(by_outcome.items()):
|
|
if not scores:
|
|
continue
|
|
scores_sorted = sorted(scores)
|
|
pct = lambda p: scores_sorted[max(0, int(len(scores_sorted) * p) - 1)] # noqa: E731
|
|
print(
|
|
f" {outcome:>12} n={len(scores):>3} "
|
|
f"min={min(scores):+.4f} p10={pct(0.10):+.4f} "
|
|
f"p25={pct(0.25):+.4f} median={statistics.median(scores):+.4f} "
|
|
f"p75={pct(0.75):+.4f} max={max(scores):+.4f}"
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
all_rows: list[dict] = []
|
|
for split, path in SPLITS.items():
|
|
cases = _load_cases(path)
|
|
if not cases:
|
|
print(f"[{split}] no cases at {path}")
|
|
continue
|
|
print(f"\n[{split}] {len(cases)} cases")
|
|
rows = [_capture_score(c["prompt"]) for c in cases]
|
|
all_rows.extend(rows)
|
|
_summarize(rows)
|
|
|
|
print("\n[all splits combined]")
|
|
_summarize(all_rows)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|