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,
|
||||||
signature_aware_pca_report,
|
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 (
|
from core.physics.biography import (
|
||||||
BiographyHolonomyBlade,
|
BiographyHolonomyBlade,
|
||||||
biography_telemetry,
|
biography_telemetry,
|
||||||
|
|
@ -79,7 +85,9 @@ __all__ = [
|
||||||
"cartan_iwasawa_extract", "cartan_iwasawa_factorize",
|
"cartan_iwasawa_extract", "cartan_iwasawa_factorize",
|
||||||
"conformal_procrustes", "dual_correction_slerp", "procrustes_residual",
|
"conformal_procrustes", "dual_correction_slerp", "procrustes_residual",
|
||||||
"signature_aware_pca", "signature_aware_pca_report",
|
"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",
|
"BiographyHolonomyBlade", "biography_telemetry",
|
||||||
"integrate_biography", "reconstruct_biography",
|
"integrate_biography", "reconstruct_biography",
|
||||||
"TemporalAdmissibilityGate", "TemporalContext",
|
"TemporalAdmissibilityGate", "TemporalContext",
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,14 @@ _NEAR_ZERO = 1e-12
|
||||||
_CLOSURE_TOL = 1e-6
|
_CLOSURE_TOL = 1e-6
|
||||||
_GRADE_TOL = 1e-9
|
_GRADE_TOL = 1e-9
|
||||||
_MAX_GRADE = 5
|
_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):
|
class SurpriseResidualError(ValueError):
|
||||||
|
|
@ -196,18 +204,45 @@ def surprise_residual(
|
||||||
raise ValueError("surprise_residual expects 5-vector or 32-vector x")
|
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(
|
def dual_procrustes_surprise(
|
||||||
P: np.ndarray,
|
P: np.ndarray,
|
||||||
Q: np.ndarray,
|
Q: np.ndarray,
|
||||||
current_basis: np.ndarray,
|
current_basis: np.ndarray,
|
||||||
|
*,
|
||||||
|
discovery_gamma: float = DEFAULT_DISCOVERY_GAMMA,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""The dual operator: run Procrustes and Surprise together.
|
"""The dual operator: run Procrustes and Surprise together.
|
||||||
|
|
||||||
``transfer_accepted`` is a PRODUCTIVE-TRANSFER gate: a structural match was
|
``transfer_accepted`` is a PRODUCTIVE-TRANSFER gate: a structural match was
|
||||||
found (low Procrustes residual) AND the probe is inside the admissible span
|
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
|
(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
|
surprise is NOT a transfer; it is a *discovery* signal
|
||||||
fail-closed surprise refusal (degenerate basis) is recorded, not raised.
|
(``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)
|
V, proc_residual = conformal_procrustes(P, Q)
|
||||||
Q_arr = np.asarray(Q, dtype=np.float64)
|
Q_arr = np.asarray(Q, dtype=np.float64)
|
||||||
|
|
@ -232,16 +267,27 @@ def dual_procrustes_surprise(
|
||||||
if np.asarray(V).shape == (N_COMPONENTS,):
|
if np.asarray(V).shape == (N_COMPONENTS,):
|
||||||
closed = versor_condition(V) < _CLOSURE_TOL
|
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 {
|
return {
|
||||||
"versor": V,
|
"versor": V,
|
||||||
"procrustes_residual": float(proc_residual),
|
"procrustes_residual": float(proc_residual),
|
||||||
"surprise_vector": sur_vec,
|
"surprise_vector": sur_vec,
|
||||||
"surprise_norm": float(sur_norm),
|
"surprise_norm": float(sur_norm),
|
||||||
"transfer_accepted": bool(
|
"transfer_accepted": transfer_accepted,
|
||||||
refused is None and proc_residual < 1e-5 and sur_norm < 1e-4 and closed
|
|
||||||
),
|
|
||||||
"versor_closed": bool(closed),
|
"versor_closed": bool(closed),
|
||||||
"surprise_refused": refused,
|
"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,
|
kappa: float = 1.0,
|
||||||
productive_threshold: float = 0.35,
|
productive_threshold: float = 0.35,
|
||||||
surprise_threshold: float = 0.35,
|
surprise_threshold: float = 0.35,
|
||||||
|
discovery_gamma: float | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Extended dual for multivector analogy seeds (ADR-0240 harness).
|
"""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
|
a structural match (``proc_r <= thr``) AND the query being inside the
|
||||||
admissible span (``sur_norm <= surprise_threshold``). The previous
|
admissible span (``sur_norm <= surprise_threshold``). The previous
|
||||||
``sur_norm >= 0.0`` conjunct was always true, so surprise never gated. A HIGH
|
``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):
|
if isinstance(basis, np.ndarray):
|
||||||
B = basis
|
B = basis
|
||||||
|
|
@ -276,7 +325,10 @@ def dual_operator(
|
||||||
except SurpriseResidualError as exc:
|
except SurpriseResidualError as exc:
|
||||||
sur_vec, sur_norm, surprise_refused = None, float("inf"), exc.reason
|
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:
|
if not analogs:
|
||||||
|
productive = False
|
||||||
return {
|
return {
|
||||||
"surprise_norm": float(sur_norm),
|
"surprise_norm": float(sur_norm),
|
||||||
"procrustes_residual": float("inf"),
|
"procrustes_residual": float("inf"),
|
||||||
|
|
@ -285,6 +337,13 @@ def dual_operator(
|
||||||
"selected_analog_id": None,
|
"selected_analog_id": None,
|
||||||
"versor": None,
|
"versor": None,
|
||||||
"surprise_refused": surprise_refused,
|
"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]
|
aid, src, tgt = analogs[0]
|
||||||
V, proc_r = conformal_procrustes(src, tgt)
|
V, proc_r = conformal_procrustes(src, tgt)
|
||||||
|
|
@ -303,4 +362,11 @@ def dual_operator(
|
||||||
"versor": V,
|
"versor": V,
|
||||||
"surprise_vector": sur_vec,
|
"surprise_vector": sur_vec,
|
||||||
"surprise_refused": surprise_refused,
|
"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 |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 7 | Trajectory invariants + zero-fabrication | R&D §2.2 | ⚫ absent | #21 |
|
||||||
| 8 | ADR-DAG conformal embedding | R&D §2.4 | ⚫ absent | #21 |
|
| 8 | ADR-DAG conformal embedding | R&D §2.4 | ⚫ absent | #21 |
|
||||||
| — | Biography holonomy | (ADR-0240; not in blueprints) | 🟢 sound | — |
|
| — | 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.
|
- **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.
|
- 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)
|
### Remaining → wiring landed (issue #30 / this surface)
|
||||||
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.
|
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 |
|
| Kabsch-conformal Procrustes on point sets | #17 |
|
||||||
| GoldTether gold-set + harmonized residual + α=Φ(R) | #18 |
|
| GoldTether gold-set + harmonized residual + α=Φ(R) | #18 |
|
||||||
| Grade-5 pseudoscalar preservation gate — ⚪ RETIRED (vacuous; see §5) | #19 (closed) |
|
| 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 |
|
| 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.
|
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 hashlib
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
|
@ -65,6 +66,9 @@ DiscoveryTrigger = Literal[
|
||||||
"successful_comparison",
|
"successful_comparison",
|
||||||
"hedge_acknowledged",
|
"hedge_acknowledged",
|
||||||
"oov_resolved_via_decomp",
|
"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=(",", ":"))
|
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__ = [
|
__all__ = [
|
||||||
"ClaimDomain",
|
"ClaimDomain",
|
||||||
"DiscoveryCandidate",
|
"DiscoveryCandidate",
|
||||||
"DiscoveryTrigger",
|
"DiscoveryTrigger",
|
||||||
"EvidencePointer",
|
"EvidencePointer",
|
||||||
"SubQuestion",
|
"SubQuestion",
|
||||||
|
"candidate_from_surprise_dual",
|
||||||
|
"emit_surprise_discovery",
|
||||||
"extract_discovery_candidates",
|
"extract_discovery_candidates",
|
||||||
"format_candidate_jsonl",
|
"format_candidate_jsonl",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,9 @@ def test_dual_procrustes_surprise_audit_dict():
|
||||||
assert "procrustes_residual" in out
|
assert "procrustes_residual" in out
|
||||||
assert "surprise_norm" in out
|
assert "surprise_norm" in out
|
||||||
assert "transfer_accepted" in out
|
assert "transfer_accepted" in out
|
||||||
|
assert "discovery_eligible" in out
|
||||||
assert isinstance(out["transfer_accepted"], bool)
|
assert isinstance(out["transfer_accepted"], bool)
|
||||||
|
assert isinstance(out["discovery_eligible"], bool)
|
||||||
if np.asarray(out["versor"]).shape == (32,):
|
if np.asarray(out["versor"]).shape == (32,):
|
||||||
assert versor_condition(out["versor"]) < 1e-6
|
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