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.
207 lines
7.9 KiB
Python
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"
|
|
)
|