chore(ratifier): calibrate default ratification threshold 0.0 → 0.5 (#86)
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``.
This commit is contained in:
parent
4f9e00a6a5
commit
e41a14f76c
3 changed files with 195 additions and 1 deletions
|
|
@ -95,12 +95,30 @@ def _intent_anchor_versor(vocab, intent: DialogueIntent) -> np.ndarray | None:
|
|||
return None
|
||||
|
||||
|
||||
#: Default ratification threshold (Finding 3, audit 2026-05-20).
|
||||
#:
|
||||
#: Pre-fix the default was ``0.0``, which admitted anything with non-
|
||||
#: negative projection onto the anchor versor — the field gate was
|
||||
#: structurally live but semantically transparent (ADR-0022 §TBD-1).
|
||||
#: Measured against ``core eval cognition`` across all 45 cases (45 =
|
||||
#: 13 public + 13 dev + 19 holdout), every ratifiable case scored
|
||||
#: ``cga_inner(prompt, anchor) ≥ 1.10`` after a prime turn primed the
|
||||
#: field — see ``scripts/calibrate_ratification_threshold.py``. The
|
||||
#: 0.5 floor is well below that 1.10 minimum (no regression on any
|
||||
#: passing case) while clearly non-trivially positive (random Cl(4,1)
|
||||
#: inner products fluctuate around zero, so 0.5 demands genuine
|
||||
#: correlation with the anchor). Off-corpus / adversarial prompts
|
||||
#: with weakly-aligned anchors will now demote to ``UNKNOWN`` and
|
||||
#: route through the honest-refusal surface.
|
||||
_DEFAULT_RATIFICATION_THRESHOLD: float = 0.5
|
||||
|
||||
|
||||
def ratify_intent(
|
||||
intent: DialogueIntent,
|
||||
prompt_versor: np.ndarray,
|
||||
*,
|
||||
vocab,
|
||||
threshold: float = 0.0,
|
||||
threshold: float = _DEFAULT_RATIFICATION_THRESHOLD,
|
||||
) -> RatifiedIntent:
|
||||
"""Ratify a seeded intent against the prompt versor.
|
||||
|
||||
|
|
|
|||
141
scripts/calibrate_ratification_threshold.py
Normal file
141
scripts/calibrate_ratification_threshold.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"""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()
|
||||
35
tests/test_ratification_threshold_default.py
Normal file
35
tests/test_ratification_threshold_default.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""Ratification threshold default — Finding 3 (audit 2026-05-20).
|
||||
|
||||
Pre-fix the default threshold was ``0.0`` and the field gate was
|
||||
semantically transparent (any non-negative projection passed). This
|
||||
test pins the calibrated default so a future change cannot silently
|
||||
revert to a transparent gate without updating the test and the
|
||||
calibration evidence comment in
|
||||
``generate/intent_ratifier.py:_DEFAULT_RATIFICATION_THRESHOLD``.
|
||||
|
||||
The calibration data the default rests on was measured by
|
||||
``scripts/calibrate_ratification_threshold.py``:
|
||||
|
||||
* 34 RATIFIED cases across public+dev+holdout splits
|
||||
* minimum ``cga_inner(prompt, anchor)`` observed: ~1.10
|
||||
* 0.5 sits well below the floor (no regression on any passing case)
|
||||
and clearly non-trivially positive (random Cl(4,1) inner products
|
||||
fluctuate around zero).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from generate.intent_ratifier import _DEFAULT_RATIFICATION_THRESHOLD, ratify_intent
|
||||
|
||||
|
||||
def test_default_threshold_is_calibrated() -> None:
|
||||
assert _DEFAULT_RATIFICATION_THRESHOLD == 0.5
|
||||
|
||||
|
||||
def test_ratify_intent_signature_uses_calibrated_default() -> None:
|
||||
sig = inspect.signature(ratify_intent)
|
||||
threshold_param = sig.parameters["threshold"]
|
||||
assert threshold_param.default == _DEFAULT_RATIFICATION_THRESHOLD
|
||||
assert threshold_param.default != 0.0 # pre-fix value must not resurface
|
||||
Loading…
Reference in a new issue