core/tests/test_relations_chains_v1.py
Shay b5ba9b6d6f feat(adr-0064): cross-pack teaching chains + relations_chains_v1 seed (Phase 1.3+1.4)
ADR-0064 is the corpus-layer sibling of ADR-0063.  The teaching-grounded
surface composer was hardcoded to cognition_chains_v1, so kinship CAUSE/
VERIFICATION prompts fell through to the universal disclosure even though
en_core_relations_v1 was mounted on the live runtime (ADR-0063).

Architectural change in chat/teaching_grounding.py:

  - New TeachingCorpusSpec dataclass (corpus_id, path, pack_id).
  - TEACHING_CORPORA tuple registers every active corpus.  Each
    corpus is 1:1-bound to one lexicon pack — cross-domain triples
    deferred per docs/teaching_order.md §5.
  - _load_corpus(spec) loads one corpus with pack-residency scoped
    to its declared pack.
  - _all_chains_index() aggregates across all registered corpora
    (first-match-wins; cognition first preserves byte-identity).
  - _pack_for_corpus(corpus_id) → bound pack lexicon.
  - clear_teaching_caches() atomic cache invalidation.
  - TeachingChain gains corpus_id field → surface tag follows resolving corpus.

Wiring updates:

  - teaching_grounded_surface + teaching_grounded_surface_composed
    consult _all_chains_index; surface tag follows chain.corpus_id.
  - teaching/discovery.py gate uses chat.pack_resolver.is_resolvable
    (any mounted pack) + _all_chains_index (any registered corpus).
  - teaching/replay.py _swap_corpus_path rewrites the registry path
    + clears all teaching caches during the gate's transient phase.
    Active corpus bytes unchanged (replay invariant preserved).
  - evals/learning_loop/run_demo.py scene-5 swap mirrors the new
    pattern so the demo still grounds against transient corpora.

Back-compat preserved: _corpus_index, _CORPUS_PATH, TEACHING_CORPUS_ID
remain cognition-corpus-specific for audit/replay consumers.

Phase 1.4 — relations_chains_v1 seeded with 7 reviewed kinship chains:
  cause_parent_precedes_child
  cause_child_follows_parent
  cause_ancestor_precedes_descendant
  cause_descendant_follows_ancestor
  cause_family_grounds_parent
  verification_child_requires_parent
  verification_descendant_requires_ancestor

5 of 8 relations lemmas covered.  All connectives already humanised.
Strict pack-internal to en_core_relations_v1 (no cross-domain in v1).
Seed pattern matches cognition_chains_v1's original pre-ADR-0055 seed.

Live verification:
  > Why does parent exist?
  parent — teaching-grounded (relations_chains_v1):
  kinship.ascendant.direct; kinship.parent.
  parent precedes child (kinship.descendant.direct).
  grounding_source = teaching

Cognition eval byte-identical to pre-ADR baseline:
  public:  intent 100% / surface 100% / term 91.7% / closure 100%
  holdout: intent 100% / surface 100% / term 83.3% / closure 100%

Lanes green: smoke 67 / cognition 121 / teaching 17 / packs 6 /
runtime 19 / algebra 132 / full 1933 passed.
2026-05-18 16:04:20 -07:00

207 lines
7.9 KiB
Python

"""ADR-0064 — ``relations_chains_v1`` reviewed teaching corpus tests.
The contract these tests pin:
- The relations corpus loads cleanly via the cross-corpus
aggregator (``_all_chains_index``); none of its chains drop on
pack-residency or schema gates.
- Every chain's subject AND object resides in
``en_core_relations_v1`` (strict pack-internal, per
``docs/teaching_order.md`` §5 — no cross-domain triples in v1).
- Every connective is already humanised by
:data:`generate.semantic_templates._PREDICATE_HUMANIZE` (no new
predicates introduced in this seed).
- Each chain emits a deterministic teaching-grounded surface tagged
``teaching-grounded (relations_chains_v1)``.
- The cognition lane invariant is preserved: cognition chains
still tag ``cognition_chains_v1`` byte-identically.
"""
from __future__ import annotations
import pytest
from chat.teaching_grounding import (
TEACHING_CORPORA,
_all_chains_index,
_load_corpus,
clear_teaching_caches,
has_teaching_chain,
teaching_grounded_surface,
)
from chat.pack_resolver import _pack_lexicon_for
from generate.intent import IntentTag
from generate.semantic_templates import _PREDICATE_HUMANIZE
RELATIONS_CORPUS_ID = "relations_chains_v1"
RELATIONS_PACK_ID = "en_core_relations_v1"
EXPECTED_CHAIN_IDS: frozenset[str] = frozenset({
"cause_parent_precedes_child",
"cause_child_follows_parent",
"cause_ancestor_precedes_descendant",
"cause_descendant_follows_ancestor",
"cause_family_grounds_parent",
"verification_child_requires_parent",
"verification_descendant_requires_ancestor",
})
@pytest.fixture(autouse=True)
def _isolate_caches():
clear_teaching_caches()
yield
clear_teaching_caches()
# ---------------------------------------------------------------------------
# Registry — the relations corpus is registered
# ---------------------------------------------------------------------------
def test_relations_corpus_is_registered() -> None:
corpus_ids = {spec.corpus_id for spec in TEACHING_CORPORA}
assert RELATIONS_CORPUS_ID in corpus_ids
def test_relations_corpus_is_bound_to_relations_pack() -> None:
spec = next(s for s in TEACHING_CORPORA if s.corpus_id == RELATIONS_CORPUS_ID)
assert spec.pack_id == RELATIONS_PACK_ID
# ---------------------------------------------------------------------------
# Corpus content — every chain loads, lives in the right pack
# ---------------------------------------------------------------------------
def test_all_seed_chains_load_cleanly() -> None:
spec = next(s for s in TEACHING_CORPORA if s.corpus_id == RELATIONS_CORPUS_ID)
loaded = _load_corpus(spec)
chain_ids = {c.chain_id for c in loaded.values()}
assert chain_ids == EXPECTED_CHAIN_IDS
def test_every_chain_is_pack_internal_to_relations() -> None:
"""Strict pack-internal invariant: subject AND object must both
reside in ``en_core_relations_v1``. Cross-domain triples are
deferred to a future ADR per teaching_order.md §5."""
spec = next(s for s in TEACHING_CORPORA if s.corpus_id == RELATIONS_CORPUS_ID)
pack = _pack_lexicon_for(RELATIONS_PACK_ID)
loaded = _load_corpus(spec)
for chain in loaded.values():
assert chain.subject in pack, (
f"{chain.chain_id}: subject {chain.subject!r} not in relations pack"
)
assert chain.object in pack, (
f"{chain.chain_id}: object {chain.object!r} not in relations pack"
)
def test_every_connective_is_humanised() -> None:
"""No new predicates introduced in the v1 seed — every connective
must already appear in ``_PREDICATE_HUMANIZE``."""
spec = next(s for s in TEACHING_CORPORA if s.corpus_id == RELATIONS_CORPUS_ID)
loaded = _load_corpus(spec)
for chain in loaded.values():
assert chain.connective in _PREDICATE_HUMANIZE, (
f"{chain.chain_id}: connective {chain.connective!r} not humanised — "
f"add to generate/semantic_templates.py or pick an existing one"
)
def test_corpus_id_recorded_on_loaded_chains() -> None:
spec = next(s for s in TEACHING_CORPORA if s.corpus_id == RELATIONS_CORPUS_ID)
loaded = _load_corpus(spec)
for chain in loaded.values():
assert chain.corpus_id == RELATIONS_CORPUS_ID
# ---------------------------------------------------------------------------
# Aggregated index — chains visible cross-corpus
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"subject,intent",
[
("parent", IntentTag.CAUSE),
("child", IntentTag.CAUSE),
("ancestor", IntentTag.CAUSE),
("descendant", IntentTag.CAUSE),
("family", IntentTag.CAUSE),
("child", IntentTag.VERIFICATION),
("descendant", IntentTag.VERIFICATION),
],
)
def test_has_teaching_chain_finds_relations_chains(subject: str, intent: IntentTag) -> None:
assert has_teaching_chain(subject, intent) is True
# ---------------------------------------------------------------------------
# Surface emission — relations-corpus chains tag their resolving corpus
# ---------------------------------------------------------------------------
def test_relations_surface_tag_is_relations_corpus_id() -> None:
surface = teaching_grounded_surface("parent", IntentTag.CAUSE)
assert surface is not None
assert "teaching-grounded (relations_chains_v1)" in surface
assert "cognition_chains_v1" not in surface
def test_cognition_surface_tag_is_cognition_corpus_id_byte_identical() -> None:
"""ADR-0064 invariant: registering a second corpus must not alter
surfaces emitted by the first. Cognition lemmas still tag
``cognition_chains_v1``."""
surface = teaching_grounded_surface("light", IntentTag.CAUSE)
assert surface is not None
assert "teaching-grounded (cognition_chains_v1)" in surface
assert "relations_chains_v1" not in surface
def test_relations_surface_emits_only_pack_atoms() -> None:
"""Every visible token must be either the lemma itself or a
verbatim ``semantic_domains`` entry from the relations pack — no
synthesis, no rewording."""
surface = teaching_grounded_surface("parent", IntentTag.CAUSE)
assert surface is not None
# Relations-pack atoms expected for parent/child:
relations_pack = _pack_lexicon_for(RELATIONS_PACK_ID)
parent_domains = relations_pack["parent"]
child_domains = relations_pack["child"]
# At least the first parent domain and first child domain appear.
assert parent_domains[0] in surface
assert child_domains[0] in surface
# No cognition-pack signature should appear in a relations
# surface. We check semantic-domain prefixes rather than bare
# lemmas — the template constant ``"No session evidence yet."``
# includes the substring ``evidence`` which would false-positive
# any lemma-substring scan.
for cognition_signature in (
"cognition.knowledge",
"cognition.truth",
"epistemic.ground",
"memory.semantic",
):
assert cognition_signature not in surface, (
f"relations surface leaked cognition signature {cognition_signature!r}"
)
# ---------------------------------------------------------------------------
# Aggregator — orthogonality enforced
# ---------------------------------------------------------------------------
def test_cross_corpus_aggregator_has_both_corpora() -> None:
index = _all_chains_index()
cognition_keys = {k for k, c in index.items() if c.corpus_id == "cognition_chains_v1"}
relations_keys = {k for k, c in index.items() if c.corpus_id == RELATIONS_CORPUS_ID}
assert cognition_keys, "cognition corpus disappeared"
assert relations_keys, "relations corpus did not register"
# Orthogonality: no (subject, intent) cell is claimed by both.
assert not (cognition_keys & relations_keys), (
"cross-corpus (subject, intent) collision — orthogonality broken"
)