fix(W-015): replace _slerp_toward with rotor-geodesic anchor pull (#255)

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.
This commit is contained in:
Shay 2026-05-24 20:05:25 -07:00 committed by GitHub
parent bf3baa8bfa
commit 11c91581e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 54 additions and 40 deletions

View file

@ -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-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-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-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-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` | `acd51d0c17948030aa2843309a3729c6f825959e2e0cbbc477e07de8269268a4` | | 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` | | ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d` |
## Verification ## Verification

View file

@ -30,7 +30,7 @@
"details": { "details": {
"all_claims_supported_a": true, "all_claims_supported_a": true,
"all_claims_supported_b": true, "all_claims_supported_b": true,
"sha256": "deb3af8fab8f3ff9cf7468efd7a403b60c825a67b76a5c07ab95147f79bd2e9a" "sha256": "6a02197c0d9fae0eb041aaed9d8c864291477c066ec915ca403cf774c301f7d3"
}, },
"divergence": null, "divergence": null,
"passed": true "passed": true

View file

@ -13,7 +13,7 @@
{ {
"case_id": "determinism_run_to_run_byte_equality", "case_id": "determinism_run_to_run_byte_equality",
"details": { "details": {
"sha256": "48fc9cba028d5d8e8e098d71cd25a140861fcf380c1214612358bce433f79996" "sha256": "58e4ddec73aba31b0e193d573921cd85df7fd225ba68716eeef1517b635c6141"
}, },
"divergence": null, "divergence": null,
"passed": true "passed": true

View file

@ -38,8 +38,8 @@ PINNED_SHAS: dict[str, str] = {
"curriculum_loop_closure": "b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d", "curriculum_loop_closure": "b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d",
"domain_contract_validation": "98ace04e3f02bbc5a8ad655bb6593c3f1ee64cb67014f1122fe6c3c85f48d22f", "domain_contract_validation": "98ace04e3f02bbc5a8ad655bb6593c3f1ee64cb67014f1122fe6c3c85f48d22f",
"fabrication_control_summary": "01e1b6b711141f2b4a14551d7df3ea482d8d6dd7b364a25c509f4f8d08cda8a8", "fabrication_control_summary": "01e1b6b711141f2b4a14551d7df3ea482d8d6dd7b364a25c509f4f8d08cda8a8",
"demo_composition": "403be13bd666389bdf1caa1b97382753e531ad72686c5237f64a5e23bc702ac4", "demo_composition": "3a3d09f3a87462737e615c2dd3481b9e13e5ff8fadee0043c37873494ded556d",
"public_demo": "acd51d0c17948030aa2843309a3729c6f825959e2e0cbbc477e07de8269268a4", "public_demo": "888ddd0d12635d709f91898cf06601062168694d870f776d2b0b7710e5d68cbd",
} }

View file

@ -11,7 +11,8 @@ from __future__ import annotations
import numpy as np import numpy as np
from algebra.backend import cga_inner, versor_apply 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 field.state import FieldState
from generate.dialogue import DialogueTurn from generate.dialogue import DialogueTurn
from generate.proposition import Proposition from generate.proposition import Proposition
@ -35,35 +36,6 @@ _BLADE_EMA_ALPHA: float = 0.15
_ANCHOR_PULL_ALPHA: float = 0.05 _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: class SessionContext:
def __init__(self, vocab, persona=None, vault=None, vault_reproject_interval: int = 100): def __init__(self, vocab, persona=None, vault=None, vault_reproject_interval: int = 100):
self.vocab = vocab self.vocab = vocab
@ -221,19 +193,29 @@ class SessionContext:
) )
def _anchor_pull(self, field_state: FieldState) -> FieldState: 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 Applied after hemisphere correction. Provides continuous conjugate
correction against slow angular drift that stays within the hemisphere correction against slow angular drift that stays within the hemisphere
but gradually moves away from the session concept attractor. 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 α=0.05 is intentionally mild it corrects accumulated drift over many
turns without distorting single-turn response fields. turns without distorting single-turn response fields.
""" """
if self._anchor_field is None: if self._anchor_field is None:
return field_state return field_state
pulled_F = _slerp_toward(field_state.F, self._anchor_field, _ANCHOR_PULL_ALPHA) try:
pulled_F = unitize_versor(pulled_F) 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( return FieldState(
F=pulled_F, F=pulled_F,
node=field_state.node, node=field_state.node,

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import numpy as np import numpy as np
from algebra.backend import cga_inner 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 session.context import SessionContext
from vocab.manifold import VocabManifold 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) prompt_score = cga_inner(result.final_state.F, initial.F)
random_score = cga_inner(result.final_state.F, random_unrelated) random_score = cga_inner(result.final_state.F, random_unrelated)
assert prompt_score > random_score 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."
)