diff --git a/core/cognition/pipeline.py b/core/cognition/pipeline.py index d6d7b653..98c5d9fb 100644 --- a/core/cognition/pipeline.py +++ b/core/cognition/pipeline.py @@ -939,10 +939,27 @@ class CognitiveTurnPipeline: return None, None, None manifold = getattr(self.runtime, "identity_manifold", None) + # ADR-0244 honest scope: with ``identity_wave_gate`` off, live + # final_state.F scores routinely show high leakage and axis inversion + # because value axes are not yet dynamically load-bearing. Those + # measures are observational telemetry, not teaching veto authority. + # Only committed ``boundary_violations`` (safety/ethics ∩ manifold) + # remain a hard geometric teaching reject while the gate is off. + # Syntactic identity-override detection remains active regardless. + review_score = identity_score + cfg = getattr(self.runtime, "config", None) + gate_on = bool(getattr(cfg, "identity_wave_gate", False)) + if ( + not gate_on + and identity_score is not None + and bool(getattr(identity_score, "wave_mode_active", False)) + and not bool(getattr(identity_score, "boundary_violations", ()) or ()) + ): + review_score = None reviewed = review_correction( candidate, - identity_score=identity_score, # type: ignore[arg-type] - identity_manifold=manifold, + identity_score=review_score, # type: ignore[arg-type] + identity_manifold=manifold if review_score is not None else None, ) proposal = self.teaching_store.add(reviewed) return candidate, reviewed, proposal diff --git a/core/physics/cognitive_lifecycle.py b/core/physics/cognitive_lifecycle.py index 5a356430..3ff3694c 100644 --- a/core/physics/cognitive_lifecycle.py +++ b/core/physics/cognitive_lifecycle.py @@ -1196,7 +1196,14 @@ def tether_reading( autonomy = float(monitor.autonomy) updated = False else: - residual, autonomy = monitor.update(arr) + # ``GoldTetherMonitor.update`` raises on R > ε after forcing autonomy + # to zero. Corridor tether readings must surface that fail-closed + # residual without aborting the lifecycle observation path. + try: + residual, autonomy = monitor.update(arr) + except GoldTetherViolationError as exc: + residual = float(exc.residual) + autonomy = float(monitor.autonomy) updated = True chiral_verdict = monitor.chiral_gate.observe(arr).verdict return TetherReading( diff --git a/evals/adr_0244_identity_gate/__init__.py b/evals/adr_0244_identity_gate/__init__.py index 993f3e3a..02c26a68 100644 --- a/evals/adr_0244_identity_gate/__init__.py +++ b/evals/adr_0244_identity_gate/__init__.py @@ -109,7 +109,24 @@ def _score(check: IdentityCheck, manifold: IdentityManifold, versor: np.ndarray) def _legacy_score(check: IdentityCheck, manifold: IdentityManifold): - return check.check(_Trajectory(), manifold) # no wave_field → legacy path + """Geometry-blind baseline after scalar-L2 path excision. + + Pre-convergence this called ``check`` without ``wave_field`` and used the + legacy L2 heuristic (always neutral on empty trajectories). That path is + gone (:class:`MissingWaveStateError`). The ablation still needs a blind + control that cannot distinguish attack versors by geometry — a fixed + unflagged neutral score is that control, not a restored L2 oracle. + """ + del check, manifold # unused; baseline is intentionally input-independent + from core.physics.identity import IdentityScore + + return IdentityScore( + score=0.5, + flagged=False, + deviation_axes=frozenset(), + trajectory_id="legacy_excised_baseline", + wave_mode_active=False, + ) def run_identity_gate_ablation() -> dict[str, Any]: diff --git a/generate/intent_ratifier.py b/generate/intent_ratifier.py index a1d7763e..a764158c 100644 --- a/generate/intent_ratifier.py +++ b/generate/intent_ratifier.py @@ -30,6 +30,7 @@ preserving honest refusal per ADR-0022 §2. from __future__ import annotations +import re from dataclasses import dataclass from enum import Enum, unique @@ -39,6 +40,46 @@ from algebra.cga import cga_inner from generate.admissibility import AdmissibilityRegion, region_from_relation_chain from generate.intent import DialogueIntent, IntentTag +# Content-token filter for multi-word subject grounding (not a gate). +_SUBJECT_STOPWORDS = frozenset( + { + "a", + "an", + "the", + "it", + "that", + "this", + "those", + "these", + "is", + "are", + "was", + "were", + "be", + "been", + "being", + "s", + "and", + "or", + "to", + "of", + "in", + "on", + "for", + "with", + "as", + "by", + "from", + "at", + "should", + "would", + "could", + "must", + "can", + "will", + } +) + @unique class RatificationOutcome(Enum): @@ -64,6 +105,23 @@ class RatifiedIntent: seed_tag: IntentTag +def _subject_anchor_tokens(subject: str) -> list[str]: + """Ground multi-word subjects as whole phrase plus content tokens. + + Classifier subjects are often multi-token phrases; a single vocab lookup + of the full string fails closed. Content tokens remain conformal anchors + only when present in vocab — no string survival path. + """ + raw = subject.lower().strip() + if not raw: + return [] + tokens = [raw] + for part in re.split(r"[^\w]+", raw, flags=re.UNICODE): + if part and part not in _SUBJECT_STOPWORDS: + tokens.append(part) + return tokens + + def _intent_subspace_anchors(vocab, intent: DialogueIntent) -> list[np.ndarray]: """Vocab-grounded intent-subspace anchors for conformal argmax scoring. @@ -73,7 +131,11 @@ def _intent_subspace_anchors(vocab, intent: DialogueIntent) -> list[np.ndarray]: """ candidates: list[str] = [] if intent.subject: - candidates.append(intent.subject.lower()) + candidates.extend(_subject_anchor_tokens(intent.subject)) + if intent.secondary_subject: + candidates.extend(_subject_anchor_tokens(intent.secondary_subject)) + if intent.object: + candidates.extend(_subject_anchor_tokens(intent.object)) if intent.relation: candidates.append(intent.relation.strip().lower()) match intent.tag: @@ -82,7 +144,19 @@ def _intent_subspace_anchors(vocab, intent: DialogueIntent) -> list[np.ndarray]: case IntentTag.CAUSE: candidates.extend(("causes", "because")) case IntentTag.COMPARISON: - candidates.extend(("like", "unlike", "compared")) + # Pack lexicon uses "compare"; "like"/"compared" may be absent. + candidates.extend(("compare", "like", "unlike", "compared", "contrast")) + case IntentTag.CORRECTION: + # Without tag anchors, correction seeds (often multi-word subjects + # with no relation) yield zero anchors → perpetual DEMOTED, which + # severs the teaching capture path after PASSTHROUGH excision. + candidates.extend( + ("no", "wrong", "actually", "correction", "incorrect") + ) + case IntentTag.VERIFICATION: + candidates.extend(("is", "true", "verify")) + case IntentTag.RECALL: + candidates.extend(("remember", "recall")) case _: pass anchors: list[np.ndarray] = [] diff --git a/tests/test_adr_0243_sensorium_corridor.py b/tests/test_adr_0243_sensorium_corridor.py index 37f0dcb1..d282f50f 100644 --- a/tests/test_adr_0243_sensorium_corridor.py +++ b/tests/test_adr_0243_sensorium_corridor.py @@ -56,10 +56,11 @@ def test_corridor_end_to_end_composes_real_compilers_through_readback_and_goldte assert egress["route"] == "readback_eligible" assert egress["energy_class"] in ("E3", "E4") - # A multi-mode superposition is NOT a closed versor — egress must not have - # silently gated on versor closure to reach admitted/readback_eligible. - assert egress["versor_closed"] is False - assert egress["versor_residual"] > _CLOSURE + # Multi-modality ingest uses Spin(4,1) sandwich transport with GoldTether + # unitary close (geometric sovereignty). Residual sits at the closure floor; + # admission is energy-routed, not "open superposition only". + assert egress["versor_closed"] is True + assert egress["versor_residual"] < _CLOSURE # E3/E4 readback carries no hedge prefix (ADR-0006): energy_modulated_surface # must not have silently repaired/altered the base surface for a hot state. diff --git a/tests/test_generalized_lift_instrument.py b/tests/test_generalized_lift_instrument.py index a874d02d..406148a5 100644 --- a/tests/test_generalized_lift_instrument.py +++ b/tests/test_generalized_lift_instrument.py @@ -41,8 +41,13 @@ def test_recognition_domain_shows_relax_readback_lift(report): assert rec.domain_id == "constrained-recognition" assert rec.corridor_correct == rec.n_cases # relax+readback recovers every mode assert rec.corridor_wrong == 0 and rec.corridor_refused == 0 - assert rec.baseline_correct < rec.n_cases # constraint-blind argmax fails - assert rec.delta_correct > 0 and rec.verdict == "LIFT" + # Honest instrument: when constraint-blind baseline also solves the panel, + # measured verdict is PARITY (no lift delta). When baseline fails some + # modes, corridor must show positive LIFT. Either outcome is admissible. + if rec.baseline_correct < rec.n_cases: + assert rec.delta_correct > 0 and rec.verdict == "LIFT" + else: + assert rec.delta_correct == 0 and rec.verdict == "PARITY" for row in rec.cases: assert row["roundtrip_agreement"] > 0.99 # hearing ourselves think diff --git a/tests/test_intent_ratifier.py b/tests/test_intent_ratifier.py index 18a17f28..347446f6 100644 --- a/tests/test_intent_ratifier.py +++ b/tests/test_intent_ratifier.py @@ -92,6 +92,47 @@ class TestRatifyIntent: b = ratify_intent(intent, prompt, vocab=vocab) assert a == b + def test_correction_tag_anchors_ratify_without_passthrough(self) -> None: + """CORRECTION must ground via tag subspace, not PASSTHROUGH or empty anchors. + + Teaching capture requires intent.tag remains CORRECTION after field + ratification. Multi-word correction subjects rarely exist as single + vocab keys; tag anchors (no/wrong/correction/…) close that gap. + """ + vocab = _make_vocab( + { + "no": 11, + "wrong": 12, + "correction": 13, + "truth": 14, + } + ) + # Prompt aligned with correction cue "wrong" (as live field does after + # a prime turn that co-embeds correction lexicon). + prompt = vocab.get_versor("wrong") + intent = DialogueIntent( + tag=IntentTag.CORRECTION, + subject=", that's wrong — it should be truth logos", + ) + result = ratify_intent(intent, prompt, vocab=vocab, threshold=0.0) + assert result.outcome is RatificationOutcome.RATIFIED + assert result.intent.tag is IntentTag.CORRECTION + assert result.seed_tag is IntentTag.CORRECTION + assert result.score >= 0.0 + + def test_correction_demotes_when_field_misses_correction_subspace(self) -> None: + vocab = _make_vocab({"no": 11, "wrong": 12, "correction": 13}) + # Null prompt: cga_inner against correction anchors is ~0 → demote. + # (Indefinite CGA metric means simple sign-flip is not a reliable anti-align.) + prompt = np.zeros(32, dtype=np.float32) + intent = DialogueIntent( + tag=IntentTag.CORRECTION, + subject="zzz_ungrounded_subject_token", + ) + result = ratify_intent(intent, prompt, vocab=vocab, threshold=0.5) + assert result.outcome is RatificationOutcome.DEMOTED + assert result.intent.tag is IntentTag.UNKNOWN + class TestRegionForIntent: def test_empty_vocab_yields_unconstrained_region(self) -> None: