core/chat/pack_resolver.py
Shay cb1eba72ae feat(packs): en_core_action_v1 — action verbs (26 lemmas)
Workstream 1 fourth pack.  Closes the common-action verb gap.  Prior
packs covered reasoning (cognition), speech/perception (meta), and
adjectives (attitude); this pack covers what an agent *does*.

Pack composition (26 VERB entries):

  action.doing.perform     do perform execute carry conduct
  action.doing.make        make
  action.doing.achieve     achieve accomplish
  action.creating.originate create build form produce generate develop
  action.changing.transform change transform
  action.moving.translate  move
  action.moving.depart_arrive go come
  action.moving.transfer   send receive
  action.possessing.acquire get take
  action.possessing.transfer give
  action.possessing.retain keep
  action.possessing.deploy use

Files:
  language_packs/data/en_core_action_v1/
    lexicon.jsonl   — 26 entries, SHA-256 checksum-sealed
    manifest.json   — operational_base / D0
  chat/pack_resolver.py
    Appended to DEFAULT_RESOLVABLE_PACK_IDS after temporal.
  core/config.py
    Added to RuntimeConfig.input_packs default mount.
  tests/test_en_core_action_v1_pack.py
    11 contract tests covering load / POS / namespace / no-collision /
    contiguous-ids / mounted-by-default / resolver-order / routing /
    prior-pack invariance.
  tests/test_procedure_surface.py
    Swapped two test fixtures from "do stuff" to "fix bugs".  ``do``
    is now correctly pack-resident in en_core_action_v1 (semantically
    correct — "How do I do stuff?" should ground on ``do``), so the
    "no pack lemma exists" contract needed a fixture where both verb
    and noun are genuinely OOV.  ``fix bugs`` satisfies this across
    all 7 mounted packs.

Authoring:
  Three parallel subagents — doing / creating / moving+possessing.
  Strict exemplar + forbidden-lemma list against all 6 prior packs.

Verification:
  Cognition eval byte-identical on both splits (100/100/91.7/100 and
  100/100/83.3/100).
  All 70 pack tests pass (cognition + meta + attitude + temporal +
  action + quant tests run together).
  Live composer probes confirm every action lemma surfaces
  deterministically from en_core_action_v1.
2026-05-19 05:38:12 -07:00

141 lines
5 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_meta_v1",
"en_core_attitude_v1",
"en_core_temporal_v1",
"en_core_action_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()