feat(third-door): wire high surprise to DiscoveryCandidate (#20 follow-up)
- Physics dual operators set discovery_eligible (γ=0.35); is_discovery_eligible pure predicate; no teaching/vault imports in core.physics.surprise. - teaching.discovery: trigger high_surprise; candidate_from_surprise_dual + emit_surprise_discovery (opt-in sink, proposal-only unreviewed, domain=math). - Boundary tests: threshold gates, determinism, no VaultStore, no teaching import in physics. Ledger §6 notes wiring surface (issue #30). Does not self-install; contemplation consumes via existing DiscoveryCandidateSink.
This commit is contained in:
parent
25f8c14e27
commit
995f4fc3b8
6 changed files with 438 additions and 12 deletions
|
|
@ -44,7 +44,13 @@ from core.physics.dynamic_manifold import (
|
|||
signature_aware_pca,
|
||||
signature_aware_pca_report,
|
||||
)
|
||||
from core.physics.surprise import dual_operator, dual_procrustes_surprise, surprise_residual
|
||||
from core.physics.surprise import (
|
||||
DEFAULT_DISCOVERY_GAMMA,
|
||||
dual_operator,
|
||||
dual_procrustes_surprise,
|
||||
is_discovery_eligible,
|
||||
surprise_residual,
|
||||
)
|
||||
from core.physics.biography import (
|
||||
BiographyHolonomyBlade,
|
||||
biography_telemetry,
|
||||
|
|
@ -79,7 +85,9 @@ __all__ = [
|
|||
"cartan_iwasawa_extract", "cartan_iwasawa_factorize",
|
||||
"conformal_procrustes", "dual_correction_slerp", "procrustes_residual",
|
||||
"signature_aware_pca", "signature_aware_pca_report",
|
||||
"dual_operator", "dual_procrustes_surprise", "surprise_residual",
|
||||
"DEFAULT_DISCOVERY_GAMMA",
|
||||
"dual_operator", "dual_procrustes_surprise", "is_discovery_eligible",
|
||||
"surprise_residual",
|
||||
"BiographyHolonomyBlade", "biography_telemetry",
|
||||
"integrate_biography", "reconstruct_biography",
|
||||
"TemporalAdmissibilityGate", "TemporalContext",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,14 @@ _NEAR_ZERO = 1e-12
|
|||
_CLOSURE_TOL = 1e-6
|
||||
_GRADE_TOL = 1e-9
|
||||
_MAX_GRADE = 5
|
||||
# Blueprint Super §3.2 discovery gate: high surprise (above γ) is a discovery
|
||||
# signal, not a productive transfer. Shared default γ for dual_operator and the
|
||||
# discovery_eligible flag on dual_procrustes_surprise. Teaching-layer factories
|
||||
# may override; physics never imports teaching.
|
||||
DEFAULT_DISCOVERY_GAMMA = 0.35
|
||||
# Productive-transfer surprise ceiling on the dual_procrustes_surprise path
|
||||
# (stricter than discovery γ — inside the admissible span for safe transport).
|
||||
_TRANSFER_SURPRISE_TOL = 1e-4
|
||||
|
||||
|
||||
class SurpriseResidualError(ValueError):
|
||||
|
|
@ -196,18 +204,45 @@ def surprise_residual(
|
|||
raise ValueError("surprise_residual expects 5-vector or 32-vector x")
|
||||
|
||||
|
||||
def is_discovery_eligible(
|
||||
*,
|
||||
surprise_norm: float,
|
||||
productive_or_transfer: bool,
|
||||
surprise_refused: Optional[str] = None,
|
||||
discovery_gamma: float = DEFAULT_DISCOVERY_GAMMA,
|
||||
) -> bool:
|
||||
"""True iff high surprise is a discovery signal (Super §3.2).
|
||||
|
||||
Discovery requires a measured residual above γ, no metric refusal, and
|
||||
**not** a productive transfer (transfer and discovery are dual, not the
|
||||
same gate). Pure predicate — no I/O, no teaching import.
|
||||
"""
|
||||
if surprise_refused is not None:
|
||||
return False
|
||||
if productive_or_transfer:
|
||||
return False
|
||||
sn = float(surprise_norm)
|
||||
if not np.isfinite(sn):
|
||||
return False
|
||||
return sn > float(discovery_gamma)
|
||||
|
||||
|
||||
def dual_procrustes_surprise(
|
||||
P: np.ndarray,
|
||||
Q: np.ndarray,
|
||||
current_basis: np.ndarray,
|
||||
*,
|
||||
discovery_gamma: float = DEFAULT_DISCOVERY_GAMMA,
|
||||
) -> dict:
|
||||
"""The dual operator: run Procrustes and Surprise together.
|
||||
|
||||
``transfer_accepted`` is a PRODUCTIVE-TRANSFER gate: a structural match was
|
||||
found (low Procrustes residual) AND the probe is inside the admissible span
|
||||
(low surprise) — i.e. it is safe to transport ``P``'s solution to ``Q``. High
|
||||
surprise is NOT a transfer; it is a *discovery* signal (wired separately). A
|
||||
fail-closed surprise refusal (degenerate basis) is recorded, not raised.
|
||||
surprise is NOT a transfer; it is a *discovery* signal
|
||||
(``discovery_eligible``). A fail-closed surprise refusal (degenerate basis)
|
||||
is recorded, not raised. Physics never constructs teaching objects — callers
|
||||
pass this dict to ``teaching.discovery.candidate_from_surprise_dual``.
|
||||
"""
|
||||
V, proc_residual = conformal_procrustes(P, Q)
|
||||
Q_arr = np.asarray(Q, dtype=np.float64)
|
||||
|
|
@ -232,16 +267,27 @@ def dual_procrustes_surprise(
|
|||
if np.asarray(V).shape == (N_COMPONENTS,):
|
||||
closed = versor_condition(V) < _CLOSURE_TOL
|
||||
|
||||
transfer_accepted = bool(
|
||||
refused is None
|
||||
and proc_residual < 1e-5
|
||||
and sur_norm < _TRANSFER_SURPRISE_TOL
|
||||
and closed
|
||||
)
|
||||
return {
|
||||
"versor": V,
|
||||
"procrustes_residual": float(proc_residual),
|
||||
"surprise_vector": sur_vec,
|
||||
"surprise_norm": float(sur_norm),
|
||||
"transfer_accepted": bool(
|
||||
refused is None and proc_residual < 1e-5 and sur_norm < 1e-4 and closed
|
||||
),
|
||||
"transfer_accepted": transfer_accepted,
|
||||
"versor_closed": bool(closed),
|
||||
"surprise_refused": refused,
|
||||
"discovery_eligible": is_discovery_eligible(
|
||||
surprise_norm=float(sur_norm),
|
||||
productive_or_transfer=transfer_accepted,
|
||||
surprise_refused=refused,
|
||||
discovery_gamma=discovery_gamma,
|
||||
),
|
||||
"discovery_gamma": float(discovery_gamma),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -255,6 +301,7 @@ def dual_operator(
|
|||
kappa: float = 1.0,
|
||||
productive_threshold: float = 0.35,
|
||||
surprise_threshold: float = 0.35,
|
||||
discovery_gamma: float | None = None,
|
||||
) -> dict:
|
||||
"""Extended dual for multivector analogy seeds (ADR-0240 harness).
|
||||
|
||||
|
|
@ -262,7 +309,9 @@ def dual_operator(
|
|||
a structural match (``proc_r <= thr``) AND the query being inside the
|
||||
admissible span (``sur_norm <= surprise_threshold``). The previous
|
||||
``sur_norm >= 0.0`` conjunct was always true, so surprise never gated. A HIGH
|
||||
``sur_norm`` marks a discovery candidate, not a productive transfer.
|
||||
``sur_norm`` marks discovery (``discovery_eligible``), not a productive
|
||||
transfer. Teaching constructs candidates from this flag; this module never
|
||||
imports teaching.
|
||||
"""
|
||||
if isinstance(basis, np.ndarray):
|
||||
B = basis
|
||||
|
|
@ -276,7 +325,10 @@ def dual_operator(
|
|||
except SurpriseResidualError as exc:
|
||||
sur_vec, sur_norm, surprise_refused = None, float("inf"), exc.reason
|
||||
|
||||
gamma = float(surprise_threshold if discovery_gamma is None else discovery_gamma)
|
||||
|
||||
if not analogs:
|
||||
productive = False
|
||||
return {
|
||||
"surprise_norm": float(sur_norm),
|
||||
"procrustes_residual": float("inf"),
|
||||
|
|
@ -285,6 +337,13 @@ def dual_operator(
|
|||
"selected_analog_id": None,
|
||||
"versor": None,
|
||||
"surprise_refused": surprise_refused,
|
||||
"discovery_eligible": is_discovery_eligible(
|
||||
surprise_norm=float(sur_norm),
|
||||
productive_or_transfer=productive,
|
||||
surprise_refused=surprise_refused,
|
||||
discovery_gamma=gamma,
|
||||
),
|
||||
"discovery_gamma": gamma,
|
||||
}
|
||||
aid, src, tgt = analogs[0]
|
||||
V, proc_r = conformal_procrustes(src, tgt)
|
||||
|
|
@ -303,4 +362,11 @@ def dual_operator(
|
|||
"versor": V,
|
||||
"surprise_vector": sur_vec,
|
||||
"surprise_refused": surprise_refused,
|
||||
"discovery_eligible": is_discovery_eligible(
|
||||
surprise_norm=float(sur_norm),
|
||||
productive_or_transfer=bool(productive),
|
||||
surprise_refused=surprise_refused,
|
||||
discovery_gamma=gamma,
|
||||
),
|
||||
"discovery_gamma": gamma,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
| 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+α; bootstrap/prune deferred) | #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 | 🟢 operator math fixed (metric proj + polarity); wiring split | #20 |
|
||||
| 6 | Surprise residual operator | Super §3.2 | 🟢 math fixed; DiscoveryCandidate wiring on branch (issue #30) | #20 |
|
||||
| 7 | Trajectory invariants + zero-fabrication | R&D §2.2 | ⚫ absent | #21 |
|
||||
| 8 | ADR-DAG conformal embedding | R&D §2.4 | ⚫ absent | #21 |
|
||||
| — | Biography holonomy | (ADR-0240; not in blueprints) | 🟢 sound | — |
|
||||
|
|
@ -159,8 +159,11 @@ This diagnostic is what killed §3.3, and it should be applied to every remainin
|
|||
- **Reconciled productivity polarity.** `productive` (and `transfer_accepted`) now both mean **productive TRANSFER = low Procrustes ∧ low surprise** (a structural match found AND the query inside the admissible span). This **corrects** §6's earlier "high surprise ∧ low procrustes = productive" phrasing, which conflated *transfer* with *discovery*: HIGH surprise is a **discovery** signal, not a productive transfer, and routes to the (split-out) `DiscoveryCandidate` path.
|
||||
- Tests: `tests/test_adr_0239_surprise_metric_projection.py` — metric-vs-Euclidean divergence (the load-bearing proof), null refusal, null-pair admission, redundant-basis admission, in/out-of-span, grade purity, polarity, determinism.
|
||||
|
||||
### Remaining (follow-up — its own PR)
|
||||
Raise a `DiscoveryCandidate` (`teaching/discovery.py`) on high surprise into the contemplation loop, behind the existing proposal-only / no-self-install discipline, with no-self-install boundary tests.
|
||||
### Remaining → wiring landed (issue #30 / this surface)
|
||||
High surprise now routes to a proposal-only `DiscoveryCandidate`:
|
||||
- Physics (`dual_operator` / `dual_procrustes_surprise`) sets `discovery_eligible` when `sur_norm > γ` and transfer is not productive (`is_discovery_eligible`; default `γ = 0.35`). **No teaching import** in `core.physics.surprise`.
|
||||
- Teaching factory: `candidate_from_surprise_dual` / `emit_surprise_discovery` produce `trigger=high_surprise`, `review_state=unreviewed`, `domain=math`. Opt-in `DiscoveryCandidateSink` emit (same stream contemplation already consumes). **Never** vault/store/self-install.
|
||||
- Tests: `tests/test_third_door_surprise_discovery_wiring.py`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -238,7 +241,7 @@ PY
|
|||
| Kabsch-conformal Procrustes on point sets | #17 |
|
||||
| GoldTether gold-set + harmonized residual + α=Φ(R) | #18 |
|
||||
| Grade-5 pseudoscalar preservation gate — ⚪ RETIRED (vacuous; see §5) | #19 (closed) |
|
||||
| Surprise: metric projection + productivity polarity — 🟢 done (#20); DiscoveryCandidate wiring split to follow-up | #20 |
|
||||
| Surprise: metric projection + productivity polarity — 🟢 done (#20); DiscoveryCandidate wiring — issue #30 / branch `feat/third-door-surprise-discovery-wiring` | #20 |
|
||||
| Absent proposals: sensorimotor + ADR-DAG | #21 |
|
||||
|
||||
Closing a gap = flip its `xfail` in `tests/test_third_door_blueprint_fidelity.py` to a passing behavioral test and delete the matching characterization lock. That is the definition of "done right" here.
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
|
||||
|
|
@ -65,6 +66,9 @@ DiscoveryTrigger = Literal[
|
|||
"successful_comparison",
|
||||
"hedge_acknowledged",
|
||||
"oov_resolved_via_decomp",
|
||||
# Third-Door Super §3.2 / fidelity #20: high geometric surprise on the
|
||||
# dual Procrustes–Surprise operator (epistemic frontier signal).
|
||||
"high_surprise",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -362,12 +366,142 @@ def format_candidate_jsonl(candidate: DiscoveryCandidate) -> str:
|
|||
return json.dumps(candidate.as_dict(), sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def candidate_from_surprise_dual(
|
||||
dual: dict[str, Any],
|
||||
*,
|
||||
source_turn_trace: str = "",
|
||||
discovery_gamma: float | None = None,
|
||||
) -> DiscoveryCandidate | None:
|
||||
"""Build a proposal-only ``DiscoveryCandidate`` from a dual-operator audit dict.
|
||||
|
||||
Pure function of its inputs (plus the dual dict). Fires only when the dual
|
||||
result is discovery-eligible: measured surprise above γ, not a productive
|
||||
transfer, and no metric refusal. Never writes vault / packs / corpus.
|
||||
|
||||
``dual`` is the audit dict from
|
||||
:func:`core.physics.surprise.dual_operator` or
|
||||
:func:`core.physics.surprise.dual_procrustes_surprise` (must carry
|
||||
``surprise_norm``; preferably also ``discovery_eligible``).
|
||||
|
||||
Returns ``None`` when the signal is not a high-surprise discovery.
|
||||
"""
|
||||
from core.physics.surprise import (
|
||||
DEFAULT_DISCOVERY_GAMMA,
|
||||
is_discovery_eligible,
|
||||
)
|
||||
|
||||
if not isinstance(dual, dict):
|
||||
return None
|
||||
|
||||
gamma = float(
|
||||
discovery_gamma
|
||||
if discovery_gamma is not None
|
||||
else dual.get("discovery_gamma", DEFAULT_DISCOVERY_GAMMA)
|
||||
)
|
||||
sur_norm = float(dual.get("surprise_norm", float("nan")))
|
||||
refused = dual.get("surprise_refused")
|
||||
productive = bool(
|
||||
dual.get("productive", False) or dual.get("transfer_accepted", False)
|
||||
)
|
||||
|
||||
# Prefer the physics flag when present; recompute otherwise so older dual
|
||||
# dicts without the field still route correctly.
|
||||
if "discovery_eligible" in dual:
|
||||
eligible = bool(dual["discovery_eligible"])
|
||||
else:
|
||||
eligible = is_discovery_eligible(
|
||||
surprise_norm=sur_norm,
|
||||
productive_or_transfer=productive,
|
||||
surprise_refused=refused if isinstance(refused, str) else None,
|
||||
discovery_gamma=gamma,
|
||||
)
|
||||
if not eligible:
|
||||
return None
|
||||
|
||||
proc_r = dual.get("procrustes_residual")
|
||||
try:
|
||||
proc_f = float(proc_r) if proc_r is not None and math.isfinite(float(proc_r)) else None
|
||||
except (TypeError, ValueError):
|
||||
proc_f = None
|
||||
analog_id = dual.get("selected_analog_id")
|
||||
|
||||
# Partial proposed chain — geometric frontier evidence, not a ready teaching
|
||||
# chain. Review / Phase C remains the only path to corpus extension.
|
||||
proposed_chain: dict[str, Any] = {
|
||||
"subject": "geometric_frontier",
|
||||
"intent": "discovery",
|
||||
"connective": None,
|
||||
"object": None,
|
||||
"kind": "high_surprise",
|
||||
"surprise_norm": sur_norm,
|
||||
"discovery_gamma": gamma,
|
||||
}
|
||||
if proc_f is not None:
|
||||
proposed_chain["procrustes_residual"] = proc_f
|
||||
if analog_id is not None:
|
||||
proposed_chain["selected_analog_id"] = str(analog_id)
|
||||
|
||||
trace = str(source_turn_trace or "")
|
||||
trigger: DiscoveryTrigger = "high_surprise"
|
||||
hash_payload = {
|
||||
"proposed_chain": proposed_chain,
|
||||
"trigger": trigger,
|
||||
"source_turn_trace": trace,
|
||||
}
|
||||
candidate_id = _hash_candidate_id(hash_payload)
|
||||
return DiscoveryCandidate(
|
||||
candidate_id=candidate_id,
|
||||
proposed_chain=proposed_chain,
|
||||
trigger=trigger,
|
||||
source_turn_trace=trace,
|
||||
pack_consistent=True, # geometric signal; pack residency N/A
|
||||
boundary_clean=True,
|
||||
review_state="unreviewed",
|
||||
domain="math",
|
||||
)
|
||||
|
||||
|
||||
def emit_surprise_discovery(
|
||||
dual: dict[str, Any],
|
||||
sink: Any | None = None,
|
||||
*,
|
||||
source_turn_trace: str = "",
|
||||
discovery_gamma: float | None = None,
|
||||
) -> DiscoveryCandidate | None:
|
||||
"""Opt-in sink emission of a high-surprise ``DiscoveryCandidate``.
|
||||
|
||||
Pure construction via :func:`candidate_from_surprise_dual`. When ``sink`` is
|
||||
provided and a candidate is produced, emits one JSONL line through the
|
||||
existing :class:`~teaching.discovery_sink.DiscoveryCandidateSink` protocol
|
||||
(same stream contemplation already consumes). Without a sink this is a pure
|
||||
factory. Never mutates vault, packs, or the teaching corpus.
|
||||
"""
|
||||
candidate = candidate_from_surprise_dual(
|
||||
dual,
|
||||
source_turn_trace=source_turn_trace,
|
||||
discovery_gamma=discovery_gamma,
|
||||
)
|
||||
if candidate is None:
|
||||
return None
|
||||
if sink is not None:
|
||||
emit = getattr(sink, "emit", None)
|
||||
if emit is None or not callable(emit):
|
||||
raise TypeError(
|
||||
"emit_surprise_discovery: sink must provide emit(line: str) "
|
||||
"(DiscoveryCandidateSink protocol)"
|
||||
)
|
||||
emit(format_candidate_jsonl(candidate))
|
||||
return candidate
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ClaimDomain",
|
||||
"DiscoveryCandidate",
|
||||
"DiscoveryTrigger",
|
||||
"EvidencePointer",
|
||||
"SubQuestion",
|
||||
"candidate_from_surprise_dual",
|
||||
"emit_surprise_discovery",
|
||||
"extract_discovery_candidates",
|
||||
"format_candidate_jsonl",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -54,7 +54,9 @@ def test_dual_procrustes_surprise_audit_dict():
|
|||
assert "procrustes_residual" in out
|
||||
assert "surprise_norm" in out
|
||||
assert "transfer_accepted" in out
|
||||
assert "discovery_eligible" in out
|
||||
assert isinstance(out["transfer_accepted"], bool)
|
||||
assert isinstance(out["discovery_eligible"], bool)
|
||||
if np.asarray(out["versor"]).shape == (32,):
|
||||
assert versor_condition(out["versor"]) < 1e-6
|
||||
|
||||
|
|
|
|||
213
tests/test_third_door_surprise_discovery_wiring.py
Normal file
213
tests/test_third_door_surprise_discovery_wiring.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
"""Third-Door #20 follow-up: high surprise → DiscoveryCandidate wiring.
|
||||
|
||||
Ledger §6 remaining surface (issue #30):
|
||||
- physics marks discovery_eligible (no teaching import)
|
||||
- teaching builds proposal-only DiscoveryCandidate (trigger=high_surprise)
|
||||
- opt-in sink emit; never VaultStore / self-install
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from algebra.cl41 import N_COMPONENTS
|
||||
from algebra.rotor import make_rotor_from_angle
|
||||
from core.physics import surprise as surprise_mod
|
||||
from core.physics.surprise import (
|
||||
DEFAULT_DISCOVERY_GAMMA,
|
||||
dual_operator,
|
||||
dual_procrustes_surprise,
|
||||
is_discovery_eligible,
|
||||
surprise_residual,
|
||||
)
|
||||
from teaching.discovery import (
|
||||
candidate_from_surprise_dual,
|
||||
emit_surprise_discovery,
|
||||
)
|
||||
from teaching.discovery_sink import DiscoveryBufferSink
|
||||
|
||||
|
||||
def _id32() -> np.ndarray:
|
||||
v = np.zeros(N_COMPONENTS, dtype=np.float64)
|
||||
v[0] = 1.0
|
||||
return v
|
||||
|
||||
|
||||
def _basis_identity() -> np.ndarray:
|
||||
"""Single-column basis = identity versor (admits only near-identity probes)."""
|
||||
return _id32().reshape(N_COMPONENTS, 1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Physics layer — discovery_eligible flag
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_is_discovery_eligible_predicate() -> None:
|
||||
assert is_discovery_eligible(
|
||||
surprise_norm=0.5, productive_or_transfer=False, discovery_gamma=0.35
|
||||
)
|
||||
assert not is_discovery_eligible(
|
||||
surprise_norm=0.1, productive_or_transfer=False, discovery_gamma=0.35
|
||||
)
|
||||
assert not is_discovery_eligible(
|
||||
surprise_norm=0.9, productive_or_transfer=True, discovery_gamma=0.35
|
||||
)
|
||||
assert not is_discovery_eligible(
|
||||
surprise_norm=0.9,
|
||||
productive_or_transfer=False,
|
||||
surprise_refused="degenerate_metric_span",
|
||||
discovery_gamma=0.35,
|
||||
)
|
||||
assert not is_discovery_eligible(
|
||||
surprise_norm=float("inf"), productive_or_transfer=False
|
||||
)
|
||||
|
||||
|
||||
def test_dual_procrustes_marks_discovery_on_out_of_span_probe() -> None:
|
||||
"""Non-identity probe against identity-only basis → high surprise → discovery."""
|
||||
basis = _basis_identity()
|
||||
src = _id32()
|
||||
tgt = make_rotor_from_angle(0.9, bivector_idx=6)
|
||||
dual = dual_procrustes_surprise(src, tgt, basis)
|
||||
assert dual["surprise_refused"] is None
|
||||
assert dual["surprise_norm"] > DEFAULT_DISCOVERY_GAMMA
|
||||
assert dual["transfer_accepted"] is False
|
||||
assert dual["discovery_eligible"] is True
|
||||
assert dual["discovery_gamma"] == DEFAULT_DISCOVERY_GAMMA
|
||||
|
||||
|
||||
def test_dual_procrustes_no_discovery_on_identity_transfer() -> None:
|
||||
basis = _basis_identity()
|
||||
src = _id32()
|
||||
dual = dual_procrustes_surprise(src, src, basis)
|
||||
assert dual["surprise_norm"] < 1e-9
|
||||
assert dual["discovery_eligible"] is False
|
||||
|
||||
|
||||
def test_dual_operator_discovery_eligible_when_surprise_high() -> None:
|
||||
x = make_rotor_from_angle(1.1, bivector_idx=7)
|
||||
basis = _basis_identity()
|
||||
analogs = [("a", _id32(), make_rotor_from_angle(0.4, bivector_idx=6))]
|
||||
out = dual_operator(x, basis, analogs, surprise_threshold=0.35)
|
||||
assert out["productive"] is False
|
||||
assert out["surprise_norm"] > 0.35
|
||||
assert out["discovery_eligible"] is True
|
||||
|
||||
|
||||
def test_physics_surprise_does_not_import_teaching_or_vault() -> None:
|
||||
"""No teaching/vault *imports* (docstrings may mention teaching by name)."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
tree = ast.parse(Path(surprise_mod.__file__).read_text(encoding="utf-8"))
|
||||
imported: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
imported.add(alias.name.split(".")[0])
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
imported.add(node.module.split(".")[0])
|
||||
assert "teaching" not in imported
|
||||
assert "vault" not in imported
|
||||
assert "VaultStore" not in inspect.getsource(surprise_mod)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Teaching factory — proposal-only DiscoveryCandidate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_candidate_from_high_surprise_dual() -> None:
|
||||
dual = dual_procrustes_surprise(
|
||||
_id32(), make_rotor_from_angle(0.9, bivector_idx=6), _basis_identity()
|
||||
)
|
||||
assert dual["discovery_eligible"] is True
|
||||
c = candidate_from_surprise_dual(dual, source_turn_trace="trace-abc")
|
||||
assert c is not None
|
||||
assert c.trigger == "high_surprise"
|
||||
assert c.review_state == "unreviewed"
|
||||
assert c.domain == "math"
|
||||
assert c.proposed_chain["kind"] == "high_surprise"
|
||||
assert c.proposed_chain["subject"] == "geometric_frontier"
|
||||
assert c.proposed_chain["intent"] == "discovery"
|
||||
assert c.source_turn_trace == "trace-abc"
|
||||
assert c.boundary_clean is True
|
||||
assert float(c.proposed_chain["surprise_norm"]) == pytest.approx(
|
||||
dual["surprise_norm"]
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_absent_when_not_eligible() -> None:
|
||||
dual = dual_procrustes_surprise(_id32(), _id32(), _basis_identity())
|
||||
assert dual["discovery_eligible"] is False
|
||||
assert candidate_from_surprise_dual(dual) is None
|
||||
|
||||
|
||||
def test_candidate_id_is_deterministic() -> None:
|
||||
# Angle must clear DEFAULT_DISCOVERY_GAMMA (0.35); 0.7 is just under.
|
||||
dual = dual_procrustes_surprise(
|
||||
_id32(), make_rotor_from_angle(1.0, bivector_idx=10), _basis_identity()
|
||||
)
|
||||
assert dual["discovery_eligible"] is True
|
||||
a = candidate_from_surprise_dual(dual, source_turn_trace="t1")
|
||||
b = candidate_from_surprise_dual(dual, source_turn_trace="t1")
|
||||
assert a is not None and b is not None
|
||||
assert a.candidate_id == b.candidate_id
|
||||
assert len(a.candidate_id) == 64 # sha256 hex
|
||||
|
||||
|
||||
def test_emit_surprise_discovery_opt_in_sink() -> None:
|
||||
dual = dual_procrustes_surprise(
|
||||
_id32(), make_rotor_from_angle(1.0, bivector_idx=6), _basis_identity()
|
||||
)
|
||||
sink = DiscoveryBufferSink()
|
||||
c = emit_surprise_discovery(dual, sink, source_turn_trace="emit-1")
|
||||
assert c is not None
|
||||
assert len(sink.lines) == 1
|
||||
payload = json.loads(sink.lines[0])
|
||||
assert payload["trigger"] == "high_surprise"
|
||||
assert payload["review_state"] == "unreviewed"
|
||||
assert payload["domain"] == "math"
|
||||
assert payload["candidate_id"] == c.candidate_id
|
||||
|
||||
|
||||
def test_emit_without_sink_is_pure() -> None:
|
||||
dual = dual_procrustes_surprise(
|
||||
_id32(), make_rotor_from_angle(0.8, bivector_idx=6), _basis_identity()
|
||||
)
|
||||
c = emit_surprise_discovery(dual, sink=None)
|
||||
assert c is not None
|
||||
assert c.trigger == "high_surprise"
|
||||
|
||||
|
||||
def test_emit_no_candidate_leaves_sink_empty() -> None:
|
||||
dual = dual_procrustes_surprise(_id32(), _id32(), _basis_identity())
|
||||
sink = DiscoveryBufferSink()
|
||||
assert emit_surprise_discovery(dual, sink) is None
|
||||
assert sink.lines == []
|
||||
|
||||
|
||||
def test_discovery_module_no_vault_store() -> None:
|
||||
import teaching.discovery as disc
|
||||
|
||||
src = inspect.getsource(disc)
|
||||
assert "VaultStore" not in src
|
||||
# Factory must not call store() / write corpus paths
|
||||
assert "vault.store" not in src
|
||||
assert "VaultStore.store" not in src
|
||||
|
||||
|
||||
def test_productive_transfer_is_not_discovery() -> None:
|
||||
"""Low surprise + low procrustes → transfer, not discovery."""
|
||||
basis = np.column_stack([_id32(), make_rotor_from_angle(0.2, bivector_idx=6)])
|
||||
# Probe nearly in span of identity alone still low surprise on identity basis
|
||||
dual = dual_procrustes_surprise(_id32(), _id32(), _basis_identity())
|
||||
assert dual["transfer_accepted"] is True or dual["surprise_norm"] < 1e-4
|
||||
assert dual["discovery_eligible"] is False
|
||||
assert candidate_from_surprise_dual(dual) is None
|
||||
_ = basis # reserved for future multi-column transfer fixtures
|
||||
Loading…
Reference in a new issue