Lands the first deterministic trigger of the discovery → reviewed-
memory loop. Candidates are structured evidence; emission is
opt-in via attach_discovery_sink and NEVER mutates the active
teaching corpus.
- teaching/discovery.py: DiscoveryCandidate dataclass + pure
extract_discovery_candidates(turn_event, intent, subject) rule
firing. Phase B fires only the would_have_grounded trigger:
grounding_source == "none"
AND intent ∈ {CAUSE, VERIFICATION}
AND subject lemma in ratified cognition pack
AND (subject, intent) NOT in active corpus
candidate_id = SHA-256 of canonical JSON payload — replay-stable.
Other DiscoveryTrigger literals (successful_comparison,
hedge_acknowledged, oov_resolved_via_decomp) are reserved for
later phases.
- teaching/discovery_sink.py: DiscoveryCandidateSink protocol,
DiscoveryBufferSink (in-memory), DiscoveryMonthlyFileSink
(append-only JSONL, <root>/<YYYY>/<YYYY-MM>.jsonl rollover,
injectable clock).
- chat/runtime.py: opt-in attach_discovery_sink, post-turn
emission inside _stub_response only when caller threads
classified intent forward (gate-fire fall-through site).
Intent classification at the call site reuses the same
deterministic classifier already invoked by
_maybe_pack_grounded_surface for the empty-vault English path.
Trust boundary: candidates write to a separate sink/file path
only; the active corpus on disk is never touched. Tests
explicitly assert corpus bytes are byte-identical before and
after a candidate-emitting turn.
Tests: tests/test_discovery_candidates.py — 24 tests covering
pure-predicate rule firing, every short-circuit path,
deterministic candidate_id, sink opt-in, runtime parity with no
sink, monthly rollover semantics, append-only behaviour, no
corpus mutation.
Lanes: smoke 67, cognition 121, runtime 19, teaching 17, packs 6
— all green. Cognition eval metrics unchanged on dev / public /
holdout splits. versor_condition < 1e-6 invariant untouched.