feat(adr-0063): cross-pack surface resolver — kinship lemmas ground on live path
ADR-0063 closes the ADR-0048/0050/0053/0061 hardcoded-cognition-pack asymmetry. New chat/pack_resolver.py provides resolve_lemma(lemma, pack_ids) → (resolving_pack_id, semantic_domains) across an ordered tuple of mounted lexicon packs (first-match-wins, lru_cache per-pack). Surface composers in chat/pack_grounding.py now consult the resolver instead of a hardcoded en_core_cognition_v1. en_core_relations_v1 joins RuntimeConfig.input_packs defaults; kinship lemmas now ground on the live path: > What is a parent? parent — pack-grounded (en_core_relations_v1): kinship.ascendant.direct; kinship.parent; biology.progenitor. No session evidence yet. Cross-pack comparison (knowledge × parent) renders composite tag (en_core_cognition_v1 × en_core_relations_v1). Cognition lane remains byte-identical: cognition is resolved first and the surface format for cognition lemmas is unchanged. 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% Curated lanes green: smoke 67 / cognition 121 / teaching 17 / packs 6 / runtime 19 / algebra 132. New tests: test_pack_resolver.py (28) + test_cross_pack_grounding.py (17). test_en_core_relations_v1_pack.py: default-input-packs guard inverted. test_pack_grounding.py: two stale ADR-0048 tests rewritten (premises invalidated by ADR-0052/0061; now use fully-out-of-pack prompts). chat/teaching_grounding.py UNCHANGED — cognition_chains_v1 corpus stays cognition-only. Cross-pack teaching corpora are the natural ADR-0064.
This commit is contained in:
parent
f0c57eb32e
commit
9f83b27a7c
10 changed files with 860 additions and 86 deletions
|
|
@ -38,6 +38,12 @@ import json
|
|||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from chat.pack_resolver import (
|
||||
DEFAULT_RESOLVABLE_PACK_IDS,
|
||||
mounted_lemmas,
|
||||
resolve_lemma,
|
||||
)
|
||||
|
||||
PACK_ID: str = "en_core_cognition_v1"
|
||||
|
||||
_PACK_LEXICON_PATH = (
|
||||
|
|
@ -77,44 +83,51 @@ def _pack_index() -> dict[str, tuple[str, ...]]:
|
|||
return out
|
||||
|
||||
|
||||
def pack_grounded_surface(lemma: str) -> str | None:
|
||||
def pack_grounded_surface(
|
||||
lemma: str,
|
||||
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
||||
) -> str | None:
|
||||
"""Return a deterministic pack-grounded surface for *lemma*, or ``None``.
|
||||
|
||||
The surface format is fixed:
|
||||
|
||||
"{lemma} — pack-grounded ({pack_id}): {d1}; {d2}; {d3}. No session evidence yet."
|
||||
"{lemma} — pack-grounded ({resolved_pack_id}): {d1}; {d2}; {d3}. No session evidence yet."
|
||||
|
||||
Only the lemma and up to three semantic_domains from the pack are
|
||||
emitted; both come directly from the ratified pack lexicon, with no
|
||||
rewording. The trailing disclosure is the constant trust-boundary
|
||||
label that distinguishes pack-grounded surfaces from vault-grounded
|
||||
surfaces (which would carry session evidence) and from the universal
|
||||
"insufficient grounding" disclosure (which carries neither).
|
||||
ADR-0063 — *resolved_pack_id* is the pack in *pack_ids* whose lexicon
|
||||
contained *lemma* (first-match-wins). Cognition lemmas keep emitting
|
||||
``pack-grounded (en_core_cognition_v1)`` byte-identically; kinship
|
||||
lemmas emit ``pack-grounded (en_core_relations_v1)``.
|
||||
|
||||
Only the lemma and up to three semantic_domains from the resolved
|
||||
pack are emitted; both come directly from the ratified pack lexicon,
|
||||
with no rewording.
|
||||
|
||||
Returns ``None`` when:
|
||||
- the lemma is empty or not a string,
|
||||
- the pack lexicon file is unavailable,
|
||||
- the lemma is not present in the pack,
|
||||
- the pack entry has no ``semantic_domains``.
|
||||
- the lemma does not resolve in any of *pack_ids*.
|
||||
"""
|
||||
if not lemma or not isinstance(lemma, str):
|
||||
resolved = resolve_lemma(lemma, pack_ids)
|
||||
if resolved is None:
|
||||
return None
|
||||
resolved_pack_id, domains = resolved
|
||||
key = lemma.strip().lower()
|
||||
if not key:
|
||||
return None
|
||||
index = _pack_index()
|
||||
domains = index.get(key)
|
||||
if not domains:
|
||||
return None
|
||||
head = "; ".join(domains[:3])
|
||||
return (
|
||||
f"{key} — pack-grounded ({PACK_ID}): {head}. "
|
||||
f"{key} — pack-grounded ({resolved_pack_id}): {head}. "
|
||||
f"No session evidence yet."
|
||||
)
|
||||
|
||||
|
||||
def is_pack_lemma(lemma: str) -> bool:
|
||||
"""Return True iff *lemma* has an entry with ``semantic_domains`` in the pack."""
|
||||
"""Return True iff *lemma* has an entry with ``semantic_domains`` in the
|
||||
ratified cognition pack (``en_core_cognition_v1``).
|
||||
|
||||
Cognition-pack-specific helper retained for back-compat with the
|
||||
cognition-corpus modules (discovery, contemplation, teaching
|
||||
chains) whose semantics are scoped to the cognition pack. For
|
||||
cross-pack residency checks, use
|
||||
:func:`chat.pack_resolver.is_resolvable`.
|
||||
"""
|
||||
if not lemma or not isinstance(lemma, str):
|
||||
return False
|
||||
return lemma.strip().lower() in _pack_index()
|
||||
|
|
@ -133,24 +146,27 @@ _CORRECTION_TOPIC_STOPWORDS: frozenset[str] = frozenset({
|
|||
|
||||
|
||||
def _extract_correction_topic_lemma(text: str) -> str | None:
|
||||
"""Return the first pack-resident, topical lemma in *text*, or None.
|
||||
"""Return the first mounted-pack-resident, topical lemma in *text*, or None.
|
||||
|
||||
Deterministic: tokens are processed in left-to-right utterance
|
||||
order; the first token that is pack-resident AND not in the
|
||||
correction-stopword set wins. Stopwords filter out the meta-
|
||||
cognition lemma itself (``correction``) and dialogue fillers
|
||||
order; the first token that is resident in any mounted pack AND not
|
||||
in the correction-stopword set wins. Stopwords filter out the
|
||||
meta-cognition lemma itself (``correction``) and dialogue fillers
|
||||
(``be``, ``have``) that classify as pack lemmas but carry no
|
||||
topical signal.
|
||||
|
||||
Used by ``pack_grounded_correction_surface`` to weave the
|
||||
ADR-0063 — residency is checked across all mounted lexicon packs
|
||||
(see :data:`chat.pack_resolver.DEFAULT_RESOLVABLE_PACK_IDS`), so a
|
||||
kinship correction (``"No, my parent disagrees"``) anchors the
|
||||
acknowledgement on the kinship topic.
|
||||
|
||||
Used by :func:`pack_grounded_correction_surface` to weave the
|
||||
corrected claim's subject into the acknowledgement template.
|
||||
"""
|
||||
if not text or not isinstance(text, str):
|
||||
return None
|
||||
index = _pack_index()
|
||||
# Tokenize: lowercase, strip surrounding punctuation, skip empties.
|
||||
lemmas = mounted_lemmas()
|
||||
raw = text.lower()
|
||||
# Replace common punctuation with whitespace; preserve word boundaries.
|
||||
for ch in ",.;:!?\"'()[]{}":
|
||||
raw = raw.replace(ch, " ")
|
||||
for token in raw.split():
|
||||
|
|
@ -158,7 +174,7 @@ def _extract_correction_topic_lemma(text: str) -> str | None:
|
|||
continue
|
||||
if token in _CORRECTION_TOPIC_STOPWORDS:
|
||||
continue
|
||||
if token in index:
|
||||
if token in lemmas:
|
||||
return token
|
||||
return None
|
||||
|
||||
|
|
@ -208,7 +224,12 @@ def pack_grounded_correction_surface(text: str | None = None) -> str | None:
|
|||
head = "; ".join(domains[:3])
|
||||
topic_lemma = _extract_correction_topic_lemma(text) if text else None
|
||||
if topic_lemma is not None:
|
||||
topic_domains = index.get(topic_lemma, ())
|
||||
# ADR-0063 — topic_lemma may resolve in a non-cognition pack
|
||||
# (e.g. ``parent`` in en_core_relations_v1). Anchor pack stays
|
||||
# cognition (``correction`` is a cognition lemma), topic domains
|
||||
# come from whichever pack resolves the topic.
|
||||
topic_resolved = resolve_lemma(topic_lemma)
|
||||
topic_domains = topic_resolved[1] if topic_resolved is not None else ()
|
||||
topic_head = "; ".join(topic_domains[:2]) if topic_domains else ""
|
||||
if topic_head:
|
||||
return (
|
||||
|
|
@ -257,7 +278,7 @@ def _extract_procedure_topic_lemma(subject_text: str) -> str | None:
|
|||
"""
|
||||
if not subject_text or not isinstance(subject_text, str):
|
||||
return None
|
||||
index = _pack_index()
|
||||
lemmas = mounted_lemmas()
|
||||
raw = subject_text.lower()
|
||||
for ch in ",.;:!?\"'()[]{}":
|
||||
raw = raw.replace(ch, " ")
|
||||
|
|
@ -267,7 +288,7 @@ def _extract_procedure_topic_lemma(subject_text: str) -> str | None:
|
|||
continue
|
||||
if token in _PROCEDURE_TOPIC_STOPWORDS:
|
||||
continue
|
||||
if token in index:
|
||||
if token in lemmas:
|
||||
last_match = token
|
||||
return last_match
|
||||
|
||||
|
|
@ -304,13 +325,17 @@ def pack_grounded_procedure_surface(subject_text: str) -> str | None:
|
|||
lemma = _extract_procedure_topic_lemma(subject_text)
|
||||
if lemma is None:
|
||||
return None
|
||||
index = _pack_index()
|
||||
domains = index.get(lemma, ())
|
||||
if not domains:
|
||||
# ADR-0063 — resolve topic across all mounted lexicon packs. The
|
||||
# surface tag follows the resolving pack id so a kinship procedure
|
||||
# (``"How do I trace my ancestor?"``) emits
|
||||
# ``procedure-grounded (en_core_relations_v1)``.
|
||||
resolved = resolve_lemma(lemma)
|
||||
if resolved is None:
|
||||
return None
|
||||
resolved_pack_id, domains = resolved
|
||||
head = "; ".join(domains[:2])
|
||||
return (
|
||||
f"procedure-grounded ({PACK_ID}): {lemma} ({head}). "
|
||||
f"procedure-grounded ({resolved_pack_id}): {lemma} ({head}). "
|
||||
f"Step-by-step guidance for {lemma} is not yet ratified in this session."
|
||||
)
|
||||
|
||||
|
|
@ -350,14 +375,22 @@ def pack_grounded_comparison_surface(
|
|||
return None
|
||||
if key_a == key_b:
|
||||
return None
|
||||
index = _pack_index()
|
||||
domains_a = index.get(key_a)
|
||||
domains_b = index.get(key_b)
|
||||
if not domains_a or not domains_b:
|
||||
resolved_a = resolve_lemma(key_a)
|
||||
resolved_b = resolve_lemma(key_b)
|
||||
if resolved_a is None or resolved_b is None:
|
||||
return None
|
||||
pack_a, domains_a = resolved_a
|
||||
pack_b, domains_b = resolved_b
|
||||
head_a = "; ".join(domains_a[:2])
|
||||
head_b = "; ".join(domains_b[:2])
|
||||
# ADR-0063 — tag follows the resolving pack ids. Cognition-only
|
||||
# comparisons stay byte-identical (both sides resolve to cognition);
|
||||
# cross-pack comparisons render the composite tag explicitly.
|
||||
if pack_a == pack_b:
|
||||
tag = f"pack-grounded ({pack_a})"
|
||||
else:
|
||||
tag = f"pack-grounded ({pack_a} × {pack_b})"
|
||||
return (
|
||||
f"{key_a} ({head_a}) contrasts with {key_b} ({head_b}) "
|
||||
f"— pack-grounded ({PACK_ID}). No session evidence yet."
|
||||
f"— {tag}. No session evidence yet."
|
||||
)
|
||||
|
|
|
|||
136
chat/pack_resolver.py
Normal file
136
chat/pack_resolver.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"""chat/pack_resolver.py — ADR-0063 cross-pack surface resolver.
|
||||
|
||||
The cold-start grounding composers in :mod:`chat.pack_grounding` were
|
||||
hardcoded to a single lexicon pack (``en_core_cognition_v1``). That
|
||||
asymmetry blocked ``en_core_relations_v1`` from being mounted on the
|
||||
default runtime: mounting it would silently widen vault recall and
|
||||
intent classification without a corresponding ratified surface
|
||||
composer for kinship lemmas.
|
||||
|
||||
This module supplies the missing abstraction: a *deterministic*,
|
||||
*first-match-wins* resolver that maps a lemma to ``(pack_id, semantic_domains)``
|
||||
across an ordered tuple of mounted lexicon packs. Surface composers
|
||||
consult the resolver instead of a single pack; the surface trust-boundary
|
||||
tag follows the *resolving* pack id.
|
||||
|
||||
Design constraints (CLAUDE.md / Reconstruction-over-storage):
|
||||
|
||||
- The resolver loads each pack lexicon at most once per process via
|
||||
:func:`functools.lru_cache`. Ratified packs are immutable, so caching
|
||||
is sound.
|
||||
- A pack that fails to load (missing file, unreadable JSON) contributes
|
||||
an empty index — callers cannot distinguish "pack absent" from "lemma
|
||||
absent in mounted packs". Both produce ``None``.
|
||||
- First-match-wins on collision. The orthogonality test in
|
||||
``tests/test_en_core_relations_v1_pack.py`` enforces zero overlap
|
||||
today; this rule documents the deterministic resolution if a future
|
||||
pack pair collides.
|
||||
- No mutation of pack data is performed anywhere in this module. All
|
||||
return types are tuples/frozensets/dicts of plain strings.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
# Default mounted lexicon-pack ids that ADR-0063 surface composers
|
||||
# consult. Order matters: earlier packs win on lemma collision. This
|
||||
# tuple is intentionally narrow — it lists only ratified ``en_core_*``
|
||||
# *content* packs. Identity/safety/ethics packs are propositional
|
||||
# overlays and never carry vocabulary; they are deliberately excluded.
|
||||
DEFAULT_RESOLVABLE_PACK_IDS: tuple[str, ...] = (
|
||||
"en_core_cognition_v1",
|
||||
"en_core_relations_v1",
|
||||
)
|
||||
|
||||
_PACK_ROOT = Path(__file__).resolve().parent.parent / "language_packs" / "data"
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _pack_lexicon_for(pack_id: str) -> dict[str, tuple[str, ...]]:
|
||||
"""Return ``{lemma_lower: semantic_domains}`` for *pack_id*, cached.
|
||||
|
||||
Returns an empty dict if the pack's lexicon file is missing,
|
||||
unreadable, or contains no entries with ``semantic_domains``.
|
||||
Ratified packs are immutable so the cache survives the process
|
||||
lifetime.
|
||||
"""
|
||||
lexicon_path = _PACK_ROOT / pack_id / "lexicon.jsonl"
|
||||
if not lexicon_path.exists():
|
||||
return {}
|
||||
out: dict[str, tuple[str, ...]] = {}
|
||||
for line in lexicon_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
|
||||
lemma = entry.get("lemma") or entry.get("surface")
|
||||
if not lemma or not isinstance(lemma, str):
|
||||
continue
|
||||
domains = entry.get("semantic_domains", ())
|
||||
if not isinstance(domains, (list, tuple)) or not domains:
|
||||
continue
|
||||
out[lemma.lower()] = tuple(str(d) for d in domains)
|
||||
return out
|
||||
|
||||
|
||||
def resolve_lemma(
|
||||
lemma: str,
|
||||
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
||||
) -> tuple[str, tuple[str, ...]] | None:
|
||||
"""Return ``(pack_id, semantic_domains)`` for the first pack in
|
||||
*pack_ids* whose lexicon contains *lemma*, else ``None``.
|
||||
|
||||
First-match-wins on collision. ``pack_ids`` defaults to
|
||||
:data:`DEFAULT_RESOLVABLE_PACK_IDS`.
|
||||
"""
|
||||
if not lemma or not isinstance(lemma, str):
|
||||
return None
|
||||
key = lemma.strip().lower()
|
||||
if not key:
|
||||
return None
|
||||
for pack_id in pack_ids:
|
||||
index = _pack_lexicon_for(pack_id)
|
||||
domains = index.get(key)
|
||||
if domains:
|
||||
return (pack_id, domains)
|
||||
return None
|
||||
|
||||
|
||||
def is_resolvable(
|
||||
lemma: str,
|
||||
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
||||
) -> bool:
|
||||
"""Return True iff *lemma* resolves in any of *pack_ids*."""
|
||||
return resolve_lemma(lemma, pack_ids) is not None
|
||||
|
||||
|
||||
def mounted_lemmas(
|
||||
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
||||
) -> frozenset[str]:
|
||||
"""Return the frozen union of lemmas across *pack_ids*.
|
||||
|
||||
Used by topic extractors that iterate over user-text tokens and
|
||||
need an O(1) "is this token any mounted lemma?" check.
|
||||
"""
|
||||
out: set[str] = set()
|
||||
for pack_id in pack_ids:
|
||||
out.update(_pack_lexicon_for(pack_id).keys())
|
||||
return frozenset(out)
|
||||
|
||||
|
||||
def clear_resolver_cache() -> None:
|
||||
"""Drop the lru_cache for :func:`_pack_lexicon_for`.
|
||||
|
||||
Test-only escape hatch: enables fixture-based pack mutation
|
||||
scenarios. Production code never calls this — ratified packs are
|
||||
immutable.
|
||||
"""
|
||||
_pack_lexicon_for.cache_clear()
|
||||
|
|
@ -5,9 +5,15 @@ from dataclasses import dataclass
|
|||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeConfig:
|
||||
# ADR-0063 — ``en_core_relations_v1`` (kinship starter pack) joins the
|
||||
# default mount once the cross-pack surface resolver lands. Pack
|
||||
# composers in :mod:`chat.pack_grounding` now consult
|
||||
# :mod:`chat.pack_resolver`, so kinship lemmas ground deterministically
|
||||
# without a separate composer module.
|
||||
input_packs: tuple[str, ...] = (
|
||||
"en_minimal_v1",
|
||||
"en_core_cognition_v1",
|
||||
"en_core_relations_v1",
|
||||
"he_logos_micro_v1",
|
||||
"grc_logos_micro_v1",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
**Author:** Shay
|
||||
**Pack ID:** `en_core_relations_v1`
|
||||
**Lemma count:** 8 (kinship-only; deliberately tight)
|
||||
**Status:** Ratified (checksum verified). **Not yet mounted** on the default runtime.
|
||||
**Status:** Ratified (checksum verified). **Mounted on the default runtime** as of ADR-0063 (`chat/pack_resolver.py`) — cross-pack surface composers ground kinship lemmas deterministically on the live path.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -86,30 +86,37 @@ follow-up ADR — there is no `relations_chains_v1.jsonl` yet).
|
|||
|
||||
---
|
||||
|
||||
## Engagement (opt-in only)
|
||||
## Engagement (default mount as of ADR-0063)
|
||||
|
||||
The pack is **not** in the default `RuntimeConfig.input_packs`.
|
||||
Mounting it changes the runtime's mounted manifold (89 → 93
|
||||
lemmas in the cognition+relations combination). Until cross-pack
|
||||
teaching-grounded composition exists, mounting the relations
|
||||
pack would expose lemmas to vault recall and intent classification
|
||||
without a corresponding ratified surface composer for them. That
|
||||
asymmetry would silently lower the deterministic-grounding
|
||||
fraction of any kinship prompt.
|
||||
The pack is in the default `RuntimeConfig.input_packs` as of
|
||||
ADR-0063 (cross-pack surface resolver). Mounting changes the
|
||||
runtime's mounted manifold (cognition+relations combination) but the
|
||||
pack-grounded surface composers in `chat/pack_grounding.py` now
|
||||
consult `chat/pack_resolver.py` for cross-pack lemma residency — so
|
||||
kinship lemmas ground on the live path:
|
||||
|
||||
To opt in for development:
|
||||
```
|
||||
> What is a parent?
|
||||
parent — pack-grounded (en_core_relations_v1): kinship.ascendant.direct;
|
||||
kinship.parent; biology.progenitor. No session evidence yet.
|
||||
```
|
||||
|
||||
For development with the pack disabled, opt out:
|
||||
|
||||
```python
|
||||
from core.config import RuntimeConfig
|
||||
from chat.runtime import ChatRuntime
|
||||
|
||||
cfg = RuntimeConfig(input_packs=RuntimeConfig().input_packs + ("en_core_relations_v1",))
|
||||
# Opt-out of the default mount (development only):
|
||||
defaults = RuntimeConfig().input_packs
|
||||
cfg = RuntimeConfig(
|
||||
input_packs=tuple(p for p in defaults if p != "en_core_relations_v1")
|
||||
)
|
||||
rt = ChatRuntime(config=cfg)
|
||||
```
|
||||
|
||||
Programmatic mounting works today. CLI-level support
|
||||
(`core chat --pack en_core_relations_v1`) is already in place via
|
||||
the `--pack` flag (see `core chat --help`).
|
||||
For full production use, no override is needed — the pack is mounted by
|
||||
default and the cross-pack resolver finds kinship lemmas automatically.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -138,13 +145,14 @@ data + a contract test. No runtime code path changed.
|
|||
|
||||
## Path forward (future ADRs in priority order)
|
||||
|
||||
1. **Cross-pack teaching-grounded composition.**
|
||||
`chat/pack_grounding.py` and `chat/teaching_grounding.py` are
|
||||
currently hardcoded to `en_core_cognition_v1`. A
|
||||
`pack_resolver` abstraction that picks the right pack(s) for a
|
||||
given intent/subject is the natural unlock — composes the
|
||||
relations pack into the live surface path without a separate
|
||||
composer module.
|
||||
1. **Cross-pack teaching-grounded composition.** ✅ **Landed (ADR-0063).**
|
||||
`chat/pack_resolver.py` provides `resolve_lemma(lemma, pack_ids) →
|
||||
(resolving_pack_id, semantic_domains)`. Surface composers in
|
||||
`chat/pack_grounding.py` consult the resolver; kinship lemmas
|
||||
ground on the live path without a separate composer module.
|
||||
`chat/teaching_grounding.py` still references the cognition-chains
|
||||
corpus only — extending it to a relations-chains corpus is the
|
||||
natural next ADR.
|
||||
|
||||
2. **First kinship reviewed chains** — a
|
||||
`relations_chains_v1.jsonl` or extension of the cognition
|
||||
|
|
|
|||
265
docs/decisions/ADR-0063-cross-pack-surface-resolver.md
Normal file
265
docs/decisions/ADR-0063-cross-pack-surface-resolver.md
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
# ADR-0063 — Cross-pack surface resolver
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-18
|
||||
**Author:** Shay
|
||||
**Supersedes:** none (extends ADR-0048 / ADR-0050 / ADR-0052 / ADR-0053 / ADR-0060 / ADR-0061 / ADR-0062)
|
||||
**Builds on:** the kinship starter pack `en_core_relations_v1` (commit `f0c57eb`).
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0048 introduced the first pack-grounded surface composer — a
|
||||
deterministic cold-start surface for `DEFINITION` / `RECALL` intents
|
||||
whose subject lemma is present in the ratified cognition pack. ADR-0050
|
||||
extended that to `COMPARISON`. ADR-0053 + ADR-0060 added a `CORRECTION`
|
||||
acknowledgement composer (cold-start branch). ADR-0061 added a
|
||||
`PROCEDURE` composer. All four composers shared a hardcoded reference
|
||||
to a single lexicon pack:
|
||||
|
||||
```python
|
||||
# chat/pack_grounding.py — pre-ADR-0063
|
||||
PACK_ID: str = "en_core_cognition_v1"
|
||||
```
|
||||
|
||||
That asymmetry was the rate-limiter on domain expansion. Mounting the
|
||||
`en_core_relations_v1` kinship starter pack (already ratified —
|
||||
checksum `1a013b46…`) would silently widen the runtime's mounted
|
||||
manifold (vault recall + intent classification) without a corresponding
|
||||
surface composer for kinship lemmas. The cold-start
|
||||
`What is a parent?` prompt would fall through to the universal
|
||||
"insufficient grounding" disclosure even though `parent` is in a
|
||||
ratified, immutable pack on disk.
|
||||
|
||||
The previous ADR's memory recorded this explicitly:
|
||||
|
||||
> Engagement status: OPT-IN ONLY. Pack is NOT in
|
||||
> `RuntimeConfig.input_packs` defaults… until cross-pack
|
||||
> teaching-grounded composition exists, mounting silently exposes
|
||||
> lemmas to vault recall and intent classification without a
|
||||
> corresponding ratified surface composer.
|
||||
|
||||
ADR-0063 is the **one-PR architectural unlock** that closes that gap:
|
||||
the surface composers consult an abstract *resolver* across all
|
||||
mounted lexicon packs instead of one hardcoded pack, and the relations
|
||||
pack joins the default mount.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. New abstraction: `chat/pack_resolver.py`
|
||||
|
||||
A small (~140 lines) pure module that maps a lemma to a
|
||||
`(resolving_pack_id, semantic_domains)` pair across an ordered tuple of
|
||||
mounted lexicon packs:
|
||||
|
||||
```python
|
||||
DEFAULT_RESOLVABLE_PACK_IDS: tuple[str, ...] = (
|
||||
"en_core_cognition_v1",
|
||||
"en_core_relations_v1",
|
||||
)
|
||||
|
||||
def resolve_lemma(
|
||||
lemma: str,
|
||||
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
||||
) -> tuple[str, tuple[str, ...]] | None: ...
|
||||
|
||||
def is_resolvable(lemma, pack_ids=...) -> bool: ...
|
||||
def mounted_lemmas(pack_ids=...) -> frozenset[str]: ...
|
||||
```
|
||||
|
||||
Properties:
|
||||
|
||||
- **First-match-wins.** Order matters: cognition is listed first so
|
||||
that any future cross-pack collision resolves cognition-side,
|
||||
preserving cognition-lane byte-identity.
|
||||
- **lru_cache per-pack.** Each pack's `lexicon.jsonl` is loaded at most
|
||||
once per process (ratified packs are immutable).
|
||||
- **No mutation.** Returns tuples, frozensets, plain strings.
|
||||
- **Pure.** No I/O outside the cached read; no network; no LLM; no
|
||||
approximation.
|
||||
|
||||
### 2. Surface composers consult the resolver
|
||||
|
||||
`chat/pack_grounding.py`:
|
||||
|
||||
- `pack_grounded_surface(lemma)` — surface tag follows the resolving
|
||||
pack id. Cognition lemmas keep emitting
|
||||
`pack-grounded (en_core_cognition_v1)` byte-identically; kinship
|
||||
lemmas emit `pack-grounded (en_core_relations_v1)`.
|
||||
- `pack_grounded_comparison_surface(a, b)` — each side resolved
|
||||
independently. When both resolve to the same pack, the tag stays
|
||||
single (`pack-grounded (en_core_cognition_v1)`). When the two
|
||||
resolve to different packs, the tag becomes composite
|
||||
(`pack-grounded (en_core_cognition_v1 × en_core_relations_v1)`).
|
||||
- `pack_grounded_correction_surface(text)` — anchor pack stays
|
||||
cognition (`correction` is a cognition lemma); topic domains come
|
||||
from whichever pack resolves the topic lemma. Topic extraction
|
||||
scans `mounted_lemmas()` instead of the cognition-only index.
|
||||
- `pack_grounded_procedure_surface(subject_text)` — topic extraction
|
||||
scans `mounted_lemmas()`; resolving pack id drives the surface tag.
|
||||
|
||||
`chat/teaching_grounding.py` is **unchanged** in this ADR. The
|
||||
reviewed teaching corpus (`cognition_chains_v1.jsonl`) still
|
||||
references cognition-pack lemmas only. Cross-pack teaching chains
|
||||
(e.g. a `relations_chains_v1.jsonl`) are the natural next ADR.
|
||||
|
||||
### 3. Backward compatibility
|
||||
|
||||
- `chat/pack_grounding.py:PACK_ID = "en_core_cognition_v1"` and
|
||||
`_pack_index()` (cognition-only) are retained for back-compat with
|
||||
consumers in `teaching/contemplation.py`, `teaching/discovery.py`,
|
||||
`chat/teaching_grounding.py`, and various tests whose semantics are
|
||||
scoped to the cognition pack. They are NOT removed — only the
|
||||
composers' internal lookups switch to the resolver.
|
||||
- `is_pack_lemma(lemma)` keeps its cognition-only semantics; cross-pack
|
||||
residency is `chat.pack_resolver.is_resolvable`.
|
||||
|
||||
### 4. Default mount adds the relations pack
|
||||
|
||||
`core/config.py`:
|
||||
|
||||
```python
|
||||
input_packs: tuple[str, ...] = (
|
||||
"en_minimal_v1",
|
||||
"en_core_cognition_v1",
|
||||
"en_core_relations_v1", # added in ADR-0063
|
||||
"he_logos_micro_v1",
|
||||
"grc_logos_micro_v1",
|
||||
)
|
||||
```
|
||||
|
||||
Mounting is now safe because the composers ground kinship lemmas
|
||||
deterministically.
|
||||
|
||||
### 5. The relations-pack default-input test inverts
|
||||
|
||||
`tests/test_en_core_relations_v1_pack.py`:
|
||||
- Before: `test_pack_is_not_in_default_input_packs`
|
||||
- After: `test_pack_is_in_default_input_packs`
|
||||
|
||||
The earlier test was an explicit *guard against premature mounting*.
|
||||
It guarded the asymmetry that ADR-0063 closes; its inverse is now the
|
||||
correct invariant.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Capability unlocked
|
||||
|
||||
| Surface composer | Cognition lemmas | Kinship lemmas |
|
||||
|---|---|---|
|
||||
| `pack_grounded_surface` (DEFINITION / RECALL) | byte-identical | **now grounds** |
|
||||
| `pack_grounded_comparison_surface` (COMPARISON) | byte-identical when both cognition | **now grounds** kinship × kinship; composite tag for cross-pack |
|
||||
| `pack_grounded_correction_surface` (CORRECTION) | byte-identical when topic ∈ cognition | **now anchors topic** on kinship lemmas |
|
||||
| `pack_grounded_procedure_surface` (PROCEDURE) | byte-identical when topic ∈ cognition | **now grounds** kinship topics |
|
||||
|
||||
### Cognition lane: byte-identical
|
||||
|
||||
The cognition lane's prompts use only cognition lemmas. The resolver
|
||||
picks cognition first, the surface tag stays `en_core_cognition_v1`,
|
||||
the surface bytes are identical:
|
||||
|
||||
```
|
||||
core eval cognition → 100/100/91.7/100 (public)
|
||||
core eval cognition --split holdout → 100/100/83.3/100 (holdout)
|
||||
```
|
||||
|
||||
Both byte-identical to the pre-ADR baseline.
|
||||
|
||||
### Live-path grounding on kinship lemmas
|
||||
|
||||
```text
|
||||
$ core chat
|
||||
> What is a parent?
|
||||
parent — pack-grounded (en_core_relations_v1): kinship.ascendant.direct;
|
||||
kinship.parent; biology.progenitor. No session evidence yet.
|
||||
grounding_source = pack
|
||||
```
|
||||
|
||||
### Future ADRs unlocked
|
||||
|
||||
1. **ADR-0064 — Cross-pack teaching chains.** Add
|
||||
`relations_chains_v1.jsonl` (reviewed kinship chains) and extend
|
||||
`chat/teaching_grounding.py` to consult both corpora. The natural
|
||||
next ADR.
|
||||
2. **Pronoun + role-filler v2.** `mother`, `father`, `son`, etc. as
|
||||
specializations of the v1 kinship primitives — extends
|
||||
`en_core_relations_v1` itself.
|
||||
3. **Cross-domain triples.** `family causes belonging`,
|
||||
`parent grounds identity` — opens after cognition + relations both
|
||||
have ratified internal DAGs of reviewed chains.
|
||||
|
||||
---
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
- The resolver does NOT execute pack metadata, validators, or scripts.
|
||||
It reads `lexicon.jsonl` only.
|
||||
- A pack that fails to load produces an empty index — callers see
|
||||
`None`. No exception leaks into the runtime.
|
||||
- No mutation site introduced. The resolver is read-only over
|
||||
immutable, ratified pack data.
|
||||
- Tag tokens emitted in surfaces are always one of:
|
||||
`en_core_cognition_v1`, `en_core_relations_v1`, or the composite
|
||||
`{a} × {b}` form derived from mounted-pack ids — never from user
|
||||
input.
|
||||
- `pack_resolver.clear_resolver_cache()` is a test-only escape hatch;
|
||||
production paths never call it.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
tests/test_pack_resolver.py 28 passed
|
||||
tests/test_cross_pack_grounding.py 17 passed
|
||||
tests/test_pack_grounding.py 13 passed
|
||||
tests/test_pack_grounded_comparison.py passed
|
||||
tests/test_pack_grounded_correction.py passed
|
||||
tests/test_correction_topic_lemma.py passed
|
||||
tests/test_procedure_surface.py passed
|
||||
tests/test_teaching_grounding.py passed
|
||||
tests/test_composed_surface.py 11 passed
|
||||
tests/test_en_core_relations_v1_pack.py 6 passed (inverted)
|
||||
|
||||
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
|
||||
core test --suite full <pending — see PR>
|
||||
|
||||
Cognition eval (byte-identical to pre-ADR baseline):
|
||||
public split → intent 100% / surface 100% / term 91.7% / closure 100%
|
||||
holdout split → intent 100% / surface 100% / term 83.3% / closure 100%
|
||||
```
|
||||
|
||||
The non-negotiable field invariant `versor_condition(F) < 1e-6` is
|
||||
unaffected — this ADR is a routing/dispatch change over immutable
|
||||
pack data; no algebra, no field operators, no normalization sites
|
||||
were touched.
|
||||
|
||||
---
|
||||
|
||||
## Files changed
|
||||
|
||||
```
|
||||
chat/pack_resolver.py NEW (~140 lines)
|
||||
chat/pack_grounding.py composers use resolver
|
||||
core/config.py relations pack joins defaults
|
||||
tests/test_pack_resolver.py NEW (28 tests)
|
||||
tests/test_cross_pack_grounding.py NEW (17 tests)
|
||||
tests/test_pack_grounding.py 2 stale tests rewritten
|
||||
tests/test_en_core_relations_v1_pack.py default-input test inverted
|
||||
docs/decisions/ADR-0063-cross-pack-surface-resolver.md NEW (this file)
|
||||
docs/decisions/README.md ADR-0063 index entry
|
||||
```
|
||||
|
||||
Lines of net change: small. The architectural unlock is in
|
||||
`chat/pack_resolver.py`; the rest of the diff is wiring + tests.
|
||||
|
|
@ -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-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) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
148
tests/test_cross_pack_grounding.py
Normal file
148
tests/test_cross_pack_grounding.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"""ADR-0063 — cross-pack surface grounding integration tests.
|
||||
|
||||
These tests pin the live-path behaviour the cross-pack surface resolver
|
||||
unlocks: with ``en_core_relations_v1`` joining the default mount, the
|
||||
pack-grounded DEFINITION / RECALL / COMPARISON / CORRECTION / PROCEDURE
|
||||
composers all engage on kinship lemmas the same way they already engage
|
||||
on cognition lemmas.
|
||||
|
||||
The trust-boundary tag in the emitted surface follows the *resolving*
|
||||
pack id — cognition lemmas still emit ``pack-grounded
|
||||
(en_core_cognition_v1)`` byte-identically, kinship lemmas emit
|
||||
``pack-grounded (en_core_relations_v1)``.
|
||||
|
||||
Cognition lane invariants (covered elsewhere) must remain unchanged:
|
||||
the only surfaces that move are the ones whose subject lemma is a
|
||||
kinship token.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from chat.pack_grounding import (
|
||||
pack_grounded_comparison_surface,
|
||||
pack_grounded_correction_surface,
|
||||
pack_grounded_procedure_surface,
|
||||
pack_grounded_surface,
|
||||
)
|
||||
from chat.runtime import ChatRuntime
|
||||
from core.config import RuntimeConfig
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure-function surface composers — kinship lemmas now ground
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lemma",
|
||||
["parent", "child", "sibling", "family",
|
||||
"ancestor", "descendant", "spouse", "offspring"],
|
||||
)
|
||||
def test_pack_grounded_surface_resolves_kinship_lemmas(lemma: str) -> None:
|
||||
"""Every kinship lemma now produces a deterministic pack-grounded
|
||||
surface tagged with the resolving pack id."""
|
||||
surface = pack_grounded_surface(lemma)
|
||||
assert surface is not None, f"{lemma!r} did not surface"
|
||||
assert lemma in surface
|
||||
assert "pack-grounded (en_core_relations_v1)" in surface
|
||||
assert "No session evidence yet." in surface
|
||||
|
||||
|
||||
def test_cognition_surface_is_byte_identical_after_resolver() -> None:
|
||||
"""ADR-0063 must not change the surface for cognition-pack lemmas
|
||||
— the resolver picks cognition first, the pack tag remains
|
||||
``en_core_cognition_v1``."""
|
||||
surface = pack_grounded_surface("light")
|
||||
assert surface is not None
|
||||
assert "pack-grounded (en_core_cognition_v1)" in surface
|
||||
assert "en_core_relations_v1" not in surface
|
||||
|
||||
|
||||
def test_comparison_cross_pack_renders_composite_tag() -> None:
|
||||
"""A kinship × cognition comparison emits the composite pack tag
|
||||
``(en_core_cognition_v1 × en_core_relations_v1)`` — first side's
|
||||
resolution first."""
|
||||
surface = pack_grounded_comparison_surface("knowledge", "parent")
|
||||
assert surface is not None
|
||||
assert "knowledge" in surface
|
||||
assert "parent" in surface
|
||||
assert "contrasts with" in surface
|
||||
assert "en_core_cognition_v1 × en_core_relations_v1" in surface
|
||||
|
||||
|
||||
def test_comparison_cognition_only_is_byte_identical() -> None:
|
||||
"""A cognition × cognition comparison must keep the single-pack
|
||||
tag — adding the resolver did not regress the existing surface."""
|
||||
surface = pack_grounded_comparison_surface("knowledge", "truth")
|
||||
assert surface is not None
|
||||
assert "pack-grounded (en_core_cognition_v1)" in surface
|
||||
assert "×" not in surface
|
||||
|
||||
|
||||
def test_procedure_surface_resolves_kinship_topic() -> None:
|
||||
"""A procedure verb-phrase whose topical lemma resolves only in
|
||||
the relations pack must tag the surface with the relations pack id."""
|
||||
surface = pack_grounded_procedure_surface("trace my ancestor")
|
||||
assert surface is not None
|
||||
assert "ancestor" in surface
|
||||
assert "procedure-grounded (en_core_relations_v1)" in surface
|
||||
assert "not yet ratified in this session" in surface
|
||||
|
||||
|
||||
def test_correction_surface_threads_kinship_topic() -> None:
|
||||
"""A CORRECTION whose first topic lemma is kinship-pack-resident
|
||||
weaves it into the acknowledgement. Anchor pack stays cognition
|
||||
(the ``correction`` lemma lives there)."""
|
||||
surface = pack_grounded_correction_surface(
|
||||
"No, my parent disagrees with that."
|
||||
)
|
||||
assert surface is not None
|
||||
assert "pack-grounded (en_core_cognition_v1)" in surface
|
||||
assert "Noted topic: parent" in surface
|
||||
# Topic domains come from the relations pack, but render verbatim
|
||||
# as part of the topic clause — no separate tag emitted (the topic
|
||||
# pack is implied by the lemma).
|
||||
assert "kinship" in surface
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Live runtime — DEFINITION on a kinship lemma grounds
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_runtime_definition_on_kinship_lemma_engages_pack_path() -> None:
|
||||
"""``What is a parent?`` on a cold-start runtime now routes through
|
||||
the pack-grounded path — the relations pack is mounted, the
|
||||
resolver finds the lemma, the surface composer emits a
|
||||
deterministic surface tagged ``en_core_relations_v1``."""
|
||||
rt = ChatRuntime()
|
||||
resp = rt.chat("What is a parent?")
|
||||
assert resp.grounding_source == "pack"
|
||||
assert "parent" in resp.surface
|
||||
assert "en_core_relations_v1" in resp.surface
|
||||
|
||||
|
||||
def test_runtime_recall_on_kinship_lemma_engages_pack_path() -> None:
|
||||
"""``Remember family`` — RECALL intent — also surfaces via the
|
||||
cross-pack path."""
|
||||
rt = ChatRuntime()
|
||||
resp = rt.chat("Remember family")
|
||||
assert resp.grounding_source == "pack"
|
||||
assert "family" in resp.surface
|
||||
|
||||
|
||||
def test_relations_pack_is_in_default_input_packs() -> None:
|
||||
"""ADR-0063 — invariant: the relations pack joins the default
|
||||
mount once the resolver lands. If this asserts false, the live
|
||||
path lost cross-pack grounding silently."""
|
||||
assert "en_core_relations_v1" in RuntimeConfig().input_packs
|
||||
|
||||
|
||||
def test_resolver_default_pack_order_favors_cognition() -> None:
|
||||
"""Cognition is resolved first. When future packs introduce a
|
||||
name collision (today there is none), cognition wins — preserving
|
||||
cognition-lane byte-identity."""
|
||||
from chat.pack_resolver import DEFAULT_RESOLVABLE_PACK_IDS
|
||||
assert DEFAULT_RESOLVABLE_PACK_IDS[0] == "en_core_cognition_v1"
|
||||
|
|
@ -5,18 +5,18 @@ Following the teaching-order doctrine
|
|||
domain expansion starts with kinship — a tight, well-bounded
|
||||
domain whose triples exercise every formation gate end-to-end.
|
||||
|
||||
This pack is **not yet mounted** on the default runtime: it is
|
||||
not in ``RuntimeConfig.input_packs`` defaults. Operator opt-in via
|
||||
explicit ``RuntimeConfig(input_packs=(..., "en_core_relations_v1"))``
|
||||
is the engagement path until a follow-up ADR introduces cross-pack
|
||||
composition for teaching-grounded surfaces.
|
||||
As of ADR-0063 (cross-pack surface resolver), this pack IS in the
|
||||
default ``RuntimeConfig.input_packs``. Pack-grounded surface
|
||||
composers in :mod:`chat.pack_grounding` consult
|
||||
:mod:`chat.pack_resolver` so kinship lemmas ground on the live path
|
||||
without a separate composer module.
|
||||
|
||||
These tests pin:
|
||||
|
||||
- The pack loads via ``load_pack("en_core_relations_v1")`` without
|
||||
a checksum mismatch (manifest.checksum matches the bytes on
|
||||
disk per CLAUDE.md's pack-discipline).
|
||||
- The 9 kinship lemmas are all present.
|
||||
- The 8 kinship lemmas are all present.
|
||||
- Each lemma carries the expected canonical semantic_domains
|
||||
(deterministic taxonomy under ``kinship.*``, ``lineage.*``,
|
||||
``biology.*``, ``social.*``).
|
||||
|
|
@ -130,11 +130,15 @@ def test_no_lemma_collision_with_cognition_pack() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_pack_is_not_in_default_input_packs() -> None:
|
||||
"""Engagement is opt-in. Adding the pack to the default
|
||||
``input_packs`` is a follow-up ADR scope: cross-pack teaching-
|
||||
grounded composition does not exist yet, and silently mounting
|
||||
the pack would change the runtime's recall surface without a
|
||||
corresponding ratification path."""
|
||||
def test_pack_is_in_default_input_packs() -> None:
|
||||
"""ADR-0063 — once the cross-pack surface resolver landed, the
|
||||
relations pack joined the default mount. Pack composers in
|
||||
:mod:`chat.pack_grounding` consult :mod:`chat.pack_resolver` for
|
||||
cross-pack lemma residency, so mounting the pack no longer
|
||||
silently widens recall without a corresponding surface composer
|
||||
— the composer is now cross-pack by default.
|
||||
|
||||
Inverted from the original ADR-0063-pre guard
|
||||
``test_pack_is_not_in_default_input_packs``."""
|
||||
from core.config import RuntimeConfig
|
||||
assert PACK_ID not in RuntimeConfig().input_packs
|
||||
assert PACK_ID in RuntimeConfig().input_packs
|
||||
|
|
|
|||
|
|
@ -89,23 +89,29 @@ def test_cold_start_recall_returns_pack_grounded_surface() -> None:
|
|||
|
||||
|
||||
def test_cold_start_unknown_lemma_returns_universal_disclosure() -> None:
|
||||
"""When the gate fires AND the subject lemma is not in the pack,
|
||||
we fall through to the universal disclosure unchanged."""
|
||||
"""When the gate fires AND no lemma in the utterance resolves in any
|
||||
mounted pack, we fall through to the universal disclosure unchanged.
|
||||
|
||||
ADR-0061 + ADR-0063 — the PROCEDURE composer scans the subject
|
||||
phrase for any mounted-pack lemma. This test deliberately uses
|
||||
a fully out-of-pack prompt so neither the cognition nor the
|
||||
relations pack catches a topic anchor."""
|
||||
rt = ChatRuntime()
|
||||
# 'addresses' is in vocab but subject after intent parsing will be
|
||||
# the multi-word phrase, which won't match the pack lemma index.
|
||||
resp = rt.chat("How can I correct an error?")
|
||||
resp = rt.chat("How can I quoxulate the wxyzabc?")
|
||||
assert resp.surface == _UNKNOWN_DOMAIN_SURFACE
|
||||
assert resp.grounding_source == "none"
|
||||
|
||||
|
||||
def test_cold_start_non_definition_intent_no_pack_grounding() -> None:
|
||||
"""CAUSE / COMPARISON / VERIFICATION intents do not engage the
|
||||
pack path even when their subject is a pack lemma — narrow scope
|
||||
of ADR-0048 is DEFINITION + RECALL only."""
|
||||
"""CAUSE on a non-pack subject lemma does not engage the
|
||||
pack-grounded DEFINITION path — pack-grounded surfaces require a
|
||||
pack-resident lemma in any mounted lexicon (ADR-0048 + ADR-0063).
|
||||
|
||||
ADR-0052 teaching-grounded surfaces handle CAUSE on subjects that
|
||||
appear in the reviewed cognition chains corpus; ``wxyzabc`` is in
|
||||
neither the pack nor the corpus, so the universal disclosure fires."""
|
||||
rt = ChatRuntime()
|
||||
# 'Why does light exist?' is CAUSE intent.
|
||||
resp = rt.chat("Why does light exist?")
|
||||
resp = rt.chat("Why does wxyzabc exist?")
|
||||
assert resp.grounding_source == "none"
|
||||
assert resp.surface == _UNKNOWN_DOMAIN_SURFACE
|
||||
|
||||
|
|
|
|||
167
tests/test_pack_resolver.py
Normal file
167
tests/test_pack_resolver.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"""ADR-0063 — cross-pack surface resolver tests.
|
||||
|
||||
The contract these tests pin:
|
||||
|
||||
- :func:`chat.pack_resolver.resolve_lemma` returns the first
|
||||
``(pack_id, semantic_domains)`` whose lexicon contains the lemma.
|
||||
- Cognition lemmas resolve to ``en_core_cognition_v1``; kinship
|
||||
lemmas resolve to ``en_core_relations_v1``; absent lemmas return
|
||||
``None``.
|
||||
- First-match-wins on order.
|
||||
- The lru_cache survives repeat calls without re-reading disk.
|
||||
- :func:`mounted_lemmas` is the union of lemma keys across the
|
||||
mounted packs in deterministic order.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from chat.pack_resolver import (
|
||||
DEFAULT_RESOLVABLE_PACK_IDS,
|
||||
_pack_lexicon_for,
|
||||
clear_resolver_cache,
|
||||
is_resolvable,
|
||||
mounted_lemmas,
|
||||
resolve_lemma,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_lemma — first-match-wins across mounted packs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lemma",
|
||||
["light", "knowledge", "meaning", "memory", "truth", "thought", "concept"],
|
||||
)
|
||||
def test_cognition_lemma_resolves_to_cognition_pack(lemma: str) -> None:
|
||||
resolved = resolve_lemma(lemma)
|
||||
assert resolved is not None, f"{lemma!r} did not resolve"
|
||||
pack_id, domains = resolved
|
||||
assert pack_id == "en_core_cognition_v1"
|
||||
assert isinstance(domains, tuple)
|
||||
assert domains, "resolved pack must surface non-empty semantic_domains"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lemma",
|
||||
["parent", "child", "sibling", "family", "ancestor",
|
||||
"descendant", "spouse", "offspring"],
|
||||
)
|
||||
def test_kinship_lemma_resolves_to_relations_pack(lemma: str) -> None:
|
||||
resolved = resolve_lemma(lemma)
|
||||
assert resolved is not None, f"{lemma!r} did not resolve"
|
||||
pack_id, domains = resolved
|
||||
assert pack_id == "en_core_relations_v1"
|
||||
assert isinstance(domains, tuple)
|
||||
assert domains
|
||||
|
||||
|
||||
def test_unknown_lemma_returns_none() -> None:
|
||||
assert resolve_lemma("nonexistent_lemma_xyz") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["", " ", None])
|
||||
def test_empty_or_invalid_lemma_returns_none(bad) -> None:
|
||||
assert resolve_lemma(bad) is None # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_resolver_normalizes_case_and_whitespace() -> None:
|
||||
a = resolve_lemma("Knowledge")
|
||||
b = resolve_lemma(" knowledge ")
|
||||
c = resolve_lemma("knowledge")
|
||||
assert a == b == c
|
||||
assert a is not None and a[0] == "en_core_cognition_v1"
|
||||
|
||||
|
||||
def test_resolver_is_first_match_wins() -> None:
|
||||
"""When the same lemma appears in two mounted packs, the earlier
|
||||
pack in *pack_ids* wins. Today no lemma collision exists between
|
||||
cognition and relations (orthogonality test enforces this); this
|
||||
test reverses the order to verify the resolution rule itself."""
|
||||
reversed_order = (
|
||||
"en_core_relations_v1",
|
||||
"en_core_cognition_v1",
|
||||
)
|
||||
# ``parent`` exists only in relations; ``knowledge`` only in
|
||||
# cognition — order swap should not change which pack carries
|
||||
# them, only their resolution order.
|
||||
parent = resolve_lemma("parent", pack_ids=reversed_order)
|
||||
knowledge = resolve_lemma("knowledge", pack_ids=reversed_order)
|
||||
assert parent is not None and parent[0] == "en_core_relations_v1"
|
||||
assert knowledge is not None and knowledge[0] == "en_core_cognition_v1"
|
||||
|
||||
|
||||
def test_pack_ids_default_contains_both_packs() -> None:
|
||||
assert "en_core_cognition_v1" in DEFAULT_RESOLVABLE_PACK_IDS
|
||||
assert "en_core_relations_v1" in DEFAULT_RESOLVABLE_PACK_IDS
|
||||
# Cognition first — first-match-wins favours cognition on any
|
||||
# future cross-pack lemma collision.
|
||||
assert DEFAULT_RESOLVABLE_PACK_IDS.index("en_core_cognition_v1") < \
|
||||
DEFAULT_RESOLVABLE_PACK_IDS.index("en_core_relations_v1")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# is_resolvable — boolean shortcut
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_is_resolvable_round_trips() -> None:
|
||||
assert is_resolvable("light") is True
|
||||
assert is_resolvable("parent") is True
|
||||
assert is_resolvable("nonexistent_lemma_xyz") is False
|
||||
assert is_resolvable("") is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# mounted_lemmas — union view used by topic extractors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_mounted_lemmas_unions_both_packs() -> None:
|
||||
union = mounted_lemmas()
|
||||
assert "knowledge" in union
|
||||
assert "parent" in union
|
||||
assert "nonexistent_lemma_xyz" not in union
|
||||
# Frozen so callers cannot mutate the cached union accidentally.
|
||||
assert isinstance(union, frozenset)
|
||||
|
||||
|
||||
def test_mounted_lemmas_respects_explicit_pack_ids() -> None:
|
||||
cognition_only = mounted_lemmas(pack_ids=("en_core_cognition_v1",))
|
||||
assert "knowledge" in cognition_only
|
||||
assert "parent" not in cognition_only, (
|
||||
"cognition-only view leaked a kinship lemma — pack residency "
|
||||
"boundary broken"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Caching contract
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_pack_lexicon_is_cached() -> None:
|
||||
"""Two calls for the same pack must return the same dict identity
|
||||
(lru_cache returns the cached object, not a copy)."""
|
||||
a = _pack_lexicon_for("en_core_cognition_v1")
|
||||
b = _pack_lexicon_for("en_core_cognition_v1")
|
||||
assert a is b
|
||||
|
||||
|
||||
def test_clear_resolver_cache_is_safe() -> None:
|
||||
"""The test-only escape hatch must not crash when called twice."""
|
||||
clear_resolver_cache()
|
||||
clear_resolver_cache()
|
||||
# And the resolver still works afterwards.
|
||||
assert resolve_lemma("knowledge") is not None
|
||||
|
||||
|
||||
def test_missing_pack_returns_empty_index() -> None:
|
||||
"""A pack id with no on-disk lexicon must yield an empty dict and
|
||||
callers see ``None`` from :func:`resolve_lemma` — no exception."""
|
||||
empty = _pack_lexicon_for("nonexistent_pack_id_zzz_v0")
|
||||
assert empty == {}
|
||||
assert resolve_lemma("knowledge", pack_ids=("nonexistent_pack_id_zzz_v0",)) is None
|
||||
Loading…
Reference in a new issue