diff --git a/core/physics/__init__.py b/core/physics/__init__.py index d81406a4..56e42ae0 100644 --- a/core/physics/__init__.py +++ b/core/physics/__init__.py @@ -29,6 +29,7 @@ from core.physics.goldtether import ( AutonomyBand, AutonomyDecision, CoherenceResidual, + GoldPromotionProof, GoldTetherMonitor, OperatingMode, coherence_residual, @@ -83,7 +84,7 @@ __all__ = [ "IdentityManifold", "IdentityCheck", "IdentityScore", "CharacterProfile", "PromotionDecision", "VaultPromotionPolicy", "AutonomyBand", "AutonomyDecision", "CoherenceResidual", - "GoldTetherMonitor", "OperatingMode", "coherence_residual", + "GoldPromotionProof", "GoldTetherMonitor", "OperatingMode", "coherence_residual", "AxisClassification", "CartanIwasawaFactors", "ConformalProcrustesResult", "PrincipalAxis", "SignatureAwarePCAResult", "cartan_iwasawa_extract", "cartan_iwasawa_factorize", diff --git a/core/physics/goldtether.py b/core/physics/goldtether.py index 1255538d..78ffe2ae 100644 --- a/core/physics/goldtether.py +++ b/core/physics/goldtether.py @@ -24,7 +24,7 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum -from typing import Any, Optional, Tuple +from typing import Any, Literal, Optional, Tuple import numpy as np @@ -39,6 +39,23 @@ _TELEMETRY_SCHEMA = "goldtether_coherence_v2" # v2: dropped vacuous grade-5 cha _E4_IDX = 4 _E5_IDX = 5 +PruneMode = Literal["fifo", "principal_axes"] + + +@dataclass(frozen=True, slots=True) +class GoldPromotionProof: + """Caller-supplied proof for replay-verified promotion into 𝓘_gold (ADR-0092). + + Physics never signs reviews. The review surface constructs this payload and + passes ``authorized=True`` only after external verification. ``replay_hash`` + is opaque to GoldTether (determinism pin for the caller). + """ + + residual: float + replay_hash: str + reviewer_id: str + closed: bool + def _primal_gold_invariants() -> list: """R&D-Revised §5 bootstrapping seeds: the identity versor and the two @@ -273,26 +290,128 @@ class GoldTetherMonitor: alpha = self.alpha_constraint(F, mode=mode) return self.supervised_blend(v_self, v_constraint, alpha) - def promote_gold_invariant(self, F: np.ndarray, *, authorized: bool = False) -> None: - """Add a state versor to 𝓘_gold. CALLER-GATED: the ADR-0092 signed / - replay-verified promotion happens in the caller; this refuses to - self-authorize (one-mutation-path discipline). The full replay-verified - promotion pipeline + principal-axis decay are deferred (issue #18 follow-up). + def promotion_eligible(self, F: np.ndarray) -> bool: + """True iff F is closed and unit-residual (drift) is at/below ε_drift. + + Bootstrap gate (R&D §5): only coherent states are candidates for 𝓘_gold. + Uses the dual-checked *closure* residual (not geo distance to gold), so + novel closed states remain eligible for promotion. Does not authorize. + """ + F_arr = _as_mv(F) + if float(versor_condition(F_arr)) >= _CLOSURE_TOL: + return False + return float(coherence_residual(F_arr)) <= float(self.epsilon_drift) + + def promote_gold_invariant( + self, + F: np.ndarray, + *, + authorized: bool = False, + proof: Optional[GoldPromotionProof] = None, + require_proof: bool = False, + ) -> None: + """Add a state versor to 𝓘_gold. CALLER-GATED (ADR-0092). + + - Without ``authorized=True``: refuse (proposal-only; proof alone is insufficient). + - With authorize: refuse non-closed or high *drift* residual (live check — + never trusts proof.closed / proof.residual as truth). + - ``require_proof=True``: refuse if ``proof`` is missing. + - Physics never self-signs reviews; ``proof`` is caller-supplied audit pin. """ if not authorized: raise ValueError( "promote_gold_invariant requires explicit authorization (ADR-0092 gate)" ) - self.gold_invariants.append(_as_mv(F).copy()) + if require_proof and proof is None: + raise ValueError("promote_gold_invariant requires proof when require_proof=True") - def prune_gold_invariants(self, max_size: int = 64) -> None: - """Bound 𝓘_gold (decay hook), always retaining the three primal seeds. - Full principal-axis pruning (R&D-Revised §5) is deferred.""" + F_arr = _as_mv(F) + cond = float(versor_condition(F_arr)) + # Closure residual only (geo distance to 𝓘_gold is expected for new axes). + drift = float(coherence_residual(F_arr)) + if cond >= _CLOSURE_TOL or drift > float(self.epsilon_drift): + raise ValueError( + "promote_gold_invariant refused: not a closed versor " + f"(versor_condition={cond:.3e}) or residual/drift {drift:.3e} " + f"exceeds epsilon_drift={float(self.epsilon_drift)}" + ) + self.gold_invariants.append(F_arr.copy()) + + def prune_gold_invariants( + self, + max_size: int = 64, + *, + mode: PruneMode | str = "fifo", + ) -> None: + """Bound 𝓘_gold, always retaining the three primal seeds. + + Modes: + * ``fifo`` — keep primals + most recent (#24). + * ``principal_axes`` — keep primals + highest principal-energy non-primals + (R&D §5 decay; coefficient PCA on the non-primal stack). + ``max_size < 3`` is clamped to 3 so primals are never stripped. + """ + mode_s = str(mode) + if mode_s not in ("fifo", "principal_axes"): + raise ValueError(f"prune_gold_invariants unknown mode: {mode_s!r}") max_size = max(3, int(max_size)) - if len(self.gold_invariants) > max_size: + if len(self.gold_invariants) <= max_size: + return + if mode_s == "fifo": primal = self.gold_invariants[:3] - recent = self.gold_invariants[3:][-(max_size - 3):] + recent = self.gold_invariants[3:][-(max_size - 3) :] self.gold_invariants = primal + recent + return + self._prune_principal_axes(max_size) + + def _prune_principal_axes(self, max_size: int) -> None: + """R&D §5: retain primals + non-primals with highest principal-subspace energy. + + Stack non-primal 32-vectors as columns, take top eigen-directions of + ``XXᵀ/m``, score each member by squared projection onto that subspace, + keep the top ``max_size - 3`` (stable by original index on ties). + Differs from FIFO (last-N) whenever early high-energy axes outrank recent + near-identity members. + """ + primal = list(self.gold_invariants[:3]) + rest = [ + np.asarray(v, dtype=np.float64).copy() for v in self.gold_invariants[3:] + ] + n_keep = int(max_size) - 3 + if n_keep <= 0 or not rest: + self.gold_invariants = primal + return + if len(rest) <= n_keep: + self.gold_invariants = primal + rest + return + + X = np.column_stack(rest) # (32, m) + m = X.shape[1] + # Gram on ambient 32-space (deterministic; no external deps). + C = (X @ X.T) / float(max(m, 1)) + evals, evecs = np.linalg.eigh(C) + # Leading subspace dimension: enough to distinguish members, ≤ n_keep. + k = max(1, min(n_keep, m, N_COMPONENTS)) + order = np.argsort(evals)[::-1] + basis = evecs[:, order[:k]] # (32, k) + scored: list[tuple[float, int]] = [] + for i, v in enumerate(rest): + coeff = basis.T @ v + energy = float(coeff @ coeff) + scored.append((energy, i)) + # Highest energy first; lower index wins ties (stable, anti-FIFO bias). + scored.sort(key=lambda t: (-t[0], t[1])) + keep_idx = sorted(i for _e, i in scored[:n_keep]) + kept = [rest[i] for i in keep_idx] + # Non-primal retained members should stay closed when they entered as versors. + for i, inv in enumerate(kept): + cond = float(versor_condition(inv)) + if cond >= _CLOSURE_TOL: + raise ValueError( + f"prune principal_axes retained non-closed member[{i}]: " + f"versor_condition={cond:.3e}" + ) + self.gold_invariants = primal + kept def measure(self, F: np.ndarray, reference: Optional[np.ndarray] = None) -> CoherenceResidual: """Structured residual (primary + optional geometric distance to reference).""" diff --git a/docs/research/third-door-blueprint-fidelity.md b/docs/research/third-door-blueprint-fidelity.md index 957500b5..85e0d56b 100644 --- a/docs/research/third-door-blueprint-fidelity.md +++ b/docs/research/third-door-blueprint-fidelity.md @@ -32,7 +32,7 @@ | 1 | Signature-aware PCA | Super §2.1 / R&D §2.1 | 🟢 faithful (one untested add-on) | — | | 2 | Cartan–Iwasawa decomposition | Super §2.2 | 🟢 faithful (null-point peel + Spin remainder) | #16 | | 3 | Conformal Procrustes | Super §3.1 | 🟢 faithful (Kabsch + field conjugacy) | #17 | -| 4 | GoldTether residual + α law | Super §2.3, R&D §2.3/§5 | 🟡 partial (#24 residual+α landed; bootstrap/prune deferred) | #18 | +| 4 | GoldTether residual + α law | Super §2.3, R&D §2.3/§5 | 🟢 residual+α + bootstrap gates + principal-axis prune (#24+#18) | #18 | | 5 | Grade-5 pseudoscalar invariant | Super §3.3 | ⚪ RETIRED — vacuous in odd-dim Cl(4,1) | #19 (closed) | | 6 | Surprise residual operator | Super §3.2 | 🟢 math + DiscoveryCandidate wiring landed (#26 + #31) | #20 | | 7 | Trajectory invariants + zero-fabrication | R&D §2.2 | ⚫ absent | #21 | @@ -106,13 +106,15 @@ Two fields `F_A`, `F_B` are structurally analogous iff a single versor `V` maps - `supervised_transition` / `supervised_blend` on the Spin geodesic (`word_transition_rotor` + `rotor_power`). - `promote_gold_invariant(..., authorized=True)` is **caller-gated** (no self-authorize); `prune_gold_invariants` is a size bound retaining the three primals — **not** full principal-axis decay. -### Remaining gap (#18 follow-up — deferred while wave GoldTether lands) -- Full ADR-0092 / replay-verified promotion pipeline into `𝓘_gold`. -- Principal-axis decay/pruning of the gold set (R&D §5). -- ADR-0241 upgrade: unitary amplitude residual on \(\psi\) + optional chiral spinor charge (does not replace SERVE-never-autonomous). +### Bootstrap / prune (#18 — landed) +- `GoldPromotionProof` + `promote_gold_invariant(..., proof=, require_proof=)`: proposal-only without `authorized=True`; live **closure drift** gate (not geo-to-gold); refuses non-closed / high-drift even when authorized; never trusts proof fields as truth. +- `promotion_eligible(F)`: closed ∧ drift ≤ ε_drift (does not self-authorize). +- `prune_gold_invariants(mode="fifo"|"principal_axes")`: primals immovable; PCA ranks non-primals by principal-subspace energy (differs from last-N FIFO); `max_size < 3` clamped. +- Unitary residual path still via wave (`measure_unitary_residual`); SERVE never autonomous. -### Done right (remaining) -Wire replay-verified bootstrap + principal-axis prune; subsume residual readout into wave unitary residual without reopening serve autonomy. Preserve fail-closed + serve-never-autonomous. +### Explicit non-goals still open +- Full signed ADR-0092 *reviewer service* wiring (physics stays caller-gated only). +- Durable vault-backed gold set (session monitor field only). --- @@ -294,7 +296,7 @@ PY ### Deferred (explicit, not namesake green) - Durable holographic memory **vault store** (CRDT-backed standing-wave spectrum) — session registry only today. - Rust/MLX acceleration of exp-map / cross-spectral (ADR-0235 later). -- #18 gold-set **bootstrap/prune** (replay-verified promotion + principal-axis decay). +- Full ADR-0092 reviewer-service integration (promote remains caller-gated). - R&D #21 trajectory invariants + ADR-DAG embedding. --- @@ -305,7 +307,7 @@ PY |---|---| | Real Cartan–Iwasawa via `n_o`/`n∞` — 🟢 done (null-point peel + Spin remainder) | #16 (closed via #29) | | Kabsch-conformal Procrustes on point sets — 🟢 done | #17 (closed via #29) | -| GoldTether gold-set + harmonized residual + α=Φ(R) — 🟡 partial (#24); bootstrap/prune remain | #18 | +| GoldTether gold-set + harmonized residual + α=Φ(R) + bootstrap/prune — 🟢 | #18 | | Grade-5 pseudoscalar preservation gate — ⚪ RETIRED (vacuous; see §5) | #19 (closed) | | Surprise: metric projection + productivity polarity + DiscoveryCandidate wiring — 🟢 done | #20 (math #26; wiring #31) | | Absent proposals: sensorimotor + ADR-DAG | #21 | diff --git a/tests/test_adr_0238_goldtether_bootstrap_prune.py b/tests/test_adr_0238_goldtether_bootstrap_prune.py new file mode 100644 index 00000000..fe21a2f2 --- /dev/null +++ b/tests/test_adr_0238_goldtether_bootstrap_prune.py @@ -0,0 +1,233 @@ +"""ADR-0238 / R&D §5 — GoldTether 𝓘_gold bootstrap + principal-axis prune (#18). + +Residual+α already landed (#24). Remaining fidelity: + * promote with proof payload (closed + residual gate + optional replay hash) + * still proposal-only unless authorized (ADR-0092 gate) + * principal-axis prune retains primals, drops low-eigen members + * retained members stay closed (versor_condition < 1e-6) + +See docs/research/third-door-blueprint-fidelity.md finding #4 / issue #18. +""" + +from __future__ import annotations + +import hashlib + +import numpy as np +import pytest + +from algebra.rotor import make_rotor_from_angle +from algebra.versor import versor_condition +from core.physics.goldtether import ( + GoldPromotionProof, + GoldTetherMonitor, + OperatingMode, +) + + +def _id() -> np.ndarray: + v = np.zeros(32, dtype=np.float64) + v[0] = 1.0 + return v + + +def _closed_rotor(angle: float = 0.3, plane: int = 6) -> np.ndarray: + return make_rotor_from_angle(angle, bivector_idx=plane) + + +def _dirty() -> np.ndarray: + v = np.zeros(32, dtype=np.float64) + v[0] = 0.5 + v[1] = 0.5 + return v + + +def _proof_for(F: np.ndarray, *, residual: float | None = None) -> GoldPromotionProof: + m = GoldTetherMonitor() + r = float(m.goldtether_residual(F) if residual is None else residual) + payload = np.asarray(F, dtype=np.float64).tobytes() + f"{r:.12g}".encode() + return GoldPromotionProof( + residual=r, + replay_hash=hashlib.sha256(payload).hexdigest(), + reviewer_id="test-reviewer", + closed=versor_condition(F) < 1e-6, + ) + + +# --- promote: ADR-0092 gate + proof discipline --------------------------------- + + +def test_promote_refuses_without_authorization_even_with_proof(): + """Proposal-only: proof alone must not mutate 𝓘_gold.""" + m = GoldTetherMonitor() + F = _closed_rotor(0.2) + proof = _proof_for(F) + with pytest.raises(ValueError, match="authorization|ADR-0092|authorized"): + m.promote_gold_invariant(F, proof=proof) + assert len(m.gold_invariants) == 3 + + +def test_promote_refuses_without_authorization(): + m = GoldTetherMonitor() + with pytest.raises(ValueError, match="authorization|ADR-0092|authorized"): + m.promote_gold_invariant(_closed_rotor(0.3)) + assert len(m.gold_invariants) == 3 + + +def test_promote_refuses_non_closed_even_when_authorized(): + m = GoldTetherMonitor() + dirty = _dirty() + with pytest.raises(ValueError, match="closed|versor|closure"): + m.promote_gold_invariant(dirty, authorized=True, proof=_proof_for(dirty, residual=0.0)) + assert len(m.gold_invariants) == 3 + + +def test_promote_refuses_high_residual_even_when_authorized(): + """Drift-loud states must not enter 𝓘_gold even under explicit authorize.""" + m = GoldTetherMonitor() + dirty = _dirty() + # proof claims residual 0 but live residual is large — refuse + with pytest.raises(ValueError, match="residual|ε|epsilon|drift"): + m.promote_gold_invariant( + dirty, + authorized=True, + proof=GoldPromotionProof( + residual=0.0, + replay_hash="0" * 64, + reviewer_id="test", + closed=False, + ), + ) + assert len(m.gold_invariants) == 3 + + +def test_promote_requires_proof_when_require_proof_true(): + m = GoldTetherMonitor() + F = _closed_rotor(0.15) + with pytest.raises(ValueError, match="proof"): + m.promote_gold_invariant(F, authorized=True, require_proof=True) + assert len(m.gold_invariants) == 3 + + +def test_promote_authorized_closed_low_residual_with_proof_appends(): + m = GoldTetherMonitor() + F = _id() # on-seed, residual 0 + proof = _proof_for(F) + m.promote_gold_invariant(F, authorized=True, proof=proof, require_proof=True) + assert len(m.gold_invariants) == 4 + assert np.allclose(m.gold_invariants[-1], F) + + +def test_promote_determinism_same_proof_payload(): + F = _closed_rotor(0.4) + p1 = _proof_for(F) + p2 = _proof_for(F) + assert p1.replay_hash == p2.replay_hash + assert p1.residual == p2.residual + + +# --- prune: principal-axis mode ----------------------------------------------- + + +def test_prune_fifo_mode_retains_primals(): + """Backward-compatible default: size bound keeps three primals.""" + m = GoldTetherMonitor() + for i in range(40): + m.promote_gold_invariant(_closed_rotor(0.01 * i + 0.05), authorized=True) + m.prune_gold_invariants(max_size=10, mode="fifo") + assert len(m.gold_invariants) == 10 + assert m.gold_invariants[0][0] == 1.0 + assert m.gold_invariants[1][5] == 0.5 + assert m.gold_invariants[2][4] == 1.0 and m.gold_invariants[2][5] == 1.0 + + +def test_prune_principal_axes_retains_primals_and_bounds_size(): + m = GoldTetherMonitor() + for i in range(30): + # Stay on Euclidean rotation planes (6–8); boost planes need cosh form. + m.promote_gold_invariant( + _closed_rotor(0.05 * i + 0.1, plane=6 + (i % 3)), + authorized=True, + ) + n_before = len(m.gold_invariants) + assert n_before > 10 + m.prune_gold_invariants(max_size=8, mode="principal_axes") + assert len(m.gold_invariants) <= 8 + assert len(m.gold_invariants) >= 3 + # Three primal seeds always first and unchanged + assert m.gold_invariants[0][0] == 1.0 + assert m.gold_invariants[1][5] == 0.5 and m.gold_invariants[1][4] == -0.5 + assert m.gold_invariants[2][4] == 1.0 and m.gold_invariants[2][5] == 1.0 + + +def test_prune_principal_axes_retained_members_are_closed(): + m = GoldTetherMonitor() + for i in range(20): + m.promote_gold_invariant(_closed_rotor(0.08 * i + 0.05), authorized=True) + m.prune_gold_invariants(max_size=6, mode="principal_axes") + for i, inv in enumerate(m.gold_invariants): + # Primals n_o / n_inf are null (not unit versors); skip strict unit check + if i == 0: + assert versor_condition(inv) < 1e-6 + elif i >= 3: + assert versor_condition(inv) < 1e-6, f"member[{i}] not closed" + + +def test_prune_principal_axes_differs_from_fifo_on_ordered_promotions(): + """PCA prune is not last-N FIFO: different retention among non-primals. + + Promote a long chain; FIFO keeps the most recent, principal_axes ranks by + energy/span. With enough spread, the retained non-primal sets must differ + (or at least the API mode must be accepted and size-bounded). + """ + m_fifo = GoldTetherMonitor() + m_pca = GoldTetherMonitor() + for i in range(25): + F = _closed_rotor(0.12 * i + 0.05, plane=6 + (i % 3)) + m_fifo.promote_gold_invariant(F, authorized=True) + m_pca.promote_gold_invariant(F, authorized=True) + m_fifo.prune_gold_invariants(max_size=7, mode="fifo") + m_pca.prune_gold_invariants(max_size=7, mode="principal_axes") + assert len(m_fifo.gold_invariants) == 7 + assert len(m_pca.gold_invariants) <= 7 + # Non-primal tails: PCA must not be a silent alias of FIFO forever. + fifo_tail = [tuple(np.round(v, 8)) for v in m_fifo.gold_invariants[3:]] + pca_tail = [tuple(np.round(v, 8)) for v in m_pca.gold_invariants[3:]] + assert fifo_tail != pca_tail or len(pca_tail) < len(fifo_tail) + + +def test_prune_unknown_mode_refused(): + m = GoldTetherMonitor() + with pytest.raises(ValueError, match="mode"): + m.prune_gold_invariants(max_size=5, mode="not_a_mode") + + +def test_prune_refuses_stripping_below_primal_count(): + """max_size < 3 must not drop primal seeds (clamp / refuse).""" + m = GoldTetherMonitor() + m.promote_gold_invariant(_closed_rotor(0.2), authorized=True) + m.prune_gold_invariants(max_size=2, mode="principal_axes") + assert len(m.gold_invariants) >= 3 + assert m.gold_invariants[0][0] == 1.0 + + +# --- eligibility helper (bootstrap pipeline surface) -------------------------- + + +def test_promotion_eligible_true_for_closed_on_seed(): + m = GoldTetherMonitor() + assert m.promotion_eligible(_id()) is True + + +def test_promotion_eligible_false_for_dirty(): + m = GoldTetherMonitor() + assert m.promotion_eligible(_dirty()) is False + + +def test_serve_mode_never_relaxes_via_gold_set_growth(): + """Containment: growing 𝓘_gold does not make SERVE autonomous.""" + m = GoldTetherMonitor() + m.autonomy = 1.0 + for i in range(5): + m.promote_gold_invariant(_closed_rotor(0.05 * i), authorized=True) + assert m.alpha_constraint(_id(), mode=OperatingMode.SERVE) == 1.0