From 11c91581e8ccbbfe2bfa21b3c96a3ad7738e37fa Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 24 May 2026 20:05:25 -0700 Subject: [PATCH] fix(W-015): replace _slerp_toward with rotor-geodesic anchor pull (#255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes W-015 wiring debt. Per Sonnet's investigation (PR #252, verdict (c)): _slerp_toward interpolates on S^31 but the versor manifold (Spin sub-group in Cl(4,1)) is a proper subset. Slerp's geodesic doesn't stay on the manifold, producing systematic off-manifold state that the post-hoc unitize_versor was repairing. Fix replaces _slerp_toward with the proper rotor-geodesic path: R = word_transition_rotor(field_state.F, anchor_field) R_step = rotor_power(R, _ANCHOR_PULL_ALPHA) pulled_F = versor_apply(R_step, field_state.F) rotor_power stays on the manifold by construction (same principle as generate/stream.py:220). versor_apply closes via algebra/ versor.py — an already-sanctioned site. The unsanctioned unitize_versor call in _anchor_pull and the entire _slerp_toward function are removed. CLAUDE.md normalization-site discipline is now restored: session/context.py:_anchor_pull no longer performs normalization. Changes: - session/context.py: import rotor_power + word_transition_rotor, remove _slerp_toward (34 lines), rewrite _anchor_pull to use rotor-geodesic (15 lines net change). - tests/test_session_coherence.py: new test pins the manifold invariant — after anchor pull, versor_condition stays < 1e-6 without any unitize call (32 lines). Intentional lane re-pins (audit-trail per #229 discipline): - demo_composition: 403be13b → 3a3d09f3 (anchor pull now produces correct on-manifold fields; demo output shifts as expected). - public_demo: acd51d0c → 888ddd0d (same cause). CLAIMS.md regenerated to reflect new pins (per #239 lesson). Verification: - tests/test_session_coherence.py: 3 passed - core test --suite smoke: 67 passed - scripts/verify_lane_shas.py: 7/7 match (post-re-pin) - Manifold invariant test pinned: anchor pull preserves versor_condition < 1e-6 by construction (no repair). Investigation source: PR #252 (Sonnet). 4,138-sample bimodal distribution confirmed _slerp_toward as the sole drift source. --- CLAIMS.md | 4 +- evals/demo_composition/results/v1_dev.json | 2 +- evals/public_demo/results/v1_dev.json | 2 +- scripts/verify_lane_shas.py | 4 +- session/context.py | 48 +++++++--------------- tests/test_session_coherence.py | 34 ++++++++++++++- 6 files changed, 54 insertions(+), 40 deletions(-) diff --git a/CLAIMS.md b/CLAIMS.md index c6beb41d..2a5b9b40 100644 --- a/CLAIMS.md +++ b/CLAIMS.md @@ -38,8 +38,8 @@ is a CI failure (`.github/workflows/lane-shas.yml`). | ADR-0093 | `domain_contract_validation` | All ratified packs satisfy the 9 ADR-0091 contract predicates | `evals/domain_contract_validation/results/v1_dev.json` | `98ace04e3f02bbc5a8ad655bb6593c3f1ee64cb67014f1122fe6c3c85f48d22f` | | ADR-0095 | `miner_loop_closure` | Miner-sourced proposals route through single reviewed teaching path | `evals/miner_loop_closure/results/v1_dev.json` | `9f071733abe7dcacf759f928548ce738fb639af3fd6e4c621a651b306d7e77ce` | | ADR-0096 | `fabrication_control_summary` | Phantom endpoints / cross-pack non-bridges / sibling collapses refuse | `evals/fabrication_control/results/v1_summary.json` | `01e1b6b711141f2b4a14551d7df3ea482d8d6dd7b364a25c509f4f8d08cda8a8` | -| ADR-0098 | `demo_composition` | Demos compose from shipped modules; no parallel mechanism | `evals/demo_composition/results/v1_dev.json` | `403be13bd666389bdf1caa1b97382753e531ad72686c5237f64a5e23bc702ac4` | -| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `acd51d0c17948030aa2843309a3729c6f825959e2e0cbbc477e07de8269268a4` | +| ADR-0098 | `demo_composition` | Demos compose from shipped modules; no parallel mechanism | `evals/demo_composition/results/v1_dev.json` | `3a3d09f3a87462737e615c2dd3481b9e13e5ff8fadee0043c37873494ded556d` | +| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `888ddd0d12635d709f91898cf06601062168694d870f776d2b0b7710e5d68cbd` | | ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d` | ## Verification diff --git a/evals/demo_composition/results/v1_dev.json b/evals/demo_composition/results/v1_dev.json index 4110e6a9..8f8b2da8 100644 --- a/evals/demo_composition/results/v1_dev.json +++ b/evals/demo_composition/results/v1_dev.json @@ -30,7 +30,7 @@ "details": { "all_claims_supported_a": true, "all_claims_supported_b": true, - "sha256": "deb3af8fab8f3ff9cf7468efd7a403b60c825a67b76a5c07ab95147f79bd2e9a" + "sha256": "6a02197c0d9fae0eb041aaed9d8c864291477c066ec915ca403cf774c301f7d3" }, "divergence": null, "passed": true diff --git a/evals/public_demo/results/v1_dev.json b/evals/public_demo/results/v1_dev.json index b31f2506..f284c5d5 100644 --- a/evals/public_demo/results/v1_dev.json +++ b/evals/public_demo/results/v1_dev.json @@ -13,7 +13,7 @@ { "case_id": "determinism_run_to_run_byte_equality", "details": { - "sha256": "48fc9cba028d5d8e8e098d71cd25a140861fcf380c1214612358bce433f79996" + "sha256": "58e4ddec73aba31b0e193d573921cd85df7fd225ba68716eeef1517b635c6141" }, "divergence": null, "passed": true diff --git a/scripts/verify_lane_shas.py b/scripts/verify_lane_shas.py index 86d8f4c6..d1af4fc1 100644 --- a/scripts/verify_lane_shas.py +++ b/scripts/verify_lane_shas.py @@ -38,8 +38,8 @@ PINNED_SHAS: dict[str, str] = { "curriculum_loop_closure": "b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d", "domain_contract_validation": "98ace04e3f02bbc5a8ad655bb6593c3f1ee64cb67014f1122fe6c3c85f48d22f", "fabrication_control_summary": "01e1b6b711141f2b4a14551d7df3ea482d8d6dd7b364a25c509f4f8d08cda8a8", - "demo_composition": "403be13bd666389bdf1caa1b97382753e531ad72686c5237f64a5e23bc702ac4", - "public_demo": "acd51d0c17948030aa2843309a3729c6f825959e2e0cbbc477e07de8269268a4", + "demo_composition": "3a3d09f3a87462737e615c2dd3481b9e13e5ff8fadee0043c37873494ded556d", + "public_demo": "888ddd0d12635d709f91898cf06601062168694d870f776d2b0b7710e5d68cbd", } diff --git a/session/context.py b/session/context.py index 9ab475e4..f1873984 100644 --- a/session/context.py +++ b/session/context.py @@ -11,7 +11,8 @@ from __future__ import annotations import numpy as np from algebra.backend import cga_inner, versor_apply -from algebra.versor import unitize_versor, versor_condition as _versor_condition +from algebra.rotor import rotor_power, word_transition_rotor +from algebra.versor import versor_condition as _versor_condition from field.state import FieldState from generate.dialogue import DialogueTurn from generate.proposition import Proposition @@ -35,35 +36,6 @@ _BLADE_EMA_ALPHA: float = 0.15 _ANCHOR_PULL_ALPHA: float = 0.05 -def _slerp_toward( - F: np.ndarray, - target: np.ndarray, - alpha: float, -) -> np.ndarray: - """Spherical-linear interpolation of F toward target by fraction alpha. - - When the inner product is near ±1 (nearly parallel/antiparallel versors), - falls back to linear interpolation to avoid numerical instability. - """ - f_norm = float(np.linalg.norm(F)) - t_norm = float(np.linalg.norm(target)) - if f_norm < 1e-10 or t_norm < 1e-10: - return F - f_unit = F / f_norm - t_unit = target / t_norm - cos_theta = float(np.clip(np.dot(f_unit.ravel(), t_unit.ravel()), -1.0, 1.0)) - theta = float(np.arccos(abs(cos_theta))) - if theta < 1e-6: - # Nearly parallel — linear blend is numerically identical - result = (1.0 - alpha) * F + alpha * target - else: - sin_theta = float(np.sin(theta)) - w_f = float(np.sin((1.0 - alpha) * theta)) / sin_theta - w_t = float(np.sin(alpha * theta)) / sin_theta - result = w_f * F + w_t * target - return np.asarray(result, dtype=F.dtype) - - class SessionContext: def __init__(self, vocab, persona=None, vault=None, vault_reproject_interval: int = 100): self.vocab = vocab @@ -221,19 +193,29 @@ class SessionContext: ) def _anchor_pull(self, field_state: FieldState) -> FieldState: - """Drift fix 3: mild slerp toward the session anchor field. + """Drift fix 3: mild rotor-geodesic pull toward the session anchor field. Applied after hemisphere correction. Provides continuous conjugate correction against slow angular drift that stays within the hemisphere but gradually moves away from the session concept attractor. + Computes the transition rotor R = anchor * reverse(F), scales it to + R^α via rotor_power (stays on the versor manifold by construction), and + applies it via versor_apply. This replaces the previous _slerp_toward + approach, which interpolated on S^31 rather than on the Spin sub-manifold + and required a post-hoc unitize_versor to repair the manifold violation. + α=0.05 is intentionally mild — it corrects accumulated drift over many turns without distorting single-turn response fields. """ if self._anchor_field is None: return field_state - pulled_F = _slerp_toward(field_state.F, self._anchor_field, _ANCHOR_PULL_ALPHA) - pulled_F = unitize_versor(pulled_F) + try: + R = word_transition_rotor(field_state.F, self._anchor_field) + except ValueError: + return field_state + R_step = rotor_power(R, _ANCHOR_PULL_ALPHA) + pulled_F = versor_apply(R_step, field_state.F) return FieldState( F=pulled_F, node=field_state.node, diff --git a/tests/test_session_coherence.py b/tests/test_session_coherence.py index a4d8b6fe..1290e827 100644 --- a/tests/test_session_coherence.py +++ b/tests/test_session_coherence.py @@ -3,7 +3,7 @@ from __future__ import annotations import numpy as np from algebra.backend import cga_inner -from algebra.versor import unitize_versor +from algebra.versor import unitize_versor, versor_condition from session.context import SessionContext from vocab.manifold import VocabManifold @@ -106,3 +106,35 @@ def test_repeated_prompt_accumulates_field_and_stays_prompt_coherent() -> None: prompt_score = cga_inner(result.final_state.F, initial.F) random_score = cga_inner(result.final_state.F, random_unrelated) assert prompt_score > random_score + + +def test_anchor_pull_output_satisfies_versor_condition() -> None: + """_anchor_pull must not break the versor condition (W-015 fix contract). + + The previous _slerp_toward implementation interpolated on S^31 rather than + the Spin sub-manifold, producing versor_condition values up to 38.58. + The rotor-geodesic replacement must satisfy vc < 1e-6 by construction. + """ + from field.state import FieldState + + vocab = _vocab() + session = SessionContext(vocab=vocab) + session.ingest(["logos"]) + + # Build a drifted field well separated from the anchor. + drifted = _random_rotor(seed=42) + drifted_state = FieldState( + F=drifted, + node=session.state.node, + step=session.state.step, + holonomy=session.state.holonomy, + energy=session.state.energy, + valence=session.state.valence, + ) + + pulled = session._anchor_pull(drifted_state) + vc = versor_condition(pulled.F) + assert vc < 1e-6, ( + f"_anchor_pull output violated versor_condition: {vc:.3e} >= 1e-6. " + "The rotor-geodesic path must stay on the Spin sub-manifold by construction." + )