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)
292 lines
13 KiB
Markdown
292 lines
13 KiB
Markdown
# ADR-0024 — Inner-Loop Per-Rotor Admissibility
|
|
|
|
| Field | Value |
|
|
|--------------|----------------|
|
|
| Status | **Accepted** |
|
|
| Date | 2026-05-17 |
|
|
| Supersedes | — |
|
|
| Extends | ADR-0022, ADR-0023 |
|
|
| Decision lead| Shay (with CORE assistant) |
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
ADR-0022 introduced `AdmissibilityRegion` and routed it through
|
|
`generate()` as a **boundary prefilter**: the region's
|
|
`allowed_indices` array is intersected with the language / salience
|
|
candidate set before `_nearest_next` runs, and an empty intersection
|
|
raises `ValueError` (honest refusal). ADR-0023 then added
|
|
per-transition trace evidence: each step records `candidates_before /
|
|
candidates_after / selected_index / verdict`, hashed into the
|
|
deterministic trace.
|
|
|
|
That is enough to make the *token-set* side of admissibility
|
|
load-bearing. It does not yet make the *blade-direction* side
|
|
load-bearing: `check_transition` is evaluated after selection and
|
|
recorded into the verdict, but the verdict does not influence which
|
|
candidate is chosen. Today, a candidate that survives the index
|
|
prefilter is *always* selected even if its versor's CGA inner product
|
|
against `relation_blade` is negative — the trace says "rejected" and
|
|
the walk emits it anyway.
|
|
|
|
ADR-0023 §Decision explicitly deferred this:
|
|
|
|
> ADR-0024 will separately scope inner-loop admissibility (per-rotor
|
|
> admissibility checks after candidate prefilter) because that *is* a
|
|
> semantic change and interacts with the `versor_condition`
|
|
> invariant.
|
|
|
|
This ADR scopes that change.
|
|
|
|
## Decision
|
|
|
|
We add **inner-loop per-rotor admissibility** to `generate()`,
|
|
flag-gated and off by default.
|
|
|
|
When `inner_loop_admissibility=True` and a non-unconstrained region
|
|
is supplied:
|
|
|
|
1. `_nearest_next` selects a destination from the index-filtered
|
|
candidate set, exactly as today.
|
|
2. `check_transition(region, candidate_index, candidate_versor,
|
|
threshold=admissibility_threshold)` evaluates the candidate.
|
|
3. If `verdict.admitted` is `False`, the candidate is appended to a
|
|
step-local `rejected_attempts` list, its index is added to a
|
|
per-step exclude set, and `_nearest_next` re-runs with that
|
|
exclusion. The retry budget is bounded by
|
|
`len(candidate_indices)`.
|
|
4. If every admissible candidate is rejected (selector returns an
|
|
already-excluded index, or the retry budget is exhausted), the
|
|
walk raises `ValueError(f"AdmissibilityRegion[{label}] inner-loop
|
|
rejected all candidates at step {step_index}.")` — the same
|
|
honest-refusal shape ADR-0022 §2 already commits to for empty
|
|
admissible sets.
|
|
5. The selected (admitted) candidate proceeds through the existing
|
|
rotor application: `V = word_transition_rotor(A, B)` and
|
|
`propagate_step(current, V)`. No new normalization site is
|
|
introduced; the runtime versor invariant
|
|
`versor_condition(F) < 1e-6` is still asserted after propagation
|
|
exactly as before.
|
|
|
|
The rejected candidates are recorded in the trace via a new
|
|
`AdmissibilityTraceStep.rejected_attempts: tuple[tuple[int, str,
|
|
float], ...]` field. The canonical form folds this field into the
|
|
trace hash only when non-empty, so any ADR-0023 turn (boundary-only
|
|
walk, or inner-loop on but no rejections) hashes to the same bytes it
|
|
hashed to before this ADR shipped.
|
|
|
|
Default is `False`. Legacy call sites (`chat/runtime.py`,
|
|
`generate/proposition.py`, the ADR-0023 ablation lane) keep their
|
|
boundary-only semantics until they opt in.
|
|
|
|
## Why flag-gated
|
|
|
|
This is a real semantics change to the walk: it can divert selection
|
|
from the geometric nearest to a non-nearest admitted candidate. That
|
|
is exactly what ADR-0022 promised the admissibility region would do
|
|
for the *direction* side of admissibility, but it changes the
|
|
distribution of emitted tokens for every constrained turn that hits
|
|
a rejection. Flag-gating means:
|
|
|
|
* every commit before this ADR's eval lanes light up
|
|
`inner_loop_admissibility=True` continues to produce byte-identical
|
|
trace hashes;
|
|
* the change can be ramped per-call-site rather than as a global
|
|
semantics flip;
|
|
* failures attributable to the inner loop are isolated by toggling
|
|
the flag, not by reverting code.
|
|
|
|
## Invariants preserved
|
|
|
|
* **Versor condition.** The rotor `V` is constructed from
|
|
`vocab.get_versor_at(word_idx)` for the *admitted* candidate; the
|
|
rejected candidates never reach `word_transition_rotor`. The
|
|
`versor_condition < 1e-6` assertion at `propagate_step` is
|
|
unaffected. CLAUDE.md §Non-Negotiable Field Invariant: not
|
|
weakened.
|
|
* **No new normalization site.** The inner loop is a selection-side
|
|
retry; it never rebalances `F`, projects grades, or unitizes
|
|
rotors. CLAUDE.md §Normalization Rules: respected (no addition to
|
|
the forbidden list).
|
|
* **Exact CGA recall.** Vault recall and `cga_inner` are unchanged.
|
|
CLAUDE.md §Core Primitives: not touched.
|
|
* **Honest refusal.** Exhaustion raises `ValueError` with the region
|
|
label in the message, the same surface ADR-0022 already commits to
|
|
for empty admissible sets.
|
|
|
|
## Trust boundary
|
|
|
|
No new I/O. No new dynamic imports. No new filesystem reads. The
|
|
`rejected_attempts` tuple is built from already-grounded vocabulary
|
|
indices and scores produced by `cga_inner`, both of which the existing
|
|
trust-boundary review (ADR-0022 §Trust Boundary) covers. No
|
|
user-controlled text enters the trace path.
|
|
|
|
## Acceptance evidence
|
|
|
|
* **Backward-compat trace hash.** `compute_trace_hash` over an
|
|
ADR-0023 turn (no rejected attempts) produces the same bytes
|
|
before and after this ADR. Covered by
|
|
`tests/test_admissibility_trace.py::TestComputeTraceHashBackwardCompat`
|
|
and by a new `tests/test_inner_loop_admissibility.py` case asserting
|
|
that an empty `rejected_attempts` list canonicalizes without the
|
|
key.
|
|
* **Re-selection on rejection.** A new test constructs a small
|
|
vocabulary where the geometric-nearest candidate would be rejected
|
|
by a region whose blade points away from it; with
|
|
`inner_loop_admissibility=True` and a positive threshold, the walk
|
|
emits a *different* admitted token and the step's
|
|
`rejected_attempts` records the rejected one.
|
|
* **Exhaustion → honest refusal.** A region whose blade rejects
|
|
every admissible candidate raises `ValueError` with the region label
|
|
embedded in the message.
|
|
* **Default off preserves behavior.** With the flag off, every
|
|
existing pipeline / runtime / eval lane test continues to pass byte-
|
|
for-byte; no test in `tests/` had to be updated to absorb this
|
|
change.
|
|
|
|
## Out of scope
|
|
|
|
* **Pipeline / runtime wiring.** This ADR only adds the parameter on
|
|
`generate()`. Wiring the flag through `RuntimeConfig`,
|
|
`CognitiveTurnPipeline`, and `chat/runtime.py` is left to a follow-
|
|
up so the eval lane can demonstrate causal isolation against the
|
|
inner loop without touching production defaults.
|
|
* **Frame-versor admissibility.** ADR-0022's `frame_versor` /
|
|
`rotor_constraint` side of the region remains observed but unused
|
|
for selection. That belongs in a future ADR after this one's trace
|
|
evidence shows whether blade-direction admissibility alone closes
|
|
the remaining causality gap.
|
|
* **Adaptive thresholds.** The threshold is a static parameter.
|
|
Adaptive thresholds (learned, frame-derived, or annealed) are a
|
|
separate semantic change.
|
|
|
|
## Risks
|
|
|
|
* **Selection drift.** A non-zero `admissibility_threshold` will
|
|
divert tokens. Mitigation: default `0.0` matches the ADR-0023
|
|
verdict computation; lanes ramp the threshold independently per
|
|
case.
|
|
* **Cost.** Up to `len(candidate_indices)` extra `check_transition`
|
|
calls per step in the worst case. In practice the admissible set
|
|
is small (chain length) and rejections terminate after the first
|
|
admitted candidate.
|
|
* **Test brittleness.** Tests that asserted exact tokens on
|
|
constrained walks could shift if they enable the flag. Mitigation:
|
|
flag stays off everywhere by default; opt-in is explicit per call
|
|
site.
|
|
|
|
## Rollback
|
|
|
|
Set `inner_loop_admissibility=False` (the default) at every call
|
|
site. The trace hash remains byte-identical to ADR-0023, so
|
|
deterministic replay over the existing corpus is unaffected.
|
|
|
|
---
|
|
|
|
## Addendum — Phase 1 v1/dev fixture retirement (2026-05-17)
|
|
|
|
### Finding
|
|
|
|
Phase 4's characterization recorded that v1 chain-token outer-product
|
|
blades skipped 9/9 cases because `chain_tokens` (`alpha`, `beta`,
|
|
`gamma`, `delta`, `mu`, `nu`, `omicron`, `pi`, `rho`, `sigma`, `tau`,
|
|
`upsilon`, `phi`, `chi`, `psi`, `omega`, `iota`, `lambda`, `eta`,
|
|
`theta`, `zeta`, `xenon`, `ytterbium`, `kappa`) were synthetic and
|
|
ungrounded in `en_core_cognition_v1`. The Phase 2 corpus-observation
|
|
runner inherited the same fixtures, surfacing `exhaustion_rate = 0.33`
|
|
at `t = 0` on v1+dev — above the 5 % benign-corpus ceiling.
|
|
|
|
Original assessment (pre-Phase-1): "v1 chain construction can produce
|
|
ungrounded regions; fix the cause, not the symptom."
|
|
|
|
### What Phase 1 did
|
|
|
|
The 1 v1 case and 8 dev cases were rewritten with pack-grounded tokens
|
|
drawn from `en_core_cognition_v1`:
|
|
|
|
* `tone → evidence → memory → wisdom` (causes chain, replacing
|
|
`alpha → beta → gamma → delta`)
|
|
* `voice → memory → wisdom` (means chain, replacing
|
|
`mu → nu → omicron`)
|
|
* `question → answer → understanding → wisdom` (precedes chain,
|
|
replacing `pi → rho → sigma → tau`)
|
|
* `word → discourse → narrative` (part_of chain, replacing
|
|
`upsilon → phi → chi`)
|
|
* `symbol → word → meaning` with `image`/`light` distractors
|
|
(adversarial branching, replacing `eta/theta/zeta` plus
|
|
`beta/rho` means-distractors)
|
|
|
|
All token-level grammar adjustments (e.g. *"register cause memory"* →
|
|
*"register causes memory"*) were corrected as part of the rewrite.
|
|
|
|
### What changed about the finding
|
|
|
|
| Metric | Pre-rewrite | Post-rewrite |
|
|
| --------------------------------------- | ----------- | ------------ |
|
|
| v1+dev cases skipped (region builds) | 9 / 9 | 0 / 9 |
|
|
| `causal_attribution_valid` | True | True |
|
|
| `code_path_residual` | 0.0 | 0.0 |
|
|
| `inner_loop_t0` hash stability | 1.0 | 1.0 |
|
|
| `best_separation_quality` (Phase 4) | 0.0 | 0.056 |
|
|
| `geometry_supports_static_threshold` | False | False |
|
|
| `inner_loop_t0` exhaustion (Phase 2) | 0.33 | 0.67 |
|
|
| `inner_loop_tpos` exhaustion (Phase 2) | (skipped) | 1.00 |
|
|
|
|
The original Phase 4 finding ("v1 chain-token blades unsuitable as the
|
|
default region construction") was caused by two layered issues:
|
|
|
|
1. **Surface fixture rot** — synthetic tokens prevented region
|
|
construction at all. This is now fixed.
|
|
2. **Deeper architectural mismatch** — v1's case schema
|
|
(`prime` + `chain_tokens`) probes *teaching-driven walk* through a
|
|
relation injected at runtime (ADR-0022 / ADR-0023's mechanism).
|
|
The inner-loop admissibility lane (ADR-0024) tests
|
|
*blade-geometric region constraint*. These are distinct
|
|
mechanisms; the same case schema cannot meaningfully exercise both.
|
|
|
|
Exhaustion *increased* post-rewrite because the chain-blade region is
|
|
now constructible *and* actively rejecting the boundary's off-chain
|
|
picks. That is honest behavior of the inner-loop on cases it was not
|
|
designed to evaluate.
|
|
|
|
### Architectural conclusion
|
|
|
|
* **v1 / dev cases belong to the ADR-0022 / ADR-0023 boundary-walk
|
|
lane (`evals/forward_semantic_control/runner.py`).** The rewrite
|
|
improves that lane (real semantic substrate for teaching priming) and
|
|
does not affect this ADR.
|
|
* **v2 cases belong to the ADR-0024 inner-loop admissibility lane
|
|
(`evals/forward_semantic_control/v2_runner.py`).** v2's schema
|
|
(`seed_token` + `admissible_tokens` + `relation_blade_token`) is the
|
|
correct fixture shape for blade-admissibility evaluation.
|
|
* **The Phase 2 corpus-observation runner's reuse of v1+dev cases was
|
|
a categorical error.** The runner remains useful as a corpus-style
|
|
observation harness; it now needs a proper benign inner-loop corpus,
|
|
authored with v2's schema. Authoring that corpus is **Phase 5's
|
|
job** (stratified mechanism-isolation families subsume benign-corpus
|
|
observation).
|
|
|
|
### What this ADR addendum does *not* claim
|
|
|
|
* The exhaustion finding is not "closed" — it is reattributed. v1/dev
|
|
inner-loop exhaustion at 67 % is an honest consequence of running the
|
|
wrong mechanism on the wrong fixture, not a defect in the inner-loop
|
|
implementation.
|
|
* The Phase 2 runner's exhaustion gate (`EXHAUSTION_CEILING = 0.05`)
|
|
remains valid as a contract; it cannot be satisfied on the current
|
|
fixture mix. Phase 5 will produce the benign-corpus fixtures the
|
|
gate was designed against.
|
|
* Phase 4's "no static threshold passes" finding stands unchanged.
|
|
|
|
### Tests pinning the new state
|
|
|
|
* `tests/test_inner_loop_phase4.py::TestV1ChainBladePostGrounding` —
|
|
asserts `skipped_count == 0` (replacing the pre-rewrite assertion
|
|
`skipped_count == case_count`) and
|
|
`best_separation_quality < 0.5` (replacing `== 0.0`).
|
|
* `tests/test_inner_loop_phase2.py` (unchanged) — continues to assert
|
|
`causal_attribution_valid` and inner-loop hash stability. No
|
|
exhaustion gate assertion; exhaustion remains a recorded finding,
|
|
not an invariant.
|