core/tests/test_cross_pack_chains.py
Shay d5a6e81b33 feat(adr-0067): cross-pack teaching chains — Plan Phase 4 closed
ADR-0064 bound each teaching corpus 1:1 to a single ratified pack;
chains whose subject + object resolved to different packs were
dropped at load time. Phases 1–3 ratified the per-pack DAGs needed
to lift that constraint safely.

ADR-0067 introduces a deliberately narrow cross-pack chain shape.
Each entry carries explicit subject_pack_id and object_pack_id
fields, and the loader verifies per-chain residency. Same-pack
entries are rejected as corpus-misfilings (anti-leakage). The
cross-pack composer is the fall-through after the in-pack composer,
so the cognition lane stays byte-identical.

Files:
- chat/cross_pack_grounding.py — CrossPackChain + loader +
  single-chain composer + multi-chain enumerators
- teaching/cross_pack_chains/cross_pack_chains_v1.jsonl — 5 seed
  chains (family×identity, parent×understanding, family×memory,
  identity×family, understanding×parent)
- chat/runtime.py — fall-through wiring in CAUSE/VERIFICATION
- chat/narrative_surface.py, chat/example_surface.py — merge
  cross-pack chains, per-chain pack-residency helpers
- tests/test_cross_pack_chains.py — 31 tests covering loader,
  surface, multi-chain access, runtime integration, in-pack
  precedence
- tests/test_narrative_example_intents.py — corpus-tag assertions
  widened to allow cross-pack aggregation

Verification:
- 31 new tests pass
- Curated lanes: smoke 67 / cognition 121 / teaching 17 / packs 6 /
  runtime 19 — all green
- Cognition eval byte-identical (public 100/100/91.7/100, holdout
  100/100/83.3/100)
- Full lane: 2098 passed, 2 skipped, 0 failed in 2:30
2026-05-18 17:22:43 -07:00

356 lines
13 KiB
Python

"""Phase 4 / ADR-0067 — cross-pack teaching chain tests.
Contracts pinned here:
Loader
- Cross-pack corpus loads chains whose subject + object reside in
DIFFERENT ratified packs (verified per-chain via the declared
``subject_pack_id`` / ``object_pack_id`` fields).
- Same-pack entries are rejected as corpus-misfilings.
- Chains whose declared packs do not actually contain the lemmas
are dropped silently (pack-corpus skew defence).
- Invalid intents / missing connective / missing pack ids are
dropped.
- Superseded chains are dropped from the active view.
Single-chain surface
- ``cross_pack_grounded_surface(subject, intent)`` returns a
deterministic surface tagged with both pack ids.
- Returns ``None`` for unknown subject / unsupported intent.
Multi-chain access
- ``cross_pack_chains_for_subject`` and
``cross_pack_chains_for_object`` enumerate every chain (including
duplicates on ``(subject, intent)``) for NARRATIVE / EXAMPLE.
Runtime integration
- CAUSE/VERIFICATION on a cross-pack subject routes to the cross-
pack composer when no in-pack chain resolves.
- In-pack chains still take precedence (cognition lane byte-
identity preserved).
- NARRATIVE aggregates cross-pack + in-pack chains on the same
subject.
- EXAMPLE aggregates cross-pack + in-pack chains on the same object.
"""
from __future__ import annotations
import pytest
from chat.cross_pack_grounding import (
CROSS_PACK_CORPUS_ID,
CrossPackChain,
_all_cross_pack_chains,
_cross_pack_index,
cross_pack_chains_for_object,
cross_pack_chains_for_subject,
cross_pack_grounded_surface,
)
from chat.runtime import ChatRuntime
from generate.intent import IntentTag
# ---------------------------------------------------------------------------
# Loader
# ---------------------------------------------------------------------------
def test_corpus_loads_five_seed_chains() -> None:
chains = _all_cross_pack_chains()
assert len(chains) == 5
chain_ids = {c.chain_id for c in chains}
assert chain_ids == {
"cause_family_grounds_identity",
"cause_parent_grounds_understanding",
"cause_family_supports_memory",
"verification_identity_requires_family",
"verification_understanding_requires_parent",
}
def test_every_chain_actually_crosses_packs() -> None:
for chain in _all_cross_pack_chains():
assert chain.subject_pack_id != chain.object_pack_id
assert chain.subject_pack_id.startswith("en_core_")
assert chain.object_pack_id.startswith("en_core_")
def test_index_first_occurrence_wins_on_collision() -> None:
"""Both ``cause_family_grounds_identity`` and
``cause_family_supports_memory`` share ``(family, cause)``;
the index keeps the first."""
chain = _cross_pack_index()[("family", "cause")]
assert chain.chain_id == "cause_family_grounds_identity"
def test_loader_drops_unparseable_lines(tmp_path, monkeypatch) -> None:
bad = tmp_path / "bad.jsonl"
bad.write_text(
'{"chain_id":"good","subject":"family","intent":"cause",'
'"connective":"grounds","object":"identity",'
'"subject_pack_id":"en_core_relations_v1",'
'"object_pack_id":"en_core_cognition_v1"}\n'
"{ not json }\n"
'"a top-level string"\n',
encoding="utf-8",
)
import chat.cross_pack_grounding as mod
monkeypatch.setattr(mod, "_CORPUS_PATH", bad)
mod.clear_cross_pack_cache()
try:
chains = _all_cross_pack_chains()
assert len(chains) == 1
assert chains[0].chain_id == "good"
finally:
mod.clear_cross_pack_cache()
def test_loader_drops_same_pack_entries(tmp_path, monkeypatch) -> None:
same = tmp_path / "same.jsonl"
same.write_text(
'{"chain_id":"bad","subject":"knowledge","intent":"cause",'
'"connective":"requires","object":"evidence",'
'"subject_pack_id":"en_core_cognition_v1",'
'"object_pack_id":"en_core_cognition_v1"}\n'
'{"chain_id":"good","subject":"family","intent":"cause",'
'"connective":"grounds","object":"identity",'
'"subject_pack_id":"en_core_relations_v1",'
'"object_pack_id":"en_core_cognition_v1"}\n',
encoding="utf-8",
)
import chat.cross_pack_grounding as mod
monkeypatch.setattr(mod, "_CORPUS_PATH", same)
mod.clear_cross_pack_cache()
try:
ids = {c.chain_id for c in _all_cross_pack_chains()}
assert ids == {"good"}
finally:
mod.clear_cross_pack_cache()
def test_loader_drops_lemma_outside_declared_pack(tmp_path, monkeypatch) -> None:
skewed = tmp_path / "skewed.jsonl"
skewed.write_text(
'{"chain_id":"skewed","subject":"family","intent":"cause",'
'"connective":"grounds","object":"identity",'
'"subject_pack_id":"en_core_cognition_v1",'
'"object_pack_id":"en_core_cognition_v1"}\n',
encoding="utf-8",
)
import chat.cross_pack_grounding as mod
monkeypatch.setattr(mod, "_CORPUS_PATH", skewed)
mod.clear_cross_pack_cache()
try:
assert _all_cross_pack_chains() == ()
finally:
mod.clear_cross_pack_cache()
def test_loader_drops_invalid_intent(tmp_path, monkeypatch) -> None:
bad = tmp_path / "bad.jsonl"
bad.write_text(
'{"chain_id":"x","subject":"family","intent":"definition",'
'"connective":"grounds","object":"identity",'
'"subject_pack_id":"en_core_relations_v1",'
'"object_pack_id":"en_core_cognition_v1"}\n',
encoding="utf-8",
)
import chat.cross_pack_grounding as mod
monkeypatch.setattr(mod, "_CORPUS_PATH", bad)
mod.clear_cross_pack_cache()
try:
assert _all_cross_pack_chains() == ()
finally:
mod.clear_cross_pack_cache()
def test_supersession_drops_retired_chain(tmp_path, monkeypatch) -> None:
corpus = tmp_path / "c.jsonl"
corpus.write_text(
'{"chain_id":"old","subject":"family","intent":"cause",'
'"connective":"grounds","object":"identity",'
'"subject_pack_id":"en_core_relations_v1",'
'"object_pack_id":"en_core_cognition_v1"}\n'
'{"chain_id":"new","subject":"family","intent":"cause",'
'"connective":"supports","object":"memory",'
'"subject_pack_id":"en_core_relations_v1",'
'"object_pack_id":"en_core_cognition_v1",'
'"superseded_by":"old"}\n',
encoding="utf-8",
)
import chat.cross_pack_grounding as mod
monkeypatch.setattr(mod, "_CORPUS_PATH", corpus)
mod.clear_cross_pack_cache()
try:
ids = {c.chain_id for c in _all_cross_pack_chains()}
assert ids == {"new"}
finally:
mod.clear_cross_pack_cache()
# ---------------------------------------------------------------------------
# Single-chain surface
# ---------------------------------------------------------------------------
def test_surface_tag_exposes_both_pack_ids() -> None:
s = cross_pack_grounded_surface("family", IntentTag.CAUSE)
assert s is not None
assert "cross-pack-grounded" in s
assert "cross_pack_chains_v1" in s
assert "en_core_relations_v1" in s
assert "en_core_cognition_v1" in s
def test_surface_relations_to_cognition_direction() -> None:
s = cross_pack_grounded_surface("family", IntentTag.CAUSE)
assert s is not None
assert "family grounds identity" in s
assert "kinship.unit" in s
assert "cognition.identity" in s
def test_surface_cognition_to_relations_direction() -> None:
s = cross_pack_grounded_surface("identity", IntentTag.VERIFICATION)
assert s is not None
assert "identity requires family" in s
assert "cognition.identity" in s
assert "kinship.unit" in s
def test_surface_returns_none_for_unknown_subject() -> None:
assert cross_pack_grounded_surface("photosynthesis", IntentTag.CAUSE) is None
assert cross_pack_grounded_surface("xyz", IntentTag.VERIFICATION) is None
@pytest.mark.parametrize("tag", [
IntentTag.DEFINITION,
IntentTag.RECALL,
IntentTag.COMPARISON,
IntentTag.CORRECTION,
IntentTag.PROCEDURE,
])
def test_surface_returns_none_for_unsupported_intent(tag: IntentTag) -> None:
assert cross_pack_grounded_surface("family", tag) is None
def test_surface_is_deterministic() -> None:
a = cross_pack_grounded_surface("family", IntentTag.CAUSE)
b = cross_pack_grounded_surface("family", IntentTag.CAUSE)
assert a == b
def test_surface_empty_input_returns_none() -> None:
assert cross_pack_grounded_surface("", IntentTag.CAUSE) is None
assert cross_pack_grounded_surface(" ", IntentTag.CAUSE) is None
# ---------------------------------------------------------------------------
# Multi-chain access
# ---------------------------------------------------------------------------
def test_chains_for_subject_returns_all_rooted_on_lemma() -> None:
chains = cross_pack_chains_for_subject("family")
ids = [c.chain_id for c in chains]
assert "cause_family_grounds_identity" in ids
assert "cause_family_supports_memory" in ids
def test_chains_for_object_returns_reverse_chains() -> None:
chains = cross_pack_chains_for_object("family")
ids = [c.chain_id for c in chains]
assert "verification_identity_requires_family" in ids
def test_chains_for_subject_unknown_returns_empty() -> None:
assert cross_pack_chains_for_subject("nonexistent") == ()
assert cross_pack_chains_for_subject("") == ()
def test_chains_for_object_unknown_returns_empty() -> None:
assert cross_pack_chains_for_object("nonexistent") == ()
# ---------------------------------------------------------------------------
# Runtime integration
# ---------------------------------------------------------------------------
def test_runtime_verification_on_cross_pack_only_subject() -> None:
"""``identity/verification`` has NO in-pack chain in any registered
corpus — cross-pack composer must fire."""
rt = ChatRuntime()
resp = rt.chat("Does identity require family?")
assert resp.grounding_source == "teaching"
assert "cross-pack-grounded" in resp.surface
assert "identity requires family" in resp.surface
def test_runtime_understanding_verification_routes_cross_pack() -> None:
"""``understanding/verification`` has no in-pack chain (only the
``understanding/cause`` chain exists in the cognition corpus)."""
rt = ChatRuntime()
resp = rt.chat("Does understanding require parent?")
assert resp.grounding_source == "teaching"
assert "cross-pack-grounded" in resp.surface
assert "understanding requires parent" in resp.surface
def test_runtime_in_pack_chain_takes_precedence_over_cross_pack() -> None:
"""When BOTH an in-pack chain and a cross-pack chain match the
same ``(subject, intent)``, the in-pack composer fires. Cross-
pack is the fall-through only — the cognition-lane byte-identity
invariant depends on this rule."""
rt = ChatRuntime()
resp = rt.chat("Why does family exist?")
assert resp.grounding_source == "teaching"
# ``family/cause`` has both an in-pack chain (``family grounds
# parent`` in relations_chains_v1) and a cross-pack chain
# (``family grounds identity``). The in-pack chain wins.
assert "cross-pack-grounded" not in resp.surface
assert "teaching-grounded" in resp.surface
assert "family grounds parent" in resp.surface
def test_runtime_in_pack_cognition_chain_unaffected() -> None:
"""Sanity: ``knowledge/cause`` continues to ground via the
cognition corpus."""
rt = ChatRuntime()
resp = rt.chat("Why does knowledge exist?")
assert resp.grounding_source == "teaching"
assert "cross-pack-grounded" not in resp.surface
assert "teaching-grounded" in resp.surface
def test_runtime_narrative_aggregates_cross_pack_chains() -> None:
rt = ChatRuntime()
resp = rt.chat("Tell me about family.")
assert resp.grounding_source == "teaching"
assert "family grounds identity" in resp.surface
assert "family supports memory" in resp.surface
assert "cross_pack_chains_v1" in resp.surface
def test_runtime_example_aggregates_cross_pack_reverse_chains() -> None:
rt = ChatRuntime()
resp = rt.chat("Give me an example of memory.")
assert resp.grounding_source == "teaching"
assert "family supports memory" in resp.surface
def test_corpus_id_constant_matches_filename() -> None:
from pathlib import Path
expected = (
Path(__file__).resolve().parent.parent
/ "teaching" / "cross_pack_chains"
/ f"{CROSS_PACK_CORPUS_ID}.jsonl"
)
assert expected.exists()
def test_cross_pack_chain_is_frozen() -> None:
chain = _all_cross_pack_chains()[0]
assert isinstance(chain, CrossPackChain)
with pytest.raises(Exception):
chain.subject = "mutated" # type: ignore[misc]