core/docs/adr/ADR-0026-ranked-admissibility-with-margin.md
Shay 54e6bfc0d0
docs: reorganize docs landscape
Implements the 4-phase documentation reorganization master plan.

- Consolidation: Merged brief/, handoff/, planning/, and decisions/ into briefs/, handoffs/, plans/, and adr/ respectively (101 ADRs relocated)
- Root Cleanup: Relocated HANDOFF-gpt55-*.md and key top-level docs (runtime_contracts.md, etc.) to canonical folders. Added superseded alerts.
- Indices & Navigation: Created docs/README.md navigation document, docs/sessions/README.md index, docs/adr/README.md index
- Note: Also includes prior commit adding ADR-0200+ corpus hygiene governance (ADR-0225, dependency map, backfilled cross-references)
2026-06-30 16:59:36 -07:00

10 KiB

ADR-0026 — Ranked Admissibility with Margin

Status: Accepted (2026-05-17) Supersedes (in part): ADR-0024's static admissibility_threshold mechanism for production admissibility gating. ADR-0024 remains Accepted; threshold mode is preserved for backward-compatible acceptance evidence and is the default unless margin mode is explicitly enabled.

Context

Phase 4 threshold characterization (tests/test_inner_loop_phase4.py) established a load-bearing geometric finding: no single global static threshold delivers separation_quality >= 0.8 across the v2 mechanism- isolation cases. Blade norms vary roughly tenfold across cases (best_separation_quality < 0.5 is now an invariant test), so the same tau value means semantically different things case-to-case.

Three options were on the table for Phase 3:

  1. Per-case normalised thresholds (e.g. alpha * blade_self_score). Adds a tuning surface; the constant becomes a knob.
  2. Per-pack thresholds. Migrates the tuning problem from the blade level to the pack level; same failure mode.
  3. Ranked-with-margin admissibility. Replaces the absolute- score gate with a relative-ordering gate: rank the admissible candidates by cga_inner(versor, relation_blade) descending and admit the top iff its margin over the second-ranked exceeds a single per-runtime delta.

Option 3 is the only option that is scale-invariant under blade- norm variation — the gap between the top and second-ranked scores scales with the blade norm, so the relative ordering carries the semantic separation the static threshold was reaching for. This is the architectural difference between "what direction is admissible" (blade alignment, absolute) and "which candidate is confidently selected over the next-best" (separation, relative).

The previous assessment (recorded in the planning conversation that preceded this ADR) put it this way:

A global threshold — even normalized — assumes admissibility is an absolute property of a candidate. It isn't: in your geometry, what matters is the ordering of admissibility scores across the candidate set, plus enough separation to be confident the boundary's pick can be rejected and replaced deterministically.

Decision

Add a new admissibility mode margin alongside ADR-0024's existing threshold mode. When margin mode is active, the inner loop's selection-and-admission collapses into a single deterministic step:

ranked = rank_candidates_by_blade(region, candidate_indices=, )
verdict = check_margin(region, ranked, delta=admissibility_margin)

rank_candidates_by_blade sorts the post-region-filter candidate set by cga_inner(versor, relation_blade) descending, with a strict > tie-break: when scores are equal, ascending vocab index wins. This matches the vocab.nearest strict-> convention documented as load-bearing in ADR-0024.

check_margin admits the top-ranked candidate iff:

  1. len(ranked) >= 1 (non-empty admissible set), AND
  2. ranked[0].score > 0 (basic positivity in the blade half-space), AND
  3. len(ranked) == 1 (trivial, no competitor) OR ranked[0].score - ranked[1].score >= delta.

When refused, the inner loop raises InnerLoopExhaustion (ADR-0024 Phase 2) carrying the full ranking as rejected_attempts — not a single rejected score, but the entire blade-ordering at the failed step.

Selection semantics in margin mode

Margin mode is blade-rank-driven selection: the top-ranked admissible candidate IS the admitted destination. This differs from threshold mode, where _nearest_next (field-driven) picks one candidate and check_transition gates it; on rejection, the selector advances to the next field-closest candidate and re-gates.

This is a meaningful semantic difference, not a re-shading. Margin mode says "of the candidates the region admits as semantically valid, the most blade-aligned one is the destination, provided its lead is confident." Threshold mode says "of the candidates the field's geometry prefers, accept the first one above an absolute score bar."

Both have a place: threshold mode is the ADR-0024 acceptance evidence and remains the default to preserve every existing turn's trace_hash byte-for-byte. Margin mode is the new production admissibility for cases where blade-norm variation makes the static bar incoherent.

The single delta choice

Phase 3 picks delta = 0.4 as the default. This was derived from the v2 mechanism-isolation case margins:

Case Top score Second score Margin
FSC-PUB-V2-001 1.420 0.824 0.596
FSC-PUB-V2-002 1.173 0.717 0.456
FSC-PUB-V2-003 12.720 -0.550 13.270
FSC-PUB-V2-004 5.740 2.370 3.370
FSC-PUB-V2-005 14.360 1.620 12.740

The minimum margin across the five cases is 0.456. delta = 0.4 admits all five with headroom; delta = 0.5 would refuse V2-002. The default is falsifiable: Phase 5's diversified failure-mode families may surface a case below 0.4, in which case the finding is architectural — margin alone is insufficient for that family — and should be reported honestly rather than patched with a per-case override.

Wiring

Surface Field
core/config.py::RuntimeConfig admissibility_mode: str = "threshold"
admissibility_margin: float = 0.4
chat/runtime.py::ChatRuntime.chat forwards both fields
generate/stream.py::generate admissibility_mode / admissibility_margin kwargs
generate/admissibility.py RankedCandidate, MarginVerdict, rank_candidates_by_blade, check_margin

Selection ordering inside generate.generate():

if margin_mode_active:
    rank → check_margin → admit-or-refuse
else:
    _nearest_next → check_transition (per-attempt loop)

The rotor V is only constructed for the admitted candidate, so the versor_condition(F) < 1e-6 invariant is preserved by construction (CLAUDE.md §Non-Negotiable Field Invariant).

Why flag-gated (default off)

Margin mode is a real semantic change in selection. Defaulting it off preserves:

  • Every existing trace_hash byte-for-byte (no payload bytes change when margin mode is unset).
  • ADR-0024's acceptance evidence intact.
  • The ability to A/B threshold vs margin on the same corpus during the transition window.

A future ADR may flip the default; this ADR does not.

Invariants preserved

  • versor_condition(F) < 1e-6 — the rotor is constructed only for the admitted candidate; margin mode does not add a normalization or repair site (CLAUDE.md §Normalization Rules).
  • Deterministic replay — strict > tie-break is now load-bearing in two places: vocab.nearest (ADR-0024) and rank_candidates_by_blade (this ADR). tests/test_margin_admissibility.py::TestRankCandidates::test_strict_tie_break_by_ascending_index pins it.
  • No approximate recall, no cosine similarity, no HNSW/ANN. Margin is a pure rank-and-difference operation on the exact cga_inner scores already in use.
  • No new code in field/propagate.py, algebra/versor.py, vault/store.py, or chat/runtime.respond().

Trust boundary

Same as ADR-0024 — admissibility regions are constructed upstream from pack-grounded data; margin mode adds no new surface that consumes user-controlled text, filesystem paths, or dynamic imports.

Acceptance evidence

  • 5/5 v2 mechanism-isolation cases pass in margin mode with delta = 0.4. Forbidden token traced in every case's rejected_attempts. Mirrors ADR-0024 §Acceptance evidence for threshold mode.
  • Threshold mode unchanged. tests/test_margin_admissibility.py::TestGenerateMarginMode::test_threshold_mode_unchanged_by_margin_plumbing asserts mode="threshold" and unset mode produce identical tokens.
  • Refusal on insufficient margin. test_v2_001_refuses_when_delta_too_high runs FSC-PUB-V2-001 at delta = 0.9 (above its 0.597 margin) and asserts InnerLoopExhaustion with reason=INNER_LOOP_EXHAUSTION and the full ranking in rejected_attempts. No silent boundary fallback.
  • Replay determinism. TestMarginModeDeterminism::test_margin_mode_replay_stable_across_5_runs asserts 5 reruns of the same input produce identical canonical trace steps.
  • Strict tie-break determinism. TestRankCandidates::test_strict_tie_break_by_ascending_index asserts equal-score candidates resolve to the lower-index winner reproducibly across 5 runs.

Out of scope

  • Promoting margin to default. Requires a separate ADR plus trace-hash migration evidence.
  • Rotor-frame margin. ADR-0025 will wire rotor admissibility; if its mechanism also benefits from a margin gate, that's an ADR-0025 decision.
  • Per-family delta calibration. Phase 5 will report metrics stratified by failure-mode family. If a family fails on the single delta, the finding is architectural and surfaces in Phase 5; this ADR explicitly forbids per-family tuned constants as a fix.
  • CLI flags. RuntimeConfig + ChatRuntime is the wired surface. A --admissibility-mode flag is a UX follow-up, not load-bearing for the contract.

Risks

  • A single delta may fail on a future failure-mode family. Mitigation: the default is falsifiable (Phase 5 will diversify); refusal is honest (InnerLoopExhaustion carries the ranking, so the failure mode is visible in the trace). Patching with per-family constants is explicitly out of scope.
  • Margin mode changes selection semantics. Threshold-mode acceptance evidence does not transfer. Mitigation: default off; separate test suite (tests/test_margin_admissibility.py) pins margin contract independently.
  • Cost. Margin mode evaluates cga_inner for every candidate in the admissible set, not just the field-closest one as threshold mode does. In practice the admissible set is small (chain length / v2 case size) so this is bounded. No approximation added.

Rollback

Set admissibility_mode = "threshold" (the default) at every call site. The threshold path is unchanged from ADR-0024. No trace_hash migration required for non-refused turns.