feat(third-door): wire high surprise to DiscoveryCandidate (#20 follow-up)
Some checks failed
lane-shas / verify pinned lane SHAs (pull_request) Failing after 15m37s
smoke / smoke (-m "not quarantine") (pull_request) Failing after 22m54s

- 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:
Shay 2026-07-13 17:25:17 -07:00
parent 25f8c14e27
commit 995f4fc3b8
6 changed files with 438 additions and 12 deletions

View file

@ -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",

View file

@ -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,
}

View file

@ -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.

View file

@ -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 ProcrustesSurprise 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",
]

View file

@ -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

View 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