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:
Shay 2026-05-18 15:00:58 -07:00
parent f0c57eb32e
commit 9f83b27a7c
10 changed files with 860 additions and 86 deletions

View file

@ -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
View 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()

View file

@ -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",
)

View file

@ -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

View 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.

View file

@ -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) |
---

View 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"

View file

@ -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

View file

@ -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
View 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