core/tests/test_discovery_candidates.py
Shay 7c80b791ec fix(tests): retire 13 stale failures from full lane — corpus saturation drift
The full lane carried 13 long-standing red tests whose premises were
invalidated by reviewed-corpus growth that landed in earlier commits.
None reflected runtime bugs — all four classes are corpus-state drift
where the test fixture became stale.  Curated lanes were green, full
lane stayed quietly red.  Closes that gap.

1. test_teaching_audit (2 tests).
   * test_audit_real_corpus_runs_clean asserted dropped == () and
     lines_on_disk == lines_loaded — premise written before any
     supersession existed.  Curriculum saturation v2 (commit a0edbb4)
     ratified the wisdom_grounds_judgment → wisdom_requires_knowledge
     supersession; the audit now correctly shows 1 dropped line.
     Rewritten as the line-conservation invariant:
       lines_loaded + len(dropped) == lines_on_disk
     plus a typed-reason check on every dropped entry.
   * test_default_superseded_by_is_null_in_loaded_entries asserted
     ALL loaded entries have superseded_by == None.  Wrong even by
     ADR-0055 design: the replacement entry IS loaded and carries
     the back-pointer to the retired chain.  Rewritten as the
     active-set invariant: any non-null superseded_by on a loaded
     entry must reference a dropped (retired) chain id, never a live
     one — no double-live state.

2. test_learning_loop_demo (7 tests).
   The demo's headline prompt was "Why does thought exist?", and the
   ADR-0057 demo trilogy (commit 82dac4b) chose (thought, cause) as
   the cold cell.  Cognition saturation v2 (commit a0edbb4) ratified
   cause_thought_reveals_meaning into the active corpus — so the
   cold turn now grounds, no discovery candidate is emitted, every
   demo scene breaks.  Rotated the cold subject to ``narrative``
   (pack-resident, no chain, same thematic shape, same affirming
   evidence pointer cause_creation_reveals_meaning).  Demo headline,
   evals/learning_loop/run_demo.py, core/cli.py preamble, and the
   test assertions all updated together so the demo reads cleanly:
       before: [none]     I don't know — insufficient grounding...
       after : [teaching] narrative — teaching-grounded ... narrative
                          reveals meaning ...

3. test_discovery_candidates (4 tests).
   Test fixture used (judgment, CAUSE) as the still-cold pair.
   Epistemology v1 (commit 2acf71f) ratified
   cause_judgment_requires_wisdom — (judgment, cause) is no longer
   cold.  Rotated to ``principle`` (pack-resident, no chain on either
   intent today).  Added a pytest.skip self-guard so when a future
   curriculum unit ratifies a (principle, *) chain the test rotates
   cleanly instead of going red.

Full lane: 1892 passed, 2 skipped, 0 failed (was 4 failed pre-fix,
13 failed pre-ADR-0063).  Cognition eval unchanged: public 100/100/
91.7/100, holdout 100/100/83.3/100.
2026-05-18 15:23:22 -07:00

398 lines
13 KiB
Python

"""ADR-0055 Phase B — DiscoveryCandidate emission + sink contracts.
Pinned contracts:
- ``extract_discovery_candidates`` is pure and deterministic; same
inputs ⇒ identical ``candidate_id``.
- The ``would_have_grounded`` trigger fires only when ALL of:
fall-through (grounding_source == "none"), intent ∈ {CAUSE,
VERIFICATION}, subject lemma is a pack lemma, (subject, intent)
is NOT in the active corpus.
- Candidate emission **never** mutates the active teaching corpus
or runtime state.
- Sink is opt-in. No sink attached ⇒ behaviour identical to pre-B.
- ``DiscoveryMonthlyFileSink`` rolls over by month; previous file
is closed cleanly.
"""
from __future__ import annotations
import json
from pathlib import Path
from datetime import datetime, timezone
import pytest
from chat.runtime import ChatRuntime
from generate.intent import IntentTag
from teaching.discovery import (
DiscoveryCandidate,
extract_discovery_candidates,
format_candidate_jsonl,
)
from teaching.discovery_sink import (
DiscoveryBufferSink,
DiscoveryMonthlyFileSink,
)
# ---------------------------------------------------------------------------
# Synthetic TurnEvent shim (avoids needing a full runtime turn just to test
# the pure rule firing)
# ---------------------------------------------------------------------------
class _TE:
def __init__(
self,
*,
grounding_source: str = "none",
trace_hash: str = "abcd",
refusal_emitted: bool = False,
hedge_injected: bool = False,
) -> None:
self.grounding_source = grounding_source
self.trace_hash = trace_hash
self.refusal_emitted = refusal_emitted
self.hedge_injected = hedge_injected
self.verdicts = None
# ---------------------------------------------------------------------------
# extract_discovery_candidates — pure predicate
# ---------------------------------------------------------------------------
def test_fires_on_would_have_grounded_for_cause_on_unknown_pack_lemma() -> None:
"""``principle`` is a pack lemma; (principle, cause) is NOT in the
active cognition corpus. ``judgment`` was the historical fixture
here, but the epistemology v1 curriculum unit (commit ``2acf71f``)
added ``cause_judgment_requires_wisdom`` — so ``principle`` is the
new still-cold cognition exemplar. If a future curriculum unit
ratifies a ``(principle, *)`` chain, this test pytest-skips and the
fixture should rotate to the next cold lemma."""
from chat.pack_grounding import _pack_index
from chat.teaching_grounding import _corpus_index
assert "principle" in _pack_index()
if ("principle", "cause") in _corpus_index():
pytest.skip("principle/cause is in the active corpus; pick another fixture")
cands = extract_discovery_candidates(
_TE(grounding_source="none"),
IntentTag.CAUSE,
"principle",
)
assert len(cands) == 1
c = cands[0]
assert c.trigger == "would_have_grounded"
assert c.proposed_chain["subject"] == "principle"
assert c.proposed_chain["intent"] == "cause"
assert c.proposed_chain["connective"] is None
assert c.proposed_chain["object"] is None
assert c.pack_consistent is True
assert c.boundary_clean is True
assert c.review_state == "unreviewed"
def test_does_not_fire_when_grounding_source_is_pack() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="pack"),
IntentTag.CAUSE,
"principle",
)
assert cands == ()
def test_does_not_fire_when_grounding_source_is_teaching() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="teaching"),
IntentTag.CAUSE,
"principle",
)
assert cands == ()
def test_does_not_fire_for_definition_intent() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="none"),
IntentTag.DEFINITION,
"principle",
)
assert cands == ()
def test_does_not_fire_for_unknown_intent() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="none"),
IntentTag.UNKNOWN,
"principle",
)
assert cands == ()
def test_does_not_fire_for_non_pack_lemma() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="none"),
IntentTag.CAUSE,
"zzznotalemma",
)
assert cands == ()
def test_does_not_fire_when_chain_already_in_corpus() -> None:
"""``(light, cause)`` IS in the corpus today (``cause_light_reveals_truth``)
— the would_have_grounded predicate must not fire."""
cands = extract_discovery_candidates(
_TE(grounding_source="none"),
IntentTag.CAUSE,
"light",
)
assert cands == ()
def test_does_not_fire_on_empty_lemma() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="none"),
IntentTag.CAUSE,
"",
)
assert cands == ()
def test_does_not_fire_on_none_lemma() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="none"),
IntentTag.CAUSE,
None,
)
assert cands == ()
def test_candidate_id_is_deterministic() -> None:
"""Identical inputs MUST produce the identical candidate_id —
this is the load-bearing replay property."""
a = extract_discovery_candidates(
_TE(grounding_source="none", trace_hash="seed-1"),
IntentTag.CAUSE,
"principle",
)
b = extract_discovery_candidates(
_TE(grounding_source="none", trace_hash="seed-1"),
IntentTag.CAUSE,
"principle",
)
assert a[0].candidate_id == b[0].candidate_id
def test_candidate_id_changes_with_trace_hash() -> None:
a = extract_discovery_candidates(
_TE(grounding_source="none", trace_hash="seed-1"),
IntentTag.CAUSE,
"principle",
)
b = extract_discovery_candidates(
_TE(grounding_source="none", trace_hash="seed-2"),
IntentTag.CAUSE,
"principle",
)
assert a[0].candidate_id != b[0].candidate_id
def test_boundary_clean_false_when_refusal_emitted() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="none", refusal_emitted=True),
IntentTag.CAUSE,
"principle",
)
# The trigger still fires (refusal does not block evidence), but
# boundary_clean flips to False so reviewers can filter these out
# downstream.
assert len(cands) == 1
assert cands[0].boundary_clean is False
def test_boundary_clean_false_when_hedge_injected() -> None:
cands = extract_discovery_candidates(
_TE(grounding_source="none", hedge_injected=True),
IntentTag.CAUSE,
"principle",
)
assert len(cands) == 1
assert cands[0].boundary_clean is False
# ---------------------------------------------------------------------------
# format_candidate_jsonl — stable on-disk shape
# ---------------------------------------------------------------------------
def test_format_jsonl_is_sorted_keys_compact() -> None:
cand = DiscoveryCandidate(
candidate_id="x",
proposed_chain={"subject": "principle", "intent": "cause", "connective": None, "object": None},
trigger="would_have_grounded",
source_turn_trace="t",
pack_consistent=True,
boundary_clean=True,
)
line = format_candidate_jsonl(cand)
parsed = json.loads(line)
assert parsed["candidate_id"] == "x"
# Compact separators — no whitespace between key/value or fields.
assert ", " not in line
assert ": " not in line
# ---------------------------------------------------------------------------
# Runtime integration — sink opt-in + behaviour parity
# ---------------------------------------------------------------------------
def _find_pack_lemma_without_active_chain() -> tuple[str, IntentTag]:
"""Pick a (lemma, intent) that lives in the pack but not in the
corpus. Used so the integration tests are not coupled to a
specific lemma whose status might change as the corpus grows."""
from chat.pack_grounding import _pack_index
from chat.teaching_grounding import _corpus_index
pack = _pack_index()
corpus = _corpus_index()
for lemma in pack:
if (lemma, "cause") not in corpus:
return lemma, IntentTag.CAUSE
if (lemma, "verification") not in corpus:
return lemma, IntentTag.VERIFICATION
pytest.skip("Every pack lemma already has a corpus chain — no would-have-grounded candidate possible")
def test_runtime_no_sink_no_emission() -> None:
"""No sink attached ⇒ no error, no side effect."""
rt = ChatRuntime()
rt.chat("Why does judgment matter?")
# Just must not crash and turn_log must have grown.
assert len(rt.turn_log) >= 1
def test_runtime_emits_to_buffer_sink_on_would_have_grounded() -> None:
"""Wire a buffer sink, send a CAUSE prompt on a lemma without a
corpus chain, expect one JSONL line."""
lemma, intent_tag = _find_pack_lemma_without_active_chain()
if intent_tag is IntentTag.CAUSE:
prompt = f"Why does {lemma} matter?"
else:
prompt = f"Does {lemma} require evidence?"
rt = ChatRuntime()
sink = DiscoveryBufferSink()
rt.attach_discovery_sink(sink)
rt.chat(prompt)
assert len(sink.lines) == 1
payload = json.loads(sink.lines[0])
assert payload["trigger"] == "would_have_grounded"
assert payload["proposed_chain"]["subject"] == lemma
assert payload["review_state"] == "unreviewed"
def test_runtime_does_not_emit_when_turn_is_grounded() -> None:
"""``light`` + CAUSE grounds via the teaching corpus today; no
candidate should be emitted on a grounded turn."""
rt = ChatRuntime()
sink = DiscoveryBufferSink()
rt.attach_discovery_sink(sink)
rt.chat("Why does light matter?")
assert sink.lines == []
def test_runtime_detach_sink_stops_emission() -> None:
lemma, _ = _find_pack_lemma_without_active_chain()
rt = ChatRuntime()
sink = DiscoveryBufferSink()
rt.attach_discovery_sink(sink)
rt.attach_discovery_sink(None)
rt.chat(f"Why does {lemma} matter?")
assert sink.lines == []
def test_runtime_emission_does_not_mutate_corpus_on_disk() -> None:
"""Phase B's core trust-boundary claim: emission writes nothing
to the corpus."""
from chat.teaching_grounding import _CORPUS_PATH
lemma, _ = _find_pack_lemma_without_active_chain()
before = _CORPUS_PATH.read_bytes()
rt = ChatRuntime()
rt.attach_discovery_sink(DiscoveryBufferSink())
rt.chat(f"Why does {lemma} matter?")
after = _CORPUS_PATH.read_bytes()
assert before == after
# ---------------------------------------------------------------------------
# DiscoveryMonthlyFileSink — rollover semantics
# ---------------------------------------------------------------------------
def test_monthly_sink_writes_jsonl_in_expected_month_path(tmp_path: Path) -> None:
fixed = datetime(2026, 5, 18, tzinfo=timezone.utc)
sink = DiscoveryMonthlyFileSink(tmp_path, clock=lambda: fixed)
sink.emit('{"a":1}')
sink.close()
written = tmp_path / "2026" / "2026-05.jsonl"
assert written.exists()
assert written.read_text(encoding="utf-8").splitlines() == ['{"a":1}']
def test_monthly_sink_rolls_over_on_month_change(tmp_path: Path) -> None:
fake = {"now": datetime(2026, 5, 31, tzinfo=timezone.utc)}
sink = DiscoveryMonthlyFileSink(tmp_path, clock=lambda: fake["now"])
sink.emit('{"a":1}')
fake["now"] = datetime(2026, 6, 1, tzinfo=timezone.utc)
sink.emit('{"b":2}')
sink.close()
may = (tmp_path / "2026" / "2026-05.jsonl").read_text(encoding="utf-8")
jun = (tmp_path / "2026" / "2026-06.jsonl").read_text(encoding="utf-8")
assert may.splitlines() == ['{"a":1}']
assert jun.splitlines() == ['{"b":2}']
def test_monthly_sink_append_only_existing_lines_preserved(tmp_path: Path) -> None:
target = tmp_path / "2026" / "2026-05.jsonl"
target.parent.mkdir(parents=True)
target.write_text('{"x":0}\n', encoding="utf-8")
fixed = datetime(2026, 5, 18, tzinfo=timezone.utc)
sink = DiscoveryMonthlyFileSink(tmp_path, clock=lambda: fixed)
sink.emit('{"y":1}')
sink.close()
lines = target.read_text(encoding="utf-8").splitlines()
assert lines == ['{"x":0}', '{"y":1}']
def test_monthly_sink_context_manager_closes(tmp_path: Path) -> None:
fixed = datetime(2026, 5, 18, tzinfo=timezone.utc)
with DiscoveryMonthlyFileSink(tmp_path, clock=lambda: fixed) as sink:
sink.emit('{"a":1}')
# After exit, internal file handle is closed.
assert sink._fh is None # type: ignore[attr-defined]
# ---------------------------------------------------------------------------
# Doctrine — no corpus mutation, no runtime state mutation
# ---------------------------------------------------------------------------
def test_pure_extract_does_not_load_or_mutate_runtime_state() -> None:
"""Calling the extractor repeatedly must not change pack or
corpus internal state — a smoke test that the extractor is
a pure read."""
from chat.pack_grounding import _pack_index
from chat.teaching_grounding import _corpus_index
pack_before = dict(_pack_index())
corpus_before = dict(_corpus_index())
for _ in range(5):
extract_discovery_candidates(_TE(), IntentTag.CAUSE, "principle")
pack_after = dict(_pack_index())
corpus_after = dict(_corpus_index())
assert pack_before.keys() == pack_after.keys()
assert corpus_before.keys() == corpus_after.keys()