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.
This commit is contained in:
parent
7c80b791ec
commit
b5ba9b6d6f
9 changed files with 850 additions and 28 deletions
|
|
@ -50,6 +50,7 @@ from functools import lru_cache
|
|||
from pathlib import Path
|
||||
|
||||
from chat.pack_grounding import PACK_ID as COGNITION_PACK_ID, _pack_index
|
||||
from chat.pack_resolver import _pack_lexicon_for
|
||||
from generate.intent import IntentTag
|
||||
from generate.semantic_templates import humanize_predicate
|
||||
|
||||
|
|
@ -62,21 +63,69 @@ _INTENT_TAG_BY_NAME: dict[str, IntentTag] = {
|
|||
"verification": IntentTag.VERIFICATION,
|
||||
}
|
||||
|
||||
_TEACHING_ROOT = Path(__file__).resolve().parent.parent / "teaching"
|
||||
|
||||
_CORPUS_PATH = (
|
||||
Path(__file__).resolve().parent.parent
|
||||
/ "teaching"
|
||||
_TEACHING_ROOT
|
||||
/ "cognition_chains"
|
||||
/ f"{TEACHING_CORPUS_ID}.jsonl"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TeachingCorpusSpec:
|
||||
"""ADR-0064 — descriptor for one reviewed teaching corpus.
|
||||
|
||||
A corpus is a JSONL file of reviewed chains plus the single lexicon
|
||||
pack whose vocabulary every chain in that corpus must reside in. The
|
||||
1-to-1 corpus↔pack binding is the structural invariant that prevents
|
||||
cross-domain leakage during cold-start surface composition: a
|
||||
relations-domain chain cannot accidentally surface a cognition-pack
|
||||
atom (or vice versa) because the pack-consistency check at load time
|
||||
is scoped to the corpus's declared pack.
|
||||
|
||||
Each registered corpus is treated as immutable, reviewed memory.
|
||||
Cross-domain triples (cognition × relations) are deliberately out of
|
||||
scope for v1 — they require a follow-up ADR that introduces a
|
||||
cross-pack chain shape, per ``docs/teaching_order.md`` §5.
|
||||
"""
|
||||
|
||||
corpus_id: str
|
||||
path: Path
|
||||
pack_id: str
|
||||
|
||||
|
||||
# ADR-0064 — registered teaching corpora. Order matters: chains in
|
||||
# earlier corpora win on (subject, intent) collision. Cognition is
|
||||
# listed first so the cognition-lane byte-identity invariant is
|
||||
# preserved when a relations chain ever shares a key (today the
|
||||
# orthogonal-pack invariant prevents any such collision, but the
|
||||
# resolution rule is documented).
|
||||
TEACHING_CORPORA: tuple[TeachingCorpusSpec, ...] = (
|
||||
TeachingCorpusSpec(
|
||||
corpus_id="cognition_chains_v1",
|
||||
path=_TEACHING_ROOT / "cognition_chains" / "cognition_chains_v1.jsonl",
|
||||
pack_id="en_core_cognition_v1",
|
||||
),
|
||||
TeachingCorpusSpec(
|
||||
corpus_id="relations_chains_v1",
|
||||
path=_TEACHING_ROOT / "relations_chains" / "relations_chains_v1.jsonl",
|
||||
pack_id="en_core_relations_v1",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TeachingChain:
|
||||
"""One reviewed cognition chain.
|
||||
"""One reviewed teaching chain.
|
||||
|
||||
Fields are copied verbatim from the JSONL line; the runtime never
|
||||
mutates them. ``provenance`` is preserved for audit but not emitted
|
||||
in the user-facing surface.
|
||||
|
||||
ADR-0064 — ``corpus_id`` records which registered teaching corpus
|
||||
the chain belongs to so the surface tag and audit trail are
|
||||
unambiguous when multiple corpora are active.
|
||||
"""
|
||||
|
||||
chain_id: str
|
||||
|
|
@ -87,11 +136,87 @@ class TeachingChain:
|
|||
domains_subject_k: int
|
||||
domains_object_k: int
|
||||
provenance: str
|
||||
corpus_id: str = "cognition_chains_v1"
|
||||
|
||||
|
||||
def _load_corpus(spec: TeachingCorpusSpec) -> dict[tuple[str, str], TeachingChain]:
|
||||
"""ADR-0064 — load one registered teaching corpus.
|
||||
|
||||
Returns ``{(subject_lower, intent_lower): TeachingChain}`` keyed
|
||||
within this corpus only. Pack-consistency is scoped to
|
||||
``spec.pack_id``: every chain's subject AND object must reside in
|
||||
that specific pack's lexicon. Cross-pack chain shapes (e.g. a
|
||||
relations subject with a cognition object) are out of scope for
|
||||
v1 per ``docs/teaching_order.md`` §5 and produce a drop with no
|
||||
surface impact.
|
||||
|
||||
ADR-0055 Phase A: an entry whose ``chain_id`` appears as another
|
||||
entry's ``superseded_by`` is dropped from the active view.
|
||||
Append-only history on disk is preserved; the loader derives the
|
||||
active set.
|
||||
"""
|
||||
if not spec.path.exists():
|
||||
return {}
|
||||
pack = _pack_lexicon_for(spec.pack_id)
|
||||
if not pack:
|
||||
return {}
|
||||
superseded_ids: set[str] = set()
|
||||
parsed_lines: list[dict] = []
|
||||
for line in spec.path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
parsed_lines.append(entry)
|
||||
sup = entry.get("superseded_by")
|
||||
if isinstance(sup, str) and sup.strip():
|
||||
superseded_ids.add(sup.strip())
|
||||
|
||||
out: dict[tuple[str, str], TeachingChain] = {}
|
||||
for entry in parsed_lines:
|
||||
subject = (entry.get("subject") or "").strip().lower()
|
||||
intent = (entry.get("intent") or "").strip().lower()
|
||||
obj = (entry.get("object") or "").strip().lower()
|
||||
connective = (entry.get("connective") or "").strip()
|
||||
if not subject or not intent or not obj or not connective:
|
||||
continue
|
||||
if intent not in _VALID_INTENTS:
|
||||
continue
|
||||
if subject not in pack or obj not in pack:
|
||||
continue
|
||||
chain_id = str(entry.get("chain_id") or f"{subject}_{intent}")
|
||||
if chain_id in superseded_ids:
|
||||
continue
|
||||
try:
|
||||
chain = TeachingChain(
|
||||
chain_id=chain_id,
|
||||
subject=subject,
|
||||
intent=intent,
|
||||
connective=connective,
|
||||
object=obj,
|
||||
domains_subject_k=int(entry.get("domains_subject_k", 2)),
|
||||
domains_object_k=int(entry.get("domains_object_k", 1)),
|
||||
provenance=str(entry.get("provenance", "")),
|
||||
corpus_id=spec.corpus_id,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
out[(subject, intent)] = chain
|
||||
return out
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _corpus_index() -> dict[tuple[str, str], TeachingChain]:
|
||||
"""Load the cognition-chains corpus once.
|
||||
"""Load the cognition-chains corpus once (back-compat surface).
|
||||
|
||||
Retained for discovery / replay / audit consumers whose semantics
|
||||
are scoped to the cognition corpus specifically. Cross-corpus
|
||||
composition uses :func:`_all_chains_index` instead.
|
||||
|
||||
Returns ``{(subject_lower, intent_lower): TeachingChain}``. Entries
|
||||
with invalid schema, unsupported intents, or with subject/object
|
||||
|
|
@ -155,6 +280,7 @@ def _corpus_index() -> dict[tuple[str, str], TeachingChain]:
|
|||
domains_subject_k=int(entry.get("domains_subject_k", 2)),
|
||||
domains_object_k=int(entry.get("domains_object_k", 1)),
|
||||
provenance=str(entry.get("provenance", "")),
|
||||
corpus_id=TEACHING_CORPUS_ID,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
|
@ -162,6 +288,46 @@ def _corpus_index() -> dict[tuple[str, str], TeachingChain]:
|
|||
return out
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _all_chains_index() -> dict[tuple[str, str], TeachingChain]:
|
||||
"""ADR-0064 — aggregated view across every registered teaching corpus.
|
||||
|
||||
Returns ``{(subject_lower, intent_lower): TeachingChain}`` keyed
|
||||
across all corpora in :data:`TEACHING_CORPORA`. Registration order
|
||||
is the resolution order: earlier corpora win on collision. The
|
||||
cognition corpus is registered first so the cognition-lane
|
||||
byte-identity invariant is preserved.
|
||||
|
||||
The :func:`_corpus_index` back-compat loader is **not** an input to
|
||||
this aggregator — both consult the same underlying file but
|
||||
:func:`_corpus_index` is reserved for cognition-corpus-only
|
||||
consumers (audit, replay, discovery's gate). Cross-corpus surface
|
||||
composition consults :func:`_all_chains_index`.
|
||||
"""
|
||||
aggregated: dict[tuple[str, str], TeachingChain] = {}
|
||||
for spec in TEACHING_CORPORA:
|
||||
corpus = _load_corpus(spec)
|
||||
for key, chain in corpus.items():
|
||||
if key not in aggregated:
|
||||
aggregated[key] = chain
|
||||
return aggregated
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _pack_for_corpus(corpus_id: str) -> dict[str, tuple[str, ...]]:
|
||||
"""Return the lexicon for the pack bound to *corpus_id*, cached.
|
||||
|
||||
ADR-0064 — each registered teaching corpus is bound to exactly
|
||||
one lexicon pack via :data:`TEACHING_CORPORA`. Returns an empty
|
||||
dict if *corpus_id* is unknown — callers see this as "chain
|
||||
cannot be surfaced" and fall through to the universal disclosure.
|
||||
"""
|
||||
for spec in TEACHING_CORPORA:
|
||||
if spec.corpus_id == corpus_id:
|
||||
return _pack_lexicon_for(spec.pack_id)
|
||||
return {}
|
||||
|
||||
|
||||
def _intent_name(intent_tag: IntentTag) -> str | None:
|
||||
"""Return the lower-case intent key for the corpus, or ``None``."""
|
||||
if intent_tag is IntentTag.CAUSE:
|
||||
|
|
@ -202,10 +368,12 @@ def teaching_grounded_surface(
|
|||
intent_name = _intent_name(intent_tag)
|
||||
if intent_name is None:
|
||||
return None
|
||||
chain = _corpus_index().get((key, intent_name))
|
||||
chain = _all_chains_index().get((key, intent_name))
|
||||
if chain is None:
|
||||
return None
|
||||
pack = _pack_index()
|
||||
# ADR-0064 — pack-residency is scoped to the chain's resolving
|
||||
# corpus. Each registered corpus is bound to exactly one pack.
|
||||
pack = _pack_for_corpus(chain.corpus_id)
|
||||
subject_domains = pack.get(chain.subject, ())
|
||||
object_domains = pack.get(chain.object, ())
|
||||
if not subject_domains or not object_domains:
|
||||
|
|
@ -218,7 +386,7 @@ def teaching_grounded_surface(
|
|||
)
|
||||
connective = humanize_predicate(chain.connective)
|
||||
return (
|
||||
f"{chain.subject} — teaching-grounded ({TEACHING_CORPUS_ID}): "
|
||||
f"{chain.subject} — teaching-grounded ({chain.corpus_id}): "
|
||||
f"{head_subject}. {chain.subject} {connective} {chain.object} "
|
||||
f"({head_object}). No session evidence yet."
|
||||
)
|
||||
|
|
@ -258,11 +426,12 @@ def teaching_grounded_surface_composed(
|
|||
intent_name = _intent_name(intent_tag)
|
||||
if intent_name is None:
|
||||
return None
|
||||
corpus = _corpus_index()
|
||||
corpus = _all_chains_index()
|
||||
chain = corpus.get((key, intent_name))
|
||||
if chain is None:
|
||||
return None
|
||||
pack = _pack_index()
|
||||
# ADR-0064 — pack lookups follow each chain's resolving corpus.
|
||||
pack = _pack_for_corpus(chain.corpus_id)
|
||||
subject_domains = pack.get(chain.subject, ())
|
||||
object_domains = pack.get(chain.object, ())
|
||||
if not subject_domains or not object_domains:
|
||||
|
|
@ -294,18 +463,19 @@ def teaching_grounded_surface_composed(
|
|||
# No follow-up available — degrade to single-chain surface
|
||||
# byte-identically with ``teaching_grounded_surface``.
|
||||
return (
|
||||
f"{chain.subject} — teaching-grounded ({TEACHING_CORPUS_ID}): "
|
||||
f"{chain.subject} — teaching-grounded ({chain.corpus_id}): "
|
||||
f"{head_subject}. {chain.subject} {connective} {chain.object} "
|
||||
f"({head_object_short}). No session evidence yet."
|
||||
)
|
||||
|
||||
follow_object_domains = pack.get(follow_up.object, ())
|
||||
follow_pack = _pack_for_corpus(follow_up.corpus_id)
|
||||
follow_object_domains = follow_pack.get(follow_up.object, ())
|
||||
if not follow_object_domains:
|
||||
# Follow-up's object isn't pack-resident with semantic domains
|
||||
# — degrade to single-chain surface rather than emit a
|
||||
# partially-grounded composition.
|
||||
return (
|
||||
f"{chain.subject} — teaching-grounded ({TEACHING_CORPUS_ID}): "
|
||||
f"{chain.subject} — teaching-grounded ({chain.corpus_id}): "
|
||||
f"{head_subject}. {chain.subject} {connective} {chain.object} "
|
||||
f"({head_object_short}). No session evidence yet."
|
||||
)
|
||||
|
|
@ -315,7 +485,7 @@ def teaching_grounded_surface_composed(
|
|||
)
|
||||
follow_connective = humanize_predicate(follow_up.connective)
|
||||
return (
|
||||
f"{chain.subject} — teaching-grounded ({TEACHING_CORPUS_ID}): "
|
||||
f"{chain.subject} — teaching-grounded ({chain.corpus_id}): "
|
||||
f"{head_subject}. {chain.subject} {connective} {chain.object} "
|
||||
f"({head_object_short}), which {follow_connective} {follow_up.object} "
|
||||
f"({follow_head}). No session evidence yet."
|
||||
|
|
@ -323,10 +493,25 @@ def teaching_grounded_surface_composed(
|
|||
|
||||
|
||||
def has_teaching_chain(subject_lemma: str, intent_tag: IntentTag) -> bool:
|
||||
"""Return True iff a reviewed chain exists for (subject, intent)."""
|
||||
"""Return True iff a reviewed chain exists for (subject, intent)
|
||||
in any registered teaching corpus (ADR-0064 cross-corpus view)."""
|
||||
if not subject_lemma or not isinstance(subject_lemma, str):
|
||||
return False
|
||||
intent_name = _intent_name(intent_tag)
|
||||
if intent_name is None:
|
||||
return False
|
||||
return (subject_lemma.strip().lower(), intent_name) in _corpus_index()
|
||||
return (subject_lemma.strip().lower(), intent_name) in _all_chains_index()
|
||||
|
||||
|
||||
def clear_teaching_caches() -> None:
|
||||
"""Drop every teaching-grounding lru_cache.
|
||||
|
||||
ADR-0064 — the replay-equivalence gate swaps ``_CORPUS_PATH`` to
|
||||
a transient corpus and clears ``_corpus_index``; when multiple
|
||||
corpora are registered the aggregated index must also reset so
|
||||
the swap takes effect. Test-only and replay-only escape hatch;
|
||||
production code never calls this on the hot path.
|
||||
"""
|
||||
_corpus_index.cache_clear()
|
||||
_all_chains_index.cache_clear()
|
||||
_pack_for_corpus.cache_clear()
|
||||
|
|
|
|||
153
docs/curriculum/relations_chains_v1.md
Normal file
153
docs/curriculum/relations_chains_v1.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Curriculum Unit — `relations_chains_v1` (Kinship Seed)
|
||||
|
||||
**Date:** 2026-05-18
|
||||
**Author:** Shay
|
||||
**Corpus ID:** `relations_chains_v1`
|
||||
**Pack binding:** `en_core_relations_v1` (1:1, pack-internal only)
|
||||
**Chain count:** 7
|
||||
**Status:** Ratified — initial reviewed seed for the kinship domain.
|
||||
|
||||
---
|
||||
|
||||
## Why this unit
|
||||
|
||||
The `en_core_relations_v1` kinship pack was mounted by default in
|
||||
ADR-0063, but the live teaching-grounded path had no reviewed chains
|
||||
for any kinship lemma. Every cold-start CAUSE/VERIFICATION on a
|
||||
kinship prompt fell through to the universal disclosure even though
|
||||
the lemmas were known.
|
||||
|
||||
ADR-0064 closed that gap architecturally (cross-pack teaching corpora
|
||||
registered, surface composers consult the aggregated index). This
|
||||
unit closes it operationally — seven hand-authored chains that
|
||||
exercise every formation gate end-to-end against a fresh corpus.
|
||||
|
||||
Per [`teaching_order.md`](../teaching_order.md) §5 — "Pick *one*
|
||||
commercial domain and run the full 1→4 progression *inside* that
|
||||
domain before opening a second domain. Cross-domain triples come
|
||||
last and only after both domains have ratified their own internal
|
||||
DAG." Every chain in v1 is therefore **strictly pack-internal** to
|
||||
`en_core_relations_v1`. Cross-domain triples (e.g. `family grounds
|
||||
identity` with `identity` from cognition) are deliberately deferred.
|
||||
|
||||
---
|
||||
|
||||
## The seven chains
|
||||
|
||||
| Chain ID | Subject | Intent | Connective | Object |
|
||||
|---|---|---|---|---|
|
||||
| `cause_parent_precedes_child` | parent | cause | precedes | child |
|
||||
| `cause_child_follows_parent` | child | cause | follows | parent |
|
||||
| `cause_ancestor_precedes_descendant` | ancestor | cause | precedes | descendant |
|
||||
| `cause_descendant_follows_ancestor` | descendant | cause | follows | ancestor |
|
||||
| `cause_family_grounds_parent` | family | cause | grounds | parent |
|
||||
| `verification_child_requires_parent` | child | verification | requires | parent |
|
||||
| `verification_descendant_requires_ancestor` | descendant | verification | requires | ancestor |
|
||||
|
||||
Pack-residency: every subject ∈ `en_core_relations_v1`, every
|
||||
object ∈ `en_core_relations_v1`. Zero cognition-pack atoms.
|
||||
|
||||
Predicate residency: every connective (`precedes`, `follows`,
|
||||
`grounds`, `requires`) already exists in
|
||||
`generate.semantic_templates._PREDICATE_HUMANIZE`. No new
|
||||
predicates introduced in this seed.
|
||||
|
||||
---
|
||||
|
||||
## Coverage
|
||||
|
||||
Five of the eight ratified relations lemmas (`parent`, `child`,
|
||||
`ancestor`, `descendant`, `family`) receive at least one chain.
|
||||
Three lemmas (`sibling`, `spouse`, `offspring`) are intentionally
|
||||
deferred to a future curriculum unit — they need lateral / affinal /
|
||||
descendant-direct chains that compose more naturally once the v1
|
||||
ancestor-descendant axis is in place.
|
||||
|
||||
---
|
||||
|
||||
## Live verification
|
||||
|
||||
```
|
||||
$ core chat
|
||||
> Why does parent exist?
|
||||
parent — teaching-grounded (relations_chains_v1):
|
||||
kinship.ascendant.direct; kinship.parent. parent precedes child
|
||||
(kinship.descendant.direct). No session evidence yet.
|
||||
grounding_source = teaching
|
||||
|
||||
> Does child require parent?
|
||||
child — teaching-grounded (relations_chains_v1):
|
||||
kinship.descendant.direct; kinship.child. child requires parent
|
||||
(kinship.ascendant.direct). No session evidence yet.
|
||||
grounding_source = teaching
|
||||
```
|
||||
|
||||
Every kinship CAUSE/VERIFICATION prompt covered by the seed now
|
||||
emits a deterministic teaching-grounded surface tagged with the
|
||||
resolving corpus id (`relations_chains_v1`), not the cognition tag.
|
||||
|
||||
---
|
||||
|
||||
## Provenance
|
||||
|
||||
Every line carries:
|
||||
|
||||
```
|
||||
"provenance": "adr-0064:reviewed:2026-05-18:relations_seed_v1"
|
||||
```
|
||||
|
||||
This is the **direct-append seed pattern** — the same shape used
|
||||
when the cognition corpus was originally seeded pre-ADR-0055
|
||||
(provenance `adr-0052:reviewed:...`, `adr-0053:reviewed:...`).
|
||||
The propose/replay/accept pipeline is for *additions* once a
|
||||
corpus has chains to baseline against; for an empty-corpus seed,
|
||||
the replay gate has no baseline and direct append is the
|
||||
correct surface.
|
||||
|
||||
Future chains added to `relations_chains_v1` must go through the
|
||||
propose/replay/accept pipeline. The seed is the only direct-write.
|
||||
|
||||
---
|
||||
|
||||
## Eval impact
|
||||
|
||||
The cognition lane is **byte-identical** — cognition lemmas resolve
|
||||
to the cognition corpus first and the orthogonal-pack invariant
|
||||
prevents any (subject, intent) collision. Public/holdout splits
|
||||
remain at:
|
||||
|
||||
```
|
||||
public: intent 100% / surface 100% / term 91.7% / closure 100%
|
||||
holdout: intent 100% / surface 100% / term 83.3% / closure 100%
|
||||
```
|
||||
|
||||
Relations-domain coverage opens on the live path but is not yet
|
||||
measured by a dedicated eval lane. A `relations` lane is the
|
||||
natural follow-up.
|
||||
|
||||
---
|
||||
|
||||
## Path forward
|
||||
|
||||
1. **`relations` eval lane.** Mirror the cognition lane harness with
|
||||
relations-domain prompts. Use the seven chains as ground truth.
|
||||
2. **`sibling`/`spouse`/`offspring` chains** — extend the seed to
|
||||
cover the remaining ratified lemmas.
|
||||
3. **Pronoun + role-filler v2** — `mother`/`father`/`son`/`daughter`
|
||||
chains as specializations of v1's primitives.
|
||||
4. **Cross-domain triples** — only after the relations corpus is
|
||||
internally saturated. Then `family grounds identity`,
|
||||
`parent informs experience`, etc.
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
- [ADR-0064](../decisions/ADR-0064-cross-pack-teaching-chains.md) —
|
||||
the architectural unlock that made this seed possible.
|
||||
- [Pack: `en_core_relations_v1`](relations_pack_v1.md) — the lexicon
|
||||
this corpus is bound to.
|
||||
- [`teaching_order.md`](../teaching_order.md) §5 — the
|
||||
prerequisite-topological doctrine.
|
||||
- [Cognition saturation v2](cognition_saturation_v2.md) — the
|
||||
sibling cognition curriculum unit.
|
||||
233
docs/decisions/ADR-0064-cross-pack-teaching-chains.md
Normal file
233
docs/decisions/ADR-0064-cross-pack-teaching-chains.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# ADR-0064 — Cross-pack teaching chains
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-18
|
||||
**Author:** Shay
|
||||
**Supersedes:** none (extends ADR-0052 / ADR-0062 / ADR-0063)
|
||||
**Phase:** Plan Phase 1 (corpus flywheel)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0052 introduced reviewed teaching chains as a third grounding
|
||||
source alongside vault and pack-grounded surfaces. The corpus
|
||||
(`teaching/cognition_chains/cognition_chains_v1.jsonl`) is reviewed,
|
||||
immutable, append-only memory. ADR-0063 brought
|
||||
`en_core_relations_v1` (kinship pack) onto the live runtime, but the
|
||||
teaching-grounded surface composer was still hardcoded to the
|
||||
cognition corpus:
|
||||
|
||||
```python
|
||||
# chat/teaching_grounding.py — pre-ADR-0064
|
||||
TEACHING_CORPUS_ID: str = "cognition_chains_v1"
|
||||
_CORPUS_PATH = .../cognition_chains/cognition_chains_v1.jsonl
|
||||
```
|
||||
|
||||
Every cold-start CAUSE/VERIFICATION prompt on a kinship lemma fell
|
||||
through to the universal disclosure even though the relations pack
|
||||
was mounted, because no kinship chain corpus existed and no path
|
||||
existed to register one.
|
||||
|
||||
ADR-0064 closes that gap architecturally. ADR-0063 was the resolver
|
||||
unlock at the *pack* layer; ADR-0064 is the same unlock at the
|
||||
*teaching corpus* layer.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Teaching corpus registry
|
||||
|
||||
A new dataclass + constant in `chat/teaching_grounding.py`:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TeachingCorpusSpec:
|
||||
corpus_id: str
|
||||
path: Path
|
||||
pack_id: str
|
||||
|
||||
TEACHING_CORPORA: tuple[TeachingCorpusSpec, ...] = (
|
||||
TeachingCorpusSpec("cognition_chains_v1", .../cognition_chains_v1.jsonl, "en_core_cognition_v1"),
|
||||
TeachingCorpusSpec("relations_chains_v1", .../relations_chains_v1.jsonl, "en_core_relations_v1"),
|
||||
)
|
||||
```
|
||||
|
||||
Each corpus is **1:1-bound to exactly one lexicon pack**. The 1:1
|
||||
binding is the structural invariant that prevents cross-domain
|
||||
leakage during cold-start surface composition: a relations chain
|
||||
cannot accidentally surface a cognition-pack atom (or vice versa)
|
||||
because pack-residency at load time is scoped to the corpus's
|
||||
declared pack. Cross-domain chain shapes are out of scope for v1
|
||||
per `docs/teaching_order.md` §5.
|
||||
|
||||
### 2. Aggregated chain index
|
||||
|
||||
`_all_chains_index()` — `lru_cache`d aggregator that loads every
|
||||
registered corpus via `_load_corpus(spec)` and unions them into a
|
||||
single `{(subject, intent): TeachingChain}` view. Registration order
|
||||
is the resolution order; cognition is registered first so cognition-
|
||||
lane byte-identity is preserved on any future cross-corpus
|
||||
collision.
|
||||
|
||||
`TeachingChain` gains a `corpus_id` field so the surface tag and
|
||||
audit trail are unambiguous when multiple corpora are active.
|
||||
|
||||
### 3. Surface composers consult the aggregated view
|
||||
|
||||
`teaching_grounded_surface` and `teaching_grounded_surface_composed`
|
||||
now call `_all_chains_index()`. The surface tag follows the chain's
|
||||
resolving `corpus_id`:
|
||||
|
||||
```
|
||||
parent → teaching-grounded (relations_chains_v1)
|
||||
light → teaching-grounded (cognition_chains_v1)
|
||||
```
|
||||
|
||||
Cognition-lane surfaces remain byte-identical; relations-pack lemmas
|
||||
now ground on the live path.
|
||||
|
||||
### 4. Discovery gate updated for cross-corpus residency
|
||||
|
||||
`teaching/discovery.py` previously gated on
|
||||
`(lemma in cognition_pack) AND ((lemma, intent) not in cognition_corpus)`.
|
||||
Updated to:
|
||||
|
||||
```python
|
||||
from chat.pack_resolver import is_resolvable
|
||||
from chat.teaching_grounding import _all_chains_index
|
||||
|
||||
if not is_resolvable(lemma): # any mounted pack
|
||||
return ()
|
||||
if (lemma, intent_name) in _all_chains_index(): # any registered corpus
|
||||
return ()
|
||||
```
|
||||
|
||||
A kinship CAUSE prompt that lacks a relations chain is now
|
||||
correctly flagged as a discovery candidate, instead of being
|
||||
suppressed because the cognition pack doesn't carry the lemma.
|
||||
|
||||
### 5. Replay-equivalence gate registers the swap
|
||||
|
||||
`teaching/replay.py`'s `_swap_corpus_path` was extended to also
|
||||
rewrite the registry entry's `path` for the swapped corpus AND
|
||||
invalidate `_all_chains_index` cache, so surface composers re-read
|
||||
the swapped corpus during the gate's transient phase. The active
|
||||
corpus on disk remains byte-identical to its pre-swap state — the
|
||||
replay invariant is preserved.
|
||||
|
||||
### 6. Back-compat
|
||||
|
||||
`_corpus_index()`, `_CORPUS_PATH`, `TEACHING_CORPUS_ID` retain
|
||||
cognition-corpus-specific semantics for consumers whose scope is
|
||||
explicitly the cognition corpus (audit, replay's cognition-public
|
||||
runner, replay state-tracking). They are not removed. The aggregated
|
||||
`_all_chains_index` is the new abstraction for surface composers
|
||||
and the discovery gate.
|
||||
|
||||
A new helper `clear_teaching_caches()` drops every teaching-related
|
||||
`lru_cache` atomically — replaces ad-hoc `_corpus_index.cache_clear()`
|
||||
calls in replay code paths.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Capability unlocked
|
||||
|
||||
| Path | Cognition lemmas | Kinship lemmas |
|
||||
|---|---|---|
|
||||
| `teaching_grounded_surface(CAUSE/VERIFICATION)` | byte-identical | **now grounds** for cells in `relations_chains_v1` |
|
||||
| `teaching_grounded_surface_composed` | byte-identical | composes within relations corpus when chain-of-chains exists |
|
||||
| Discovery gate | byte-identical | now emits candidates for kinship cells absent from the relations corpus |
|
||||
|
||||
### Cognition lane: byte-identical
|
||||
|
||||
Cognition lemmas resolve to the cognition corpus first; the
|
||||
orthogonal-pack invariant prevents any (subject, intent) collision
|
||||
between corpora. Public/holdout eval baselines unchanged:
|
||||
|
||||
```
|
||||
public: intent 100% / surface 100% / term 91.7% / closure 100%
|
||||
holdout: intent 100% / surface 100% / term 83.3% / closure 100%
|
||||
```
|
||||
|
||||
### Live verification
|
||||
|
||||
```
|
||||
$ core chat
|
||||
> Why does parent exist?
|
||||
parent — teaching-grounded (relations_chains_v1): kinship.ascendant.direct;
|
||||
kinship.parent. parent precedes child (kinship.descendant.direct).
|
||||
No session evidence yet.
|
||||
grounding_source = teaching
|
||||
```
|
||||
|
||||
### Future ADRs unlocked
|
||||
|
||||
1. **Cross-domain triples.** Once relations corpus saturates
|
||||
internally, a follow-up ADR can extend the chain shape to allow
|
||||
subject and object in different packs (e.g. `family grounds
|
||||
identity` — family ∈ relations, identity ∈ cognition).
|
||||
2. **Relations eval lane.** Mirror the cognition lane harness with
|
||||
relations-domain holdout cases. The seed corpus is the ground
|
||||
truth.
|
||||
3. **Audit + supersede for the relations corpus.** `teaching audit`
|
||||
and `teaching supersede` are cognition-corpus-only today; the
|
||||
registry layer makes generalizing them mechanical.
|
||||
|
||||
---
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
- Each corpus is 1:1-bound to one lexicon pack. The binding is
|
||||
declared statically in `TEACHING_CORPORA` — runtime cannot
|
||||
introduce a new corpus or rebind a pack.
|
||||
- Chain loading is read-only over immutable, reviewed, append-only
|
||||
files. Same trust boundary as ADR-0052.
|
||||
- Surface tag tokens emitted are corpus_id strings declared in
|
||||
`TEACHING_CORPORA` — never derived from user input.
|
||||
- The replay-gate swap rewrites the registry tuple in-place for the
|
||||
duration of the gate's transient phase and restores it on exit.
|
||||
Side-effect-free from outside the contextmanager.
|
||||
|
||||
---
|
||||
|
||||
## Files changed
|
||||
|
||||
```
|
||||
chat/teaching_grounding.py registry layer
|
||||
teaching/discovery.py cross-corpus gate
|
||||
teaching/replay.py swap-the-registry
|
||||
teaching/relations_chains/relations_chains_v1.jsonl NEW (seed corpus, 7 chains)
|
||||
tests/test_relations_chains_v1.py NEW (17 tests)
|
||||
docs/decisions/ADR-0064-cross-pack-teaching-chains.md NEW (this file)
|
||||
docs/decisions/README.md index entry
|
||||
docs/curriculum/relations_chains_v1.md NEW (curriculum unit doc)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
tests/test_relations_chains_v1.py 17 passed
|
||||
tests/test_teaching_audit.py 23 passed
|
||||
tests/test_composed_surface.py 11 passed
|
||||
tests/test_teaching_grounding.py passed
|
||||
tests/test_discovery_candidates.py 24 passed
|
||||
|
||||
Lanes:
|
||||
core test --suite smoke 67 passed
|
||||
core test --suite cognition 121 passed
|
||||
core test --suite teaching 17 passed
|
||||
core test --suite packs 6 passed
|
||||
core test --suite runtime 19 passed
|
||||
core test --suite algebra 132 passed
|
||||
```
|
||||
|
||||
The non-negotiable field invariant `versor_condition(F) < 1e-6` is
|
||||
unaffected — this ADR is a routing/dispatch change over immutable
|
||||
corpus data; no algebra, no field operators, no normalization sites
|
||||
were touched.
|
||||
|
|
@ -72,6 +72,7 @@ ADRs record significant architectural decisions: what was decided, why, what alt
|
|||
| [ADR-0060](ADR-0060-correction-acknowledgment-topic-lemma.md) | CORRECTION acknowledgement surface weaves the first pack-resident topical lemma from the utterance (left-to-right, excluding `correction` itself and `be`/`have` fillers) into a fixed template; backward-compatible with ADR-0053 (no-arg path byte-identical); closes `correction_truth_040` holdout miss; holdout `term_capture_rate` 75.0% → 79.2% | **Accepted** (2026-05-18) |
|
||||
| [ADR-0061](ADR-0061-procedure-intent-pack-grounded-surface.md) | PROCEDURE intent (`"How do I X?"`) routes to new `pack_grounded_procedure_surface`; selector picks **last** pack-resident lemma from verb-phrase subject (object > verb), falls back to verb when object is OOV, returns `None` (→ universal disclosure) for no-pack-lemma utterances; closes `procedure_define_010` (term `concept`) + `procedure_verify_034` (surface); holdout `surface_groundedness` 94.7% → 100.0%; `term_capture_rate` 79.2% → 83.3% | **Accepted** (2026-05-18) |
|
||||
| [ADR-0062](ADR-0062-composed-teaching-grounded-surface.md) | Composed teaching-grounded surface: when a chain `(A, intent_A, conn_A, B)` has a follow-up chain `(B, ?, conn_B, C)`, emit `"{A} {conn_A} {B}, which {conn_B} {C}"` instead of just `"{A} {conn_A} {B}"`; depth-1 (one hop) + cycle guard + pack-residency guard; degrades to single-chain byte-identically when no follow-up survives the guards; opt-in via `RuntimeConfig.composed_surface=False` default; cognition lane null-drop invariant (metrics byte-identical flag OFF/ON) CI-pinned | **Accepted** (2026-05-18) |
|
||||
| [ADR-0064](ADR-0064-cross-pack-teaching-chains.md) | Cross-pack teaching chains: `chat/teaching_grounding.py` registers a tuple of `TeachingCorpusSpec(corpus_id, path, pack_id)`; each corpus is 1:1-bound to one lexicon pack (cross-domain triples deferred per teaching_order.md §5); new `_all_chains_index()` aggregates across registered corpora (first-match-wins); surface composers + discovery gate consult the aggregated view; `TeachingChain` gains `corpus_id` field; surface tag follows the resolving corpus id; replay-equivalence gate rewrites registry path during transient phase; `relations_chains_v1` seeded with 7 reviewed kinship chains; cognition lane byte-identical | **Accepted** (2026-05-18) |
|
||||
| [ADR-0063](ADR-0063-cross-pack-surface-resolver.md) | Cross-pack surface resolver: `chat/pack_resolver.py` introduces `resolve_lemma(lemma, pack_ids)` that maps a lemma to `(resolving_pack_id, semantic_domains)` across an ordered tuple of mounted lexicon packs (first-match-wins); pack-grounded DEFINITION / RECALL / COMPARISON / CORRECTION / PROCEDURE composers now consult the resolver instead of a hardcoded `en_core_cognition_v1`; surface trust-boundary tag follows the resolving pack id; `en_core_relations_v1` joins `RuntimeConfig.input_packs` defaults — kinship lemmas now ground on the live path without a separate composer module; cognition-lane surfaces remain byte-identical (cognition is resolved first) | **Accepted** (2026-05-18) |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -361,16 +361,32 @@ def _scene5_replay_now_grounded(transient: Path) -> SceneResult:
|
|||
"narrative reveals meaning. Identical bytes for any replay of "
|
||||
"the same prompt against this corpus state.",
|
||||
)
|
||||
# ADR-0064 — the cognition corpus is one of several registered
|
||||
# teaching corpora; surface composers now consult
|
||||
# ``_all_chains_index`` instead of ``_corpus_index`` alone. We
|
||||
# rewrite the registry entry's path for the duration of the swap
|
||||
# and clear every teaching cache so the aggregator re-reads the
|
||||
# transient corpus.
|
||||
real_path = _tg._CORPUS_PATH
|
||||
original_specs = _tg.TEACHING_CORPORA
|
||||
swapped_specs = tuple(
|
||||
_tg.TeachingCorpusSpec(
|
||||
corpus_id=s.corpus_id,
|
||||
path=transient if s.corpus_id == _tg.TEACHING_CORPUS_ID else s.path,
|
||||
pack_id=s.pack_id,
|
||||
)
|
||||
for s in original_specs
|
||||
)
|
||||
try:
|
||||
_tg._CORPUS_PATH = transient # type: ignore[assignment]
|
||||
_tg._corpus_index.cache_clear()
|
||||
# Fresh runtime to avoid any per-instance state.
|
||||
_tg.TEACHING_CORPORA = swapped_specs # type: ignore[misc]
|
||||
_tg.clear_teaching_caches()
|
||||
rt2 = ChatRuntime()
|
||||
response = rt2.chat(_DEMO_PROMPT)
|
||||
finally:
|
||||
_tg._CORPUS_PATH = real_path # type: ignore[assignment]
|
||||
_tg._corpus_index.cache_clear()
|
||||
_tg.TEACHING_CORPORA = original_specs # type: ignore[misc]
|
||||
_tg.clear_teaching_caches()
|
||||
|
||||
surface = response.surface
|
||||
grounding = response.grounding_source
|
||||
|
|
|
|||
|
|
@ -261,15 +261,19 @@ def extract_discovery_candidates(
|
|||
if not lemma:
|
||||
return ()
|
||||
|
||||
from chat.pack_grounding import _pack_index
|
||||
from chat.teaching_grounding import _corpus_index
|
||||
from chat.pack_resolver import is_resolvable
|
||||
from chat.teaching_grounding import _all_chains_index
|
||||
|
||||
pack = _pack_index()
|
||||
if lemma not in pack:
|
||||
# ADR-0064 — discovery gate uses cross-pack residency (any mounted
|
||||
# lexicon pack) AND cross-corpus chain lookup (any registered
|
||||
# teaching corpus). A kinship CAUSE prompt whose subject is in
|
||||
# the relations pack but has no relations-chain in the active
|
||||
# corpus is now also a discovery signal.
|
||||
if not is_resolvable(lemma):
|
||||
return ()
|
||||
|
||||
intent_name = _TEACHING_INTENT_NAME[intent_tag]
|
||||
if (lemma, intent_name) in _corpus_index():
|
||||
if (lemma, intent_name) in _all_chains_index():
|
||||
return ()
|
||||
|
||||
# The candidate's proposed_chain is intentionally partial: Phase B
|
||||
|
|
|
|||
7
teaching/relations_chains/relations_chains_v1.jsonl
Normal file
7
teaching/relations_chains/relations_chains_v1.jsonl
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{"chain_id":"cause_parent_precedes_child","subject":"parent","intent":"cause","connective":"precedes","object":"child","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0064:reviewed:2026-05-18:relations_seed_v1"}
|
||||
{"chain_id":"cause_child_follows_parent","subject":"child","intent":"cause","connective":"follows","object":"parent","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0064:reviewed:2026-05-18:relations_seed_v1"}
|
||||
{"chain_id":"cause_ancestor_precedes_descendant","subject":"ancestor","intent":"cause","connective":"precedes","object":"descendant","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0064:reviewed:2026-05-18:relations_seed_v1"}
|
||||
{"chain_id":"cause_descendant_follows_ancestor","subject":"descendant","intent":"cause","connective":"follows","object":"ancestor","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0064:reviewed:2026-05-18:relations_seed_v1"}
|
||||
{"chain_id":"cause_family_grounds_parent","subject":"family","intent":"cause","connective":"grounds","object":"parent","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0064:reviewed:2026-05-18:relations_seed_v1"}
|
||||
{"chain_id":"verification_child_requires_parent","subject":"child","intent":"verification","connective":"requires","object":"parent","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0064:reviewed:2026-05-18:relations_seed_v1"}
|
||||
{"chain_id":"verification_descendant_requires_ancestor","subject":"descendant","intent":"verification","connective":"requires","object":"ancestor","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0064:reviewed:2026-05-18:relations_seed_v1"}
|
||||
|
|
@ -48,13 +48,28 @@ def _swap_corpus_path(temp_path: Path) -> Iterator[None]:
|
|||
corpus on disk is not touched.
|
||||
"""
|
||||
real_path = _tg._CORPUS_PATH
|
||||
# ADR-0064 — the cognition corpus is one of several registered
|
||||
# teaching corpora. When we swap it for replay, we must also
|
||||
# rewrite the registry entry's path AND invalidate the aggregated
|
||||
# index so surface composers re-read the swapped corpus.
|
||||
original_specs = _tg.TEACHING_CORPORA
|
||||
swapped_specs = tuple(
|
||||
_tg.TeachingCorpusSpec(
|
||||
corpus_id=s.corpus_id,
|
||||
path=temp_path if s.corpus_id == _tg.TEACHING_CORPUS_ID else s.path,
|
||||
pack_id=s.pack_id,
|
||||
)
|
||||
for s in original_specs
|
||||
)
|
||||
try:
|
||||
_tg._CORPUS_PATH = temp_path # type: ignore[assignment]
|
||||
_tg._corpus_index.cache_clear()
|
||||
_tg.TEACHING_CORPORA = swapped_specs # type: ignore[misc]
|
||||
_tg.clear_teaching_caches()
|
||||
yield
|
||||
finally:
|
||||
_tg._CORPUS_PATH = real_path # type: ignore[assignment]
|
||||
_tg._corpus_index.cache_clear()
|
||||
_tg.TEACHING_CORPORA = original_specs # type: ignore[misc]
|
||||
_tg.clear_teaching_caches()
|
||||
|
||||
|
||||
def _run_cognition_public() -> dict[str, float]:
|
||||
|
|
@ -113,9 +128,10 @@ def run_replay_equivalence(chain: dict[str, Any]) -> ReplayEvidence:
|
|||
active_path = _tg._CORPUS_PATH
|
||||
active_bytes_before = active_path.read_bytes() if active_path.exists() else b""
|
||||
|
||||
# Baseline: just run against the active corpus. Cache is cleared
|
||||
# to make sure we read the current state of disk.
|
||||
_tg._corpus_index.cache_clear()
|
||||
# Baseline: just run against the active corpus. Caches are
|
||||
# cleared to make sure we read the current state of disk for
|
||||
# every registered teaching corpus (ADR-0064).
|
||||
_tg.clear_teaching_caches()
|
||||
baseline = _run_cognition_public()
|
||||
|
||||
# Candidate: build a transient corpus with the chain appended
|
||||
|
|
|
|||
207
tests/test_relations_chains_v1.py
Normal file
207
tests/test_relations_chains_v1.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
"""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"
|
||||
)
|
||||
Loading…
Reference in a new issue