feat(physics): GoldTether, dynamic manifold, surprise dual, biography holonomy (refs #11 #12 #13)

Implement Third-Door operators:
- goldtether: coherence residual, dynamic floor, practice/serve autonomy
- dynamic_manifold: signature-aware PCA, Procrustes, Cartan-Iwasawa
- surprise: residual + dual with Procrustes
- biography / temporal_gate / self_authorship: lifelong + proposal-only

All versor outputs enforce versor_condition < 1e-6. Spin left-composition
geodesic for supervised blend. Arena GoldTether (ADR-0199) untouched.
This commit is contained in:
Shay 2026-07-11 22:01:13 -07:00
parent bad9b87bff
commit a8e9977cbc
7 changed files with 1606 additions and 0 deletions

View file

@ -5,6 +5,10 @@ Three physics sublayers:
compositional binding, digest, reasoning, articulation (ADR-0009)
identity identity manifold, drives, exertion, character (ADR-0010)
Third-Door Horizon (ADR-02380240):
coherence GoldTether, dynamic manifold (Procrustes/PCA), surprise dual,
biography holonomy, temporal gate, self-authorship miner.
All operators are stateless and frozen where possible.
State lives in the FieldState; operators are pure transformations.
"""
@ -22,6 +26,50 @@ from core.physics.drive import DriveGradientMap, GradientField, ValueAxis
from core.physics.exertion import ExertionMeter, FatigueIndex, CycleCost
from core.physics.identity import IdentityManifold, IdentityCheck, IdentityScore, CharacterProfile
from core.physics.learning import PromotionDecision, VaultPromotionPolicy
from core.physics.goldtether import (
AutonomyBand,
AutonomyDecision,
CoherenceResidual,
GoldTetherConfig,
GoldTetherMonitor,
OperatingMode,
PseudoscalarFloorState,
derive_kappa,
)
from core.physics.dynamic_manifold import (
AxisClassification,
CartanIwasawaFactors,
ConformalProcrustesResult,
PrincipalAxis,
SignatureAwarePCAResult,
cartan_iwasawa_factorize,
conformal_procrustes,
dual_correction_slerp,
procrustes_residual,
signature_aware_pca,
)
from core.physics.surprise import (
AnalogySeed,
DualOperatorResult,
SurpriseResult,
analogy_seed,
dual_operator,
project_onto_basis,
surprise_residual,
)
from core.physics.biography import (
BiographyHolonomyBlade,
biography_telemetry,
integrate_biography,
reconstruct_biography,
)
from core.physics.temporal_gate import (
TemporalAdmissibilityGate,
TemporalContext,
TemporalDecision,
TemporalVerdict,
)
from core.physics.self_authorship import AuthorshipProposal, SelfAuthorshipMiner
__all__ = [
"SalienceOperator", "SalienceMap", "FieldRegion",
@ -36,4 +84,42 @@ __all__ = [
"ExertionMeter", "FatigueIndex", "CycleCost",
"IdentityManifold", "IdentityCheck", "IdentityScore", "CharacterProfile",
"PromotionDecision", "VaultPromotionPolicy",
# ADR-0238
"AutonomyBand",
"AutonomyDecision",
"CoherenceResidual",
"GoldTetherConfig",
"GoldTetherMonitor",
"OperatingMode",
"PseudoscalarFloorState",
"derive_kappa",
# ADR-0239
"AxisClassification",
"CartanIwasawaFactors",
"ConformalProcrustesResult",
"PrincipalAxis",
"SignatureAwarePCAResult",
"cartan_iwasawa_factorize",
"conformal_procrustes",
"dual_correction_slerp",
"procrustes_residual",
"signature_aware_pca",
"AnalogySeed",
"DualOperatorResult",
"SurpriseResult",
"analogy_seed",
"dual_operator",
"project_onto_basis",
"surprise_residual",
# ADR-0240
"BiographyHolonomyBlade",
"biography_telemetry",
"integrate_biography",
"reconstruct_biography",
"TemporalAdmissibilityGate",
"TemporalContext",
"TemporalDecision",
"TemporalVerdict",
"AuthorshipProposal",
"SelfAuthorshipMiner",
]

107
core/physics/biography.py Normal file
View file

@ -0,0 +1,107 @@
"""core.physics.biography — Biography Holonomy Blade (ADR-0240).
Forever-lived individuality as an integrated holonomy of the identity
trajectory. Reconstructible from an ordered sequence of session versors
reconstruction-over-storage. Not a raw experience dump; not a parallel
identity store.
Integrates with ``algebra.holonomy.holonomy_encode`` and the identity motor
surface; does not mutate packs, vault, or serving paths.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Sequence
import numpy as np
from algebra.cl41 import N_COMPONENTS
from algebra.holonomy import holonomy_encode, holonomy_similarity
from algebra.versor import unitize_versor, versor_condition
_CLOSURE_TOL = 1e-6
_TELEMETRY_SCHEMA = "biography_holonomy_v1"
@dataclass(frozen=True, slots=True)
class BiographyHolonomyBlade:
"""Integrated holonomy blade of a lived trajectory."""
blade: np.ndarray
n_steps: int
trajectory_hash: str
closure: float
def similarity(self, other: "BiographyHolonomyBlade") -> float:
return float(holonomy_similarity(self.blade, other.blade))
def _as_versor(v: np.ndarray, name: str) -> np.ndarray:
arr = np.asarray(v, dtype=np.float64)
if arr.shape != (N_COMPONENTS,):
raise ValueError(f"{name} must have shape ({N_COMPONENTS},)")
# Construction boundary: trajectory elements must be closed versors.
try:
closed = unitize_versor(arr)
except ValueError as exc:
raise ValueError(f"{name} is not a closed versor: {exc}") from exc
cond = versor_condition(closed)
if cond >= _CLOSURE_TOL:
raise ValueError(f"{name} versor_condition={cond:.3e}")
return closed.astype(np.float64, copy=False)
def _trajectory_hash(versors: Sequence[np.ndarray]) -> str:
import hashlib
h = hashlib.sha256()
for v in versors:
h.update(np.asarray(v, dtype=np.float64).tobytes())
return h.hexdigest()
def integrate_biography(
trajectory: Sequence[np.ndarray],
*,
alpha: float = 0.5,
) -> BiographyHolonomyBlade:
"""Integrate ordered identity/session versors into a biography holonomy blade.
Order is load-bearing. Empty trajectory is refused (no confabulated self).
"""
if not trajectory:
raise ValueError("biography trajectory must be non-empty")
closed = [_as_versor(v, f"trajectory[{i}]") for i, v in enumerate(trajectory)]
blade = holonomy_encode(closed, alpha=alpha)
cond = versor_condition(blade)
if cond >= _CLOSURE_TOL:
raise ValueError(f"biography blade not closed: {cond:.3e}")
return BiographyHolonomyBlade(
blade=np.asarray(blade, dtype=np.float64),
n_steps=len(closed),
trajectory_hash=_trajectory_hash(closed),
closure=float(cond),
)
def reconstruct_biography(
trajectory: Sequence[np.ndarray],
*,
alpha: float = 0.5,
) -> BiographyHolonomyBlade:
"""Alias for integrate — reconstruction is recompute, not storage load."""
return integrate_biography(trajectory, alpha=alpha)
def biography_telemetry(blade: BiographyHolonomyBlade) -> dict[str, Any]:
"""Workbench-safe projection (no full multivector dump required for UI)."""
return {
"schema_version": _TELEMETRY_SCHEMA,
"n_steps": int(blade.n_steps),
"trajectory_hash": blade.trajectory_hash,
"closure": float(blade.closure),
"blade_scalar": float(blade.blade[0]),
"blade_pseudoscalar": float(blade.blade[31]),
"blade_l2": float(np.linalg.norm(blade.blade)),
}

View file

@ -0,0 +1,449 @@
"""core.physics.dynamic_manifold — Conformal manifold operators (ADR-0239).
Signature-aware PCA with explicit null classification, Conformal Procrustes
(versor search for structural analogy), CartanIwasawa constructive
factorization for dual-correction slerp, and a dedicated Procrustes residual
norm (not null-margin; not ADR-0006 energy residual).
All operators are pure and deterministic. Null eigenvectors are never silently
skipped they are classified and returned.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Sequence
import numpy as np
from algebra.cl41 import (
N_COMPONENTS,
SIGNATURE,
geometric_product,
grade_project,
reverse,
)
from algebra.rotor import word_transition_rotor
from algebra.versor import unitize_versor, versor_apply, versor_condition
_CLOSURE_TOL = 1e-6
_NEAR_ZERO = 1e-12
_NULL_TOL = 1e-8
_METRIC = np.ones(N_COMPONENTS, dtype=np.float64)
# Grade-1 components (indices 1..5) carry Cl(4,1) signature (+,+,+,+,-).
_METRIC[1:6] = SIGNATURE.astype(np.float64)
class AxisClassification(str, Enum):
SPACELIKE = "spacelike"
TIMELIKE = "timelike"
NULL = "null"
DEGENERATE = "degenerate"
@dataclass(frozen=True, slots=True)
class PrincipalAxis:
vector: tuple[float, ...]
eigenvalue: float
classification: AxisClassification
metric_quadratic: float
@dataclass(frozen=True, slots=True)
class SignatureAwarePCAResult:
axes: tuple[PrincipalAxis, ...]
mean: tuple[float, ...]
explained: tuple[float, ...]
n_points: int
n_null: int
n_spacelike: int
n_timelike: int
n_degenerate: int
@dataclass(frozen=True, slots=True)
class ConformalProcrustesResult:
versor: np.ndarray
residual_norm: float
n_pairs: int
pair_residuals: tuple[float, ...]
@dataclass(frozen=True, slots=True)
class CartanIwasawaFactors:
"""Constructive K · A · N factorization for dual-correction surfaces.
K compact (rotation-like, < 0 planes)
A abelian (boost-like, > 0 planes)
N nilpotent / residual (null + higher-grade remainder, unitized if needed)
"""
K: np.ndarray
A: np.ndarray
N: np.ndarray
reconstruction_residual: float
def _as_points(points: Sequence[np.ndarray]) -> np.ndarray:
if not points:
raise ValueError("signature_aware_pca requires at least one point")
rows = []
for i, p in enumerate(points):
arr = np.asarray(p, dtype=np.float64)
if arr.shape != (N_COMPONENTS,):
raise ValueError(
f"point[{i}] must have shape ({N_COMPONENTS},); got {arr.shape}"
)
rows.append(arr)
return np.stack(rows, axis=0)
def _metric_quadratic_form(v: np.ndarray) -> float:
"""Quadratic form on grade-1 part under Cl(4,1) signature; higher grades +.
Used only for axis *classification*, not as a substitute for versor_condition.
"""
g1 = v[1:6]
q = float(np.dot(g1 * _METRIC[1:6], g1))
higher = float(np.dot(v[6:], v[6:]))
scalar = float(v[0] * v[0])
return q + higher + scalar
def _classify_axis(v: np.ndarray) -> tuple[AxisClassification, float]:
nrm = float(np.linalg.norm(v))
if nrm < _NEAR_ZERO:
return AxisClassification.DEGENERATE, 0.0
q = _metric_quadratic_form(v)
# Grade-1 signature sense dominates classification when g1 is present.
g1 = v[1:6]
g1_q = float(np.dot(g1 * _METRIC[1:6], g1))
g1_n = float(np.linalg.norm(g1))
if g1_n < _NEAR_ZERO:
# Pure higher-grade axis: treat as spacelike if energy present else degenerate.
if nrm < _NULL_TOL:
return AxisClassification.DEGENERATE, q
return AxisClassification.SPACELIKE, q
if abs(g1_q) < _NULL_TOL:
return AxisClassification.NULL, g1_q
if g1_q > 0.0:
return AxisClassification.SPACELIKE, g1_q
return AxisClassification.TIMELIKE, g1_q
def signature_aware_pca(
points: Sequence[np.ndarray],
*,
max_axes: int | None = None,
) -> SignatureAwarePCAResult:
"""Signature-aware PCA on Cl(4,1) multivector clouds.
1. Center the cloud in coefficient space.
2. Form the metric-rescaled covariance (whitening by |G| on coords).
3. Eigen-decompose symmetrically (deterministic via numpy eigh).
4. Classify every axis null axes are returned, never dropped.
"""
X = _as_points(points)
n = X.shape[0]
mean = X.mean(axis=0)
Xc = X - mean
# Metric rescaling: multiply each coordinate by sqrt(|g_ii|)*sign-preserving weight.
# For signature -1 on e5, use imaginary-free absolute metric then restore sense
# via classification (not by complex eigen).
scale = np.sqrt(np.abs(_METRIC))
scale = np.where(scale < _NEAR_ZERO, 1.0, scale)
Y = Xc * scale # broadcast
# Covariance of rescaled coordinates.
if n == 1:
cov = np.zeros((N_COMPONENTS, N_COMPONENTS), dtype=np.float64)
else:
cov = (Y.T @ Y) / float(n)
# Symmetric eigh → ascending eigenvalues; reverse for explained variance order.
evals, evecs = np.linalg.eigh(cov)
order = np.argsort(evals)[::-1]
evals = evals[order]
evecs = evecs[:, order]
k = N_COMPONENTS if max_axes is None else min(int(max_axes), N_COMPONENTS)
total = float(np.sum(np.clip(evals, 0.0, None)))
axes: list[PrincipalAxis] = []
counts = {
AxisClassification.NULL: 0,
AxisClassification.SPACELIKE: 0,
AxisClassification.TIMELIKE: 0,
AxisClassification.DEGENERATE: 0,
}
explained_list: list[float] = []
for i in range(k):
# Map eigenvector back from metric-rescaled coords.
v_scaled = evecs[:, i]
v = v_scaled / scale
nrm = float(np.linalg.norm(v))
if nrm > _NEAR_ZERO:
v = v / nrm
# Deterministic sign convention: first nonzero component positive.
for c in v:
if abs(c) > _NEAR_ZERO:
if c < 0.0:
v = -v
break
cls, mq = _classify_axis(v)
counts[cls] = counts.get(cls, 0) + 1
ev = float(max(0.0, evals[i]))
frac = float(ev / total) if total > _NEAR_ZERO else 0.0
explained_list.append(frac)
axes.append(
PrincipalAxis(
vector=tuple(float(x) for x in v),
eigenvalue=ev,
classification=cls,
metric_quadratic=float(mq),
)
)
return SignatureAwarePCAResult(
axes=tuple(axes),
mean=tuple(float(x) for x in mean),
explained=tuple(explained_list),
n_points=n,
n_null=counts[AxisClassification.NULL],
n_spacelike=counts[AxisClassification.SPACELIKE],
n_timelike=counts[AxisClassification.TIMELIKE],
n_degenerate=counts[AxisClassification.DEGENERATE],
)
def procrustes_residual(
source: np.ndarray,
target: np.ndarray,
versor: np.ndarray,
) -> float:
"""Dedicated Procrustes residual: || V * s * reverse(V) - t ||_F.
Named separately from null-margin and energy coherence_residual so it
cannot be silently reused as a different residual.
"""
s = np.asarray(source, dtype=np.float64)
t = np.asarray(target, dtype=np.float64)
V = np.asarray(versor, dtype=np.float64)
mapped = versor_apply(V, s)
return float(np.linalg.norm(mapped - t))
def conformal_procrustes(
sources: Sequence[np.ndarray] | np.ndarray,
targets: Sequence[np.ndarray] | np.ndarray,
) -> ConformalProcrustesResult:
"""Find a unit versor aligning source structure to target structure.
Single pair: closed transition rotor.
Multiple pairs: manifold average of per-pair transition rotors via
successive equal-weight slerp composition (deterministic order).
"""
if isinstance(sources, np.ndarray) and sources.ndim == 1:
src_list = [sources]
else:
src_list = list(sources) # type: ignore[arg-type]
if isinstance(targets, np.ndarray) and targets.ndim == 1:
tgt_list = [targets]
else:
tgt_list = list(targets) # type: ignore[arg-type]
if len(src_list) != len(tgt_list):
raise ValueError("sources and targets must have equal length")
if not src_list:
raise ValueError("conformal_procrustes requires at least one pair")
rotors: list[np.ndarray] = []
for i, (s, t) in enumerate(zip(src_list, tgt_list)):
s_arr = np.asarray(s, dtype=np.float64)
t_arr = np.asarray(t, dtype=np.float64)
if s_arr.shape != (N_COMPONENTS,) or t_arr.shape != (N_COMPONENTS,):
raise ValueError(f"pair[{i}] must be 32-component multivectors")
# Construction boundary: transition rotor is a closed unit versor.
R = word_transition_rotor(s_arr, t_arr)
rotors.append(np.asarray(R, dtype=np.float64))
# Manifold average: sequential geodesic midpoints in pair order.
V = rotors[0].copy()
for k, R in enumerate(rotors[1:], start=2):
# Slerp V toward R with weight 1/k so equal contribution in the limit.
try:
T = word_transition_rotor(V, R)
from algebra.rotor import rotor_power
T_a = rotor_power(T, 1.0 / float(k))
V = geometric_product(geometric_product(T_a, V), reverse(T_a)).astype(
np.float64
)
V = unitize_versor(V)
except ValueError:
# Fail closed to previous average if a pair is non-connectable.
continue
cond = versor_condition(V)
if cond >= _CLOSURE_TOL:
raise ValueError(f"Procrustes versor not closed: condition={cond:.3e}")
pair_res = tuple(
procrustes_residual(s, t, V) for s, t in zip(src_list, tgt_list)
)
residual_norm = float(np.sqrt(sum(r * r for r in pair_res) / len(pair_res)))
return ConformalProcrustesResult(
versor=V.astype(np.float64, copy=False),
residual_norm=residual_norm,
n_pairs=len(src_list),
pair_residuals=pair_res,
)
def _identity_mv() -> np.ndarray:
out = np.zeros(N_COMPONENTS, dtype=np.float64)
out[0] = 1.0
return out
def cartan_iwasawa_factorize(V: np.ndarray) -> CartanIwasawaFactors:
"""Constructive CartanIwasawa-style factorization of a closed versor.
For simple scalar+bivector rotors:
- If < 0 pure K (rotation)
- If > 0 pure A (boost)
- If 0 and B 0 pure N (null)
- Mixed: split bivector energy by sign of contribution and leave residual N.
Reconstruction: K * A * N; residual measured in coefficient space.
"""
V_arr = np.asarray(V, dtype=np.float64)
if V_arr.shape != (N_COMPONENTS,):
raise ValueError(f"V must have shape ({N_COMPONENTS},)")
cond = versor_condition(V_arr)
if cond >= 1e-2:
# Construction boundary: attempt unitize only at this explicit boundary.
V_arr = unitize_versor(V_arr)
cond = versor_condition(V_arr)
if cond >= _CLOSURE_TOL:
raise ValueError(f"cartan_iwasawa_factorize: input not closed ({cond:.3e})")
scalar = float(V_arr[0])
B = grade_project(V_arr, 2)
higher = V_arr.copy()
higher[0] = 0.0
higher[6:16] = 0.0 # clear grade-2; keep 1,3,4,5
B_sq = geometric_product(B, B).astype(np.float64)
bsq_scalar = float(B_sq[0])
B_sq_res = B_sq.copy()
B_sq_res[0] = 0.0
simple = float(np.linalg.norm(B_sq_res)) < 1e-6
b_norm = float(np.linalg.norm(B))
I = _identity_mv()
K = I.copy()
A = I.copy()
N = I.copy()
if b_norm < _NEAR_ZERO and float(np.linalg.norm(higher)) < _NEAR_ZERO:
# Near-identity
K = V_arr.copy()
elif simple and bsq_scalar < 0.0:
K = V_arr.copy()
# Zero out non-scalar/bivector if any residual grades present.
K = grade_project(K, 0) + grade_project(K, 2)
K = unitize_versor(K)
elif simple and bsq_scalar > 0.0:
A = grade_project(V_arr, 0) + grade_project(V_arr, 2)
A = unitize_versor(A)
elif simple and abs(bsq_scalar) <= _NEAR_ZERO:
N_cand = grade_project(V_arr, 0) + grade_project(V_arr, 2)
try:
N = unitize_versor(N_cand)
except ValueError:
N = I.copy()
N[0] = scalar if abs(scalar) > _NEAR_ZERO else 1.0
N = unitize_versor(N)
else:
# Mixed: put scalar+rotation-like half in K, boost-like half in A, rest N.
# Split B into two parallel bivectors by halving coefficients when non-simple.
half = B * 0.5
K = grade_project(V_arr, 0) * 0.0
K[0] = abs(scalar) ** 0.5 if abs(scalar) > _NEAR_ZERO else 1.0
K = K + half
try:
K = unitize_versor(K)
except ValueError:
K = I.copy()
A = I.copy()
A[0] = abs(scalar) ** 0.5 if abs(scalar) > _NEAR_ZERO else 1.0
A = A + half
try:
A = unitize_versor(A)
except ValueError:
A = I.copy()
# N absorbs higher-grade residual relative to K*A
KA = geometric_product(K, A)
try:
N = unitize_versor(geometric_product(reverse(KA), V_arr))
except ValueError:
N = I.copy()
recon = geometric_product(geometric_product(K, A), N)
recon_res = float(np.linalg.norm(recon - V_arr))
for name, factor in (("K", K), ("A", A), ("N", N)):
c = versor_condition(factor)
if c >= _CLOSURE_TOL:
raise ValueError(f"CartanIwasawa factor {name} not closed: {c:.3e}")
return CartanIwasawaFactors(
K=K.astype(np.float64, copy=False),
A=A.astype(np.float64, copy=False),
N=N.astype(np.float64, copy=False),
reconstruction_residual=recon_res,
)
def dual_correction_slerp(
source: np.ndarray,
target: np.ndarray,
alpha: float,
) -> np.ndarray:
"""Slerp on CartanIwasawa factors of the transition rotor (dual-correction).
Factors are powered independently then recomposed as left action on source:
R = target * reverse(source) = K A N
out = (K^α A^α N^α) * source
α=0 source; α=1 target for unit versors. Sandwich conjugation is not
the state geodesic (see ADR-0238 supervised_blend).
"""
from algebra.rotor import rotor_power
a = float(alpha)
if a < 0.0 or a > 1.0:
raise ValueError("alpha must be in [0, 1]")
src = np.asarray(source, dtype=np.float64)
tgt = np.asarray(target, dtype=np.float64)
if a <= _NEAR_ZERO:
out = src.copy()
elif a >= 1.0 - _NEAR_ZERO:
out = tgt.copy()
else:
R = word_transition_rotor(src, tgt)
factors = cartan_iwasawa_factorize(R)
K_a = rotor_power(factors.K, a)
A_a = rotor_power(factors.A, a)
N_a = rotor_power(factors.N, a)
R_a = geometric_product(geometric_product(K_a, A_a), N_a)
R_a = unitize_versor(R_a)
out = geometric_product(R_a, src).astype(np.float64)
cond = versor_condition(out)
if cond >= _CLOSURE_TOL:
raise ValueError(f"dual_correction_slerp broke closure: {cond:.3e}")
return out

432
core/physics/goldtether.py Normal file
View file

@ -0,0 +1,432 @@
"""core.physics.goldtether — Coherence GoldTether (ADR-0238).
This module implements the *field* GoldTether: dynamic grade-5 pseudoscalar
floor, harmonized coherence residual, and practice/serve autonomy modulation.
It is intentionally distinct from the *arena* GoldTether protocol in
``core.learning_arena.protocols.GoldTether`` (ADR-0199), which scores practice
attempts against independent truth anchors. Shared metaphor; different contracts.
Never import or subclass the arena protocol from this module.
Construction boundary: supervised blend closes via ``algebra.rotor`` manifold
slerp (``word_transition_rotor`` + ``rotor_power``). No hot-path drift repair.
"""
from __future__ import annotations
from dataclasses import dataclass, field, replace
from enum import Enum
from typing import Any, Mapping
import numpy as np
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse
from algebra.rotor import rotor_power, word_transition_rotor
from algebra.versor import versor_condition
_PSEUDOSCALAR_IDX = 31
_CLOSURE_TOL = 1e-6
_NEAR_ZERO = 1e-12
_DEFAULT_DECAY_N = 32
_DEFAULT_W_DRIFT = 0.35
_DEFAULT_FLOOR_INIT = 0.15
_DEFAULT_CRITICAL_RATIO = 2.5
_TELEMETRY_SCHEMA = "goldtether_coherence_v1"
class OperatingMode(str, Enum):
"""Practice vs Serve — risk-reward physics boundary."""
PRACTICE = "practice"
SERVE = "serve"
class AutonomyBand(str, Enum):
"""Residual-relative autonomy envelope (ADR-0238)."""
AUTONOMOUS = "autonomous"
SUPERVISED_BLEND = "supervised_blend"
FAIL_CLOSED = "fail_closed"
@dataclass(frozen=True, slots=True)
class GoldTetherConfig:
"""Named configuration only — no silent magic constants at call sites."""
decay_N: int = _DEFAULT_DECAY_N
w_drift: float = _DEFAULT_W_DRIFT
floor_init: float = _DEFAULT_FLOOR_INIT
critical_ratio: float = _DEFAULT_CRITICAL_RATIO
practice_autonomy_enabled: bool = False
serve_supervised_blend_authorized: bool = False
def __post_init__(self) -> None:
if self.decay_N < 1:
raise ValueError("decay_N must be >= 1")
if not 0.0 <= self.w_drift <= 1.0:
raise ValueError("w_drift must be in [0, 1]")
if self.floor_init <= 0.0:
raise ValueError("floor_init must be positive")
if self.critical_ratio <= 1.0:
raise ValueError("critical_ratio must be > 1")
@dataclass(frozen=True, slots=True)
class CoherenceResidual:
"""Harmonized residual: drift + geometric distance (normalized).
Distinct from ADR-0006 ``EnergyProfile.coherence_residual`` and from
ADR-0239 Procrustes/Surprise residuals.
"""
drift: float
geometric_distance: float
combined: float
kappa: float
pseudoscalar_current: float
pseudoscalar_reference: float
@dataclass(frozen=True, slots=True)
class AutonomyDecision:
band: AutonomyBand
residual: float
floor: float
critical: float
mode: OperatingMode
blend_alpha: float
reason: str
@dataclass(frozen=True, slots=True)
class PseudoscalarFloorState:
"""Dynamic grade-5 coherence floor + sign/magnitude telemetry."""
value: float
sign: float
n_samples: int
last_update_step: int
primal_floor: float
recent_residuals: tuple[float, ...] = ()
def _as_mv(v: np.ndarray, name: str) -> np.ndarray:
arr = np.asarray(v, dtype=np.float64)
if arr.shape != (N_COMPONENTS,):
raise ValueError(f"{name} must have shape ({N_COMPONENTS},); got {arr.shape}")
return arr
def _pseudoscalar(v: np.ndarray) -> float:
return float(v[_PSEUDOSCALAR_IDX])
def _geometric_distance(a: np.ndarray, b: np.ndarray) -> float:
"""Closed geometric distance on the versor manifold.
Uses ``|| reverse(a) * b - 1 ||_F`` zero iff a and b are the same unit
versor (up to float noise). Not cosine similarity; not ANN.
"""
product = geometric_product(reverse(a), b).astype(np.float64)
product[0] -= 1.0
return float(np.linalg.norm(product))
def _normalize_distance(d: float, scale: float = 2.0) -> float:
"""Map unbounded residual to [0, 1) for blend with drift."""
if d <= 0.0:
return 0.0
return float(d / (d + scale))
def derive_kappa(combined_residual: float, floor: float) -> float:
"""Monotone κ from residual relative to floor.
κ (0, 1]: small residual κ near 1 (more trust); large residual κ 0.
Used only to scale dual-correction blend weight never to invent content.
"""
if floor <= _NEAR_ZERO:
floor = _NEAR_ZERO
ratio = max(0.0, float(combined_residual) / float(floor))
return float(1.0 / (1.0 + ratio))
@dataclass
class GoldTetherMonitor:
"""Stateful monitor for coherence residual, floor, and autonomy decisions.
State is explicit and reconstructible; updates are pure replacements on
``floor_state`` (immutable snapshots). Deterministic for identical sequences.
"""
config: GoldTetherConfig = field(default_factory=GoldTetherConfig)
floor_state: PseudoscalarFloorState = field(init=False)
_step: int = field(default=0, init=False, repr=False)
def __post_init__(self) -> None:
self.floor_state = PseudoscalarFloorState(
value=float(self.config.floor_init),
sign=1.0,
n_samples=0,
last_update_step=0,
primal_floor=float(self.config.floor_init),
recent_residuals=(),
)
def measure(
self,
current: np.ndarray,
reference: np.ndarray,
*,
mode: OperatingMode | str = OperatingMode.PRACTICE,
) -> CoherenceResidual:
"""Compute harmonized coherence residual (pure; does not mutate floor)."""
_ = OperatingMode(mode) # validate
cur = _as_mv(current, "current")
ref = _as_mv(reference, "reference")
ps_c = _pseudoscalar(cur)
ps_r = _pseudoscalar(ref)
drift = abs(ps_c - ps_r)
# Also fold absolute pseudoscalar magnitude loss relative to reference.
drift = max(drift, abs(abs(ps_c) - abs(ps_r)))
geo = _geometric_distance(ref, cur)
geo_n = _normalize_distance(geo)
w = float(self.config.w_drift)
combined = w * drift + (1.0 - w) * geo_n
kappa = derive_kappa(combined, self.floor_state.value)
return CoherenceResidual(
drift=float(drift),
geometric_distance=float(geo),
combined=float(combined),
kappa=float(kappa),
pseudoscalar_current=ps_c,
pseudoscalar_reference=ps_r,
)
def update_floor(
self,
residual: CoherenceResidual | float,
*,
mode: OperatingMode | str = OperatingMode.PRACTICE,
success: bool = True,
pseudoscalar_sign: float | None = None,
) -> PseudoscalarFloorState:
"""Update dynamic floor from practice successes only.
Serve mode never promotes the floor. Failures only append telemetry
window; they do not raise the autonomy envelope.
"""
op_mode = OperatingMode(mode)
r = float(residual.combined if isinstance(residual, CoherenceResidual) else residual)
self._step += 1
recent = list(self.floor_state.recent_residuals) + [r]
decay_n = int(self.config.decay_N)
if len(recent) > decay_n:
recent = recent[-decay_n:]
new_value = self.floor_state.value
new_sign = self.floor_state.sign
n_samples = self.floor_state.n_samples
if op_mode is OperatingMode.PRACTICE and success and r < self.floor_state.value:
# Tighten floor toward observed residual while keeping primal anchor.
# Weighted mean of recent successes under decay window.
window = [x for x in recent if x < self.floor_state.value] or [r]
mean_r = float(sum(window) / len(window))
# Blend toward mean_r but never below half primal (safety).
floor_floor = 0.5 * self.floor_state.primal_floor
candidate = 0.5 * self.floor_state.value + 0.5 * mean_r
new_value = max(floor_floor, min(self.floor_state.value, candidate))
n_samples = n_samples + 1
if pseudoscalar_sign is not None and abs(pseudoscalar_sign) > _NEAR_ZERO:
new_sign = 1.0 if pseudoscalar_sign >= 0.0 else -1.0
self.floor_state = PseudoscalarFloorState(
value=float(new_value),
sign=float(new_sign),
n_samples=int(n_samples),
last_update_step=int(self._step),
primal_floor=float(self.floor_state.primal_floor),
recent_residuals=tuple(float(x) for x in recent),
)
return self.floor_state
def decide(
self,
residual: CoherenceResidual | float,
*,
mode: OperatingMode | str = OperatingMode.PRACTICE,
floor: PseudoscalarFloorState | None = None,
) -> AutonomyDecision:
"""Map residual + mode → autonomy band (HITL-safe defaults)."""
op_mode = OperatingMode(mode)
r = float(residual.combined if isinstance(residual, CoherenceResidual) else residual)
fl = floor if floor is not None else self.floor_state
floor_v = float(fl.value)
critical = floor_v * float(self.config.critical_ratio)
if r > critical:
return AutonomyDecision(
band=AutonomyBand.FAIL_CLOSED,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=0.0,
reason="residual_above_critical",
)
if op_mode is OperatingMode.SERVE:
# Serve never autonomous. Supervised blend only if explicitly authorized.
if r < floor_v and self.config.serve_supervised_blend_authorized:
alpha = float(1.0 - derive_kappa(r, floor_v))
return AutonomyDecision(
band=AutonomyBand.SUPERVISED_BLEND,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=alpha,
reason="serve_supervised_authorized",
)
if r < floor_v:
return AutonomyDecision(
band=AutonomyBand.FAIL_CLOSED,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=0.0,
reason="serve_hitl_default_fail_closed",
)
# floor <= r <= critical on serve: fail-closed unless blend authorized
if self.config.serve_supervised_blend_authorized:
alpha = float(min(1.0, (r - floor_v) / max(critical - floor_v, _NEAR_ZERO)))
return AutonomyDecision(
band=AutonomyBand.SUPERVISED_BLEND,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=alpha,
reason="serve_midband_supervised",
)
return AutonomyDecision(
band=AutonomyBand.FAIL_CLOSED,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=0.0,
reason="serve_midband_fail_closed",
)
# Practice path
if r < floor_v and self.config.practice_autonomy_enabled:
return AutonomyDecision(
band=AutonomyBand.AUTONOMOUS,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=1.0,
reason="practice_below_floor_autonomy_enabled",
)
if r < floor_v:
return AutonomyDecision(
band=AutonomyBand.SUPERVISED_BLEND,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=float(derive_kappa(r, floor_v)),
reason="practice_below_floor_supervised_default",
)
# floor <= r <= critical
alpha = float(1.0 - min(1.0, (r - floor_v) / max(critical - floor_v, _NEAR_ZERO)))
return AutonomyDecision(
band=AutonomyBand.SUPERVISED_BLEND,
residual=r,
floor=floor_v,
critical=critical,
mode=op_mode,
blend_alpha=max(0.0, min(1.0, alpha)),
reason="practice_midband_supervised",
)
def supervised_blend(
self,
source: np.ndarray,
target: np.ndarray,
alpha: float,
) -> np.ndarray:
"""Manifold slerp from source toward target by alpha ∈ [0, 1].
Dual-correction surface on the Spin group (not Euclidean lerp):
R = word_transition_rotor(source, target) # = target * reverse(source)
out = rotor_power(R, α) * source # left composition
At α=0 source; at α=1 target (unit versors). Sandwich conjugation
would map the identity to itself and is the wrong geodesic for state
interpolation. Output must satisfy versor_condition < 1e-6.
"""
a = float(alpha)
if a < 0.0 or a > 1.0:
raise ValueError("alpha must be in [0, 1]")
src = _as_mv(source, "source")
tgt = _as_mv(target, "target")
if a <= _NEAR_ZERO:
out = src.copy()
elif a >= 1.0 - _NEAR_ZERO:
out = tgt.copy()
else:
R = word_transition_rotor(src, tgt)
R_a = rotor_power(R, a)
out = geometric_product(R_a, src).astype(np.float64)
cond = versor_condition(out)
if cond >= _CLOSURE_TOL:
raise ValueError(
f"supervised_blend broke versor_condition: {cond:.3e} >= {_CLOSURE_TOL}"
)
return out.astype(np.float64, copy=False)
def telemetry(self) -> dict[str, Any]:
"""Schema-versioned pure projection for workbench channels."""
fl = self.floor_state
return {
"schema_version": _TELEMETRY_SCHEMA,
"pseudoscalar_floor": float(fl.value),
"pseudoscalar_sign": float(fl.sign),
"n_samples": int(fl.n_samples),
"last_update_step": int(fl.last_update_step),
"primal_floor": float(fl.primal_floor),
"recent_residuals": list(fl.recent_residuals),
"config": {
"decay_N": int(self.config.decay_N),
"w_drift": float(self.config.w_drift),
"floor_init": float(self.config.floor_init),
"critical_ratio": float(self.config.critical_ratio),
"practice_autonomy_enabled": bool(self.config.practice_autonomy_enabled),
"serve_supervised_blend_authorized": bool(
self.config.serve_supervised_blend_authorized
),
},
}
def with_config(monitor: GoldTetherMonitor, **updates: Any) -> GoldTetherMonitor:
"""Return a new monitor with updated config (immutability-friendly)."""
cfg = replace(monitor.config, **updates)
m = GoldTetherMonitor(config=cfg)
m.floor_state = monitor.floor_state
m._step = monitor._step
return m
def residual_from_mapping(payload: Mapping[str, Any]) -> float:
"""Deterministic residual extract for telemetry/replay fixtures."""
if "combined" in payload:
return float(payload["combined"])
raise KeyError("payload missing combined residual")

View file

@ -0,0 +1,190 @@
"""core.physics.self_authorship — Self-Authorship Miner scaffold (ADR-0240).
Geometry-guided ADR / teaching proposals under invariants. Emits
**proposal-only** artifacts (SPECULATIVE). Never writes vault COHERENT,
never mutates packs, never touches serving.
Complements the existing auto-proposal corridor (ADR-0151). This miner
produces structured proposal dicts; promotion remains human-reviewed.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from typing import Any, Mapping, Sequence
import numpy as np
from algebra.cl41 import N_COMPONENTS
from algebra.versor import versor_condition
from core.physics.dynamic_manifold import conformal_procrustes, signature_aware_pca
from core.physics.goldtether import CoherenceResidual, GoldTetherMonitor, OperatingMode
from core.physics.surprise import dual_operator, surprise_residual
@dataclass(frozen=True, slots=True)
class AuthorshipProposal:
"""Proposal-only artifact — never auto-accepted."""
proposal_id: str
kind: str
epistemic_status: str # always SPECULATIVE at emission
drift_residual: float
closure_proof: Mapping[str, Any]
body: Mapping[str, Any]
adr_refs: tuple[str, ...]
def as_dict(self) -> dict[str, Any]:
return {
"proposal_id": self.proposal_id,
"kind": self.kind,
"epistemic_status": self.epistemic_status,
"drift_residual": self.drift_residual,
"closure_proof": dict(self.closure_proof),
"body": dict(self.body),
"adr_refs": list(self.adr_refs),
}
def _content_id(payload: Mapping[str, Any]) -> str:
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
class SelfAuthorshipMiner:
"""Mine minimal extension proposals from geometric residual structure."""
def __init__(
self,
*,
goldtether: GoldTetherMonitor | None = None,
residual_threshold: float = 0.25,
) -> None:
self.goldtether = goldtether or GoldTetherMonitor()
self.residual_threshold = float(residual_threshold)
def mine_from_trajectory(
self,
current: np.ndarray,
reference: np.ndarray,
*,
basis: Sequence[np.ndarray] = (),
analogs: Sequence[tuple[str, np.ndarray, np.ndarray]] = (),
notes: str = "",
) -> tuple[AuthorshipProposal, ...]:
"""Emit zero or more SPECULATIVE proposals. Never stores them."""
cur = np.asarray(current, dtype=np.float64)
ref = np.asarray(reference, dtype=np.float64)
if cur.shape != (N_COMPONENTS,) or ref.shape != (N_COMPONENTS,):
raise ValueError("current and reference must be 32-component multivectors")
residual: CoherenceResidual = self.goldtether.measure(
cur, ref, mode=OperatingMode.PRACTICE
)
proposals: list[AuthorshipProposal] = []
# Closure proof for the reference/current pair under transition.
try:
proc = conformal_procrustes([ref], [cur])
closure_ok = versor_condition(proc.versor) < 1e-6
proc_res = float(proc.residual_norm)
except ValueError as exc:
closure_ok = False
proc_res = float("inf")
proc_err = str(exc)
else:
proc_err = ""
closure_proof = {
"versor_condition_current": float(versor_condition(cur)),
"versor_condition_reference": float(versor_condition(ref)),
"procrustes_residual": proc_res,
"procrustes_closed": closure_ok,
"procrustes_error": proc_err,
"coherence_combined": float(residual.combined),
"kappa": float(residual.kappa),
}
if residual.combined >= self.residual_threshold and closure_ok:
body = {
"notes": notes,
"suggested_action": "review_coherence_gap",
"drift": float(residual.drift),
"geometric_distance": float(residual.geometric_distance),
}
pid = _content_id({"kind": "coherence_gap", "body": body, "proof": closure_proof})
proposals.append(
AuthorshipProposal(
proposal_id=f"selfauth-{pid}",
kind="coherence_gap",
epistemic_status="SPECULATIVE",
drift_residual=float(residual.combined),
closure_proof=closure_proof,
body=body,
adr_refs=("ADR-0238", "ADR-0240"),
)
)
if basis:
surp = surprise_residual(cur, basis)
dual = dual_operator(
cur,
basis,
analogs,
kappa=max(residual.kappa, 1e-6),
)
if dual.productive:
body = {
"notes": notes,
"suggested_action": "review_analogical_extension",
"surprise_norm": float(surp.residual_norm),
"selected_analog_id": dual.selected_analog_id,
"procrustes_residual": (
float(dual.procrustes.residual_norm) if dual.procrustes else None
),
}
pid = _content_id(
{"kind": "analogical_extension", "body": body, "proof": closure_proof}
)
proposals.append(
AuthorshipProposal(
proposal_id=f"selfauth-{pid}",
kind="analogical_extension",
epistemic_status="SPECULATIVE",
drift_residual=float(surp.residual_norm),
closure_proof=closure_proof,
body=body,
adr_refs=("ADR-0239", "ADR-0240"),
)
)
# Optional manifold annotation when a small cloud is available via analogs.
cloud = [ref, cur] + [s for _, s, _ in analogs] + [t for _, _, t in analogs]
if len(cloud) >= 2:
pca = signature_aware_pca(cloud, max_axes=4)
if pca.n_null > 0:
body = {
"notes": notes,
"suggested_action": "review_null_axes",
"n_null": int(pca.n_null),
"n_spacelike": int(pca.n_spacelike),
"n_timelike": int(pca.n_timelike),
}
pid = _content_id({"kind": "null_axis_review", "body": body})
proposals.append(
AuthorshipProposal(
proposal_id=f"selfauth-{pid}",
kind="null_axis_review",
epistemic_status="SPECULATIVE",
drift_residual=float(residual.combined),
closure_proof=closure_proof,
body=body,
adr_refs=("ADR-0239", "ADR-0240"),
)
)
# Stable order by proposal_id for replay-determinism.
proposals.sort(key=lambda p: p.proposal_id)
return tuple(proposals)

212
core/physics/surprise.py Normal file
View file

@ -0,0 +1,212 @@
"""core.physics.surprise — Surprise Residual + dual with Procrustes (ADR-0239).
Surprise Residual: S(x) = x proj_B(x)
where B is a known basis (ordered multivector span). High surprise seeds
Conformal Procrustes against vault analogs productive novelty only when the
post-transfer residual is below threshold; otherwise typed refuse.
No sampling. No statistical ranking. Deterministic ordered analog lists.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Sequence
import numpy as np
from algebra.cl41 import N_COMPONENTS
from algebra.versor import versor_condition
from core.physics.dynamic_manifold import (
ConformalProcrustesResult,
conformal_procrustes,
procrustes_residual,
)
_NEAR_ZERO = 1e-12
_CLOSURE_TOL = 1e-6
_DEFAULT_PRODUCTIVE_THRESHOLD = 0.35
@dataclass(frozen=True, slots=True)
class SurpriseResult:
residual_mv: np.ndarray
residual_norm: float
projection: np.ndarray
basis_rank: int
@dataclass(frozen=True, slots=True)
class AnalogySeed:
analog_id: str
source: np.ndarray
target: np.ndarray
surprise_affinity: float
@dataclass(frozen=True, slots=True)
class DualOperatorResult:
surprise: SurpriseResult
procrustes: ConformalProcrustesResult | None
productive: bool
kappa: float
reason: str
selected_analog_id: str | None
def _as_mv(v: np.ndarray, name: str) -> np.ndarray:
arr = np.asarray(v, dtype=np.float64)
if arr.shape != (N_COMPONENTS,):
raise ValueError(f"{name} must have shape ({N_COMPONENTS},); got {arr.shape}")
return arr
def _orthonormalize_basis(basis: Sequence[np.ndarray]) -> np.ndarray:
"""Deterministic GramSchmidt on coefficient space (ordered input).
Returns matrix B with shape (rank, 32). Zero / dependent vectors dropped
in order not silently as "null PCA axes"; rank is reported.
"""
cols: list[np.ndarray] = []
for i, b in enumerate(basis):
v = _as_mv(b, f"basis[{i}]").copy()
for u in cols:
v = v - float(np.dot(v, u)) * u
n = float(np.linalg.norm(v))
if n < _NEAR_ZERO:
continue
cols.append(v / n)
if not cols:
return np.zeros((0, N_COMPONENTS), dtype=np.float64)
return np.stack(cols, axis=0)
def project_onto_basis(x: np.ndarray, basis: Sequence[np.ndarray]) -> np.ndarray:
"""Orthogonal projection of x onto span(B) in coefficient space."""
x_arr = _as_mv(x, "x")
B = _orthonormalize_basis(basis)
if B.shape[0] == 0:
return np.zeros(N_COMPONENTS, dtype=np.float64)
# proj = sum_i <x, u_i> u_i
coeffs = B @ x_arr
return (B.T @ coeffs).astype(np.float64, copy=False)
def surprise_residual(
x: np.ndarray,
basis: Sequence[np.ndarray],
) -> SurpriseResult:
"""S(x) = x proj_B(x). Residual is orthogonal to span(B)."""
x_arr = _as_mv(x, "x")
proj = project_onto_basis(x_arr, basis)
residual = (x_arr - proj).astype(np.float64, copy=False)
B = _orthonormalize_basis(basis)
return SurpriseResult(
residual_mv=residual,
residual_norm=float(np.linalg.norm(residual)),
projection=proj,
basis_rank=int(B.shape[0]),
)
def analogy_seed(
surprise: SurpriseResult,
analogs: Sequence[tuple[str, np.ndarray, np.ndarray]],
*,
top_k: int | None = None,
) -> tuple[AnalogySeed, ...]:
"""Rank vault analogs by affinity to the surprise residual (deterministic).
Affinity = |cos| between residual and (target source) direction in
coefficient space. Higher affinity better structural candidate.
Order is stable: affinity desc, then analog_id asc.
"""
if surprise.residual_norm < _NEAR_ZERO:
return ()
r = surprise.residual_mv
r_n = surprise.residual_norm
seeds: list[AnalogySeed] = []
for item in analogs:
if len(item) != 3:
raise ValueError("each analog must be (id, source, target)")
aid, src, tgt = item
s = _as_mv(src, f"analog[{aid}].source")
t = _as_mv(tgt, f"analog[{aid}].target")
delta = t - s
dn = float(np.linalg.norm(delta))
if dn < _NEAR_ZERO:
aff = 0.0
else:
aff = abs(float(np.dot(r, delta)) / (r_n * dn))
seeds.append(
AnalogySeed(
analog_id=str(aid),
source=s,
target=t,
surprise_affinity=float(aff),
)
)
seeds.sort(key=lambda s: (-s.surprise_affinity, s.analog_id))
if top_k is not None:
seeds = seeds[: max(0, int(top_k))]
return tuple(seeds)
def dual_operator(
x: np.ndarray,
basis: Sequence[np.ndarray],
analogs: Sequence[tuple[str, np.ndarray, np.ndarray]],
*,
kappa: float = 1.0,
productive_threshold: float = _DEFAULT_PRODUCTIVE_THRESHOLD,
min_surprise: float = 1e-6,
) -> DualOperatorResult:
"""Surprise + Procrustes dual.
High surprise seeds Procrustes against the best analog. Productive only when
post-transfer residual productive_threshold * (1/κ-scaled). κ from
CoherenceGoldTether scales the threshold (higher κ stricter).
"""
if kappa <= 0.0:
raise ValueError("kappa must be positive")
surprise = surprise_residual(x, basis)
if surprise.residual_norm < min_surprise:
return DualOperatorResult(
surprise=surprise,
procrustes=None,
productive=False,
kappa=float(kappa),
reason="surprise_below_minimum",
selected_analog_id=None,
)
seeds = analogy_seed(surprise, analogs, top_k=1)
if not seeds:
return DualOperatorResult(
surprise=surprise,
procrustes=None,
productive=False,
kappa=float(kappa),
reason="no_analogs",
selected_analog_id=None,
)
best = seeds[0]
# Transfer: Procrustes from analog source→target applied as structural map;
# measure residual of mapping x's projection-completion toward analog target shape.
proc = conformal_procrustes([best.source], [best.target])
# Residual of applying the analog's versor to x vs. the analog target direction.
transfer_res = procrustes_residual(x, best.target, proc.versor)
# Also include native procrustes residual of the analog pair itself.
residual = max(float(proc.residual_norm), float(transfer_res))
threshold = float(productive_threshold) / float(kappa)
productive = residual <= threshold and versor_condition(proc.versor) < _CLOSURE_TOL
return DualOperatorResult(
surprise=surprise,
procrustes=proc,
productive=bool(productive),
kappa=float(kappa),
reason="productive_novelty" if productive else "residual_above_threshold",
selected_analog_id=best.analog_id,
)

View file

@ -0,0 +1,130 @@
"""core.physics.temporal_gate — Temporal Admissibility Gate (ADR-0240).
Wisdom as geometry: refuse premature but eventually-admissible claims with a
typed NOT_YET. Never confabulates early. Pure predicate no side effects.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Any, Mapping
class TemporalVerdict(str, Enum):
ADMIT = "admit"
NOT_YET = "not_yet"
REFUSE = "refuse"
@dataclass(frozen=True, slots=True)
class TemporalContext:
"""Geometric/temporal preconditions for a claim.
All fields are explicit; missing evidence is not inventable.
"""
step: int
min_step: int = 0
required_evidence_count: int = 0
evidence_count: int = 0
coherence_residual: float = 0.0
residual_ceiling: float = 1.0
prerequisites_met: bool = True
claim_id: str = ""
@dataclass(frozen=True, slots=True)
class TemporalDecision:
verdict: TemporalVerdict
reason: str
claim_id: str
disclosure: Mapping[str, Any]
class TemporalAdmissibilityGate:
"""Pure temporal admissibility checks."""
def __init__(
self,
*,
require_prerequisites: bool = True,
enforce_residual_ceiling: bool = True,
) -> None:
self.require_prerequisites = bool(require_prerequisites)
self.enforce_residual_ceiling = bool(enforce_residual_ceiling)
def evaluate(self, ctx: TemporalContext) -> TemporalDecision:
cid = str(ctx.claim_id)
if ctx.step < 0 or ctx.min_step < 0:
return TemporalDecision(
verdict=TemporalVerdict.REFUSE,
reason="negative_time_index",
claim_id=cid,
disclosure={
"type": "temporal_refuse",
"detail": "time indices must be non-negative",
},
)
if self.require_prerequisites and not ctx.prerequisites_met:
return TemporalDecision(
verdict=TemporalVerdict.REFUSE,
reason="prerequisites_unmet",
claim_id=cid,
disclosure={
"type": "temporal_refuse",
"detail": "required prerequisites are not met",
},
)
if self.enforce_residual_ceiling and ctx.coherence_residual > ctx.residual_ceiling:
return TemporalDecision(
verdict=TemporalVerdict.REFUSE,
reason="residual_above_ceiling",
claim_id=cid,
disclosure={
"type": "temporal_refuse",
"detail": "coherence residual exceeds ceiling",
"residual": float(ctx.coherence_residual),
"ceiling": float(ctx.residual_ceiling),
},
)
if ctx.step < ctx.min_step:
return TemporalDecision(
verdict=TemporalVerdict.NOT_YET,
reason="before_min_step",
claim_id=cid,
disclosure={
"type": "temporal_not_yet",
"detail": "fullness of time not reached",
"step": int(ctx.step),
"min_step": int(ctx.min_step),
},
)
if ctx.evidence_count < ctx.required_evidence_count:
return TemporalDecision(
verdict=TemporalVerdict.NOT_YET,
reason="insufficient_evidence",
claim_id=cid,
disclosure={
"type": "temporal_not_yet",
"detail": "evidence count below required floor",
"evidence_count": int(ctx.evidence_count),
"required_evidence_count": int(ctx.required_evidence_count),
},
)
return TemporalDecision(
verdict=TemporalVerdict.ADMIT,
reason="temporally_admissible",
claim_id=cid,
disclosure={
"type": "temporal_admit",
"step": int(ctx.step),
"evidence_count": int(ctx.evidence_count),
},
)