ADR-0065 P2.4. Eight specialization lemmas, each a typed
specialization of an en_core_relations_v1 primitive:
mother / father is-a parent
daughter / son is-a child
sister / brother is-a sibling
grandparent / grandchild is-a ancestor / descendant (1-step)
Strict pack-internal taxonomy under kinship.*:
mother → kinship.parent.female
father → kinship.parent.male
daughter → kinship.child.female
son → kinship.child.male
brother → kinship.sibling.male
sister → kinship.sibling.female
grandparent → kinship.ascendant.transitive_1step
grandchild → kinship.descendant.transitive_1step
Pack ratification:
- SHA-256 checksum 7d0583f7e6a13ce72a5b0b191786cfc57af31583dc5111b24c3466e89ee70856
- Orthogonal to en_core_relations_v1 + en_core_cognition_v1 (zero
lemma collision in either direction)
- Mounted by default in RuntimeConfig.input_packs + added to the
cross-pack resolver's DEFAULT_RESOLVABLE_PACK_IDS
Companion corpus relations_chains_v2.jsonl seeds 7 v2-internal
reviewed chains so DEFINITION/CAUSE/VERIFICATION on every v2 lemma
grounds (not just DEFINITION via the pack path):
cause_mother_precedes_daughter
cause_father_precedes_son
cause_grandparent_precedes_grandchild
cause_daughter_follows_mother
cause_son_follows_father
verification_daughter_requires_mother
verification_son_requires_father
Registered as a third TeachingCorpusSpec alongside cognition and
relations_v1. Strict pack-internal: every chain's subject AND
object reside in en_core_relations_v2. Cross-pack chain shapes
(e.g. v2 subject + v1 object) deferred per teaching_order.md §5.
Live verification:
> What is mother?
[pack] mother — pack-grounded (en_core_relations_v2):
kinship.parent.female; kinship.parent; biology.maternal.
> Why does mother exist?
[teaching] mother — teaching-grounded (relations_chains_v2):
mother precedes daughter (kinship.child.female).
> Does daughter require mother?
[teaching] daughter requires mother — verification-grounded.
10 pack-contract tests passed. Curated lanes all green; cognition
eval byte-identical.
137 lines
4.9 KiB
Python
137 lines
4.9 KiB
Python
"""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",
|
|
"en_core_relations_v2",
|
|
)
|
|
|
|
_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()
|