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.
396 lines
15 KiB
Python
396 lines
15 KiB
Python
"""chat/pack_grounding.py — pack-grounded surface for cold-start DEFINITION
|
||
and RECALL intents (ADR-0048).
|
||
|
||
When the ``UnknownDomainGate`` fires with ``source="empty_vault"`` — i.e.
|
||
the runtime has no session evidence yet — the runtime would otherwise
|
||
emit the universal ``_UNKNOWN_DOMAIN_SURFACE`` disclosure on every turn,
|
||
including for terms that are explicitly compiled into the ratified
|
||
cognition pack.
|
||
|
||
This module supplies a narrow, auditable alternative: when the input's
|
||
intent is ``DEFINITION`` or ``RECALL`` AND the intent's subject lemma is
|
||
present in ``en_core_cognition_v1``, return a deterministic surface
|
||
composed from the pack lexicon's ``semantic_domains`` for that lemma,
|
||
explicitly tagged as pack-sourced.
|
||
|
||
Design constraints (matching the seven axioms):
|
||
|
||
- Geometry-first: the pack lookup is by lemma surface, but the
|
||
``semantic_domains`` were curated against the same versors the
|
||
vocabulary carries; the surface refers only to the lemma and its
|
||
curated descriptors — no synthesis, no LLM fallback.
|
||
- Reconstruction-over-storage: the surface is reconstructed from the
|
||
pack at call time; the lexicon is loaded once and cached because
|
||
ratified packs are immutable.
|
||
- Dual-correction: any lemma not in the pack returns ``None``;
|
||
callers fall through to ``_UNKNOWN_DOMAIN_SURFACE`` unchanged.
|
||
- Compilation-last: no tensors, no kernels — JSONL read and string
|
||
formatting only.
|
||
- Trust boundary: every surface produced here is explicitly tagged
|
||
``pack:en_core_cognition_v1`` so the audit contract distinguishes
|
||
pack-grounded surfaces from vault-grounded surfaces and from the
|
||
universal disclosure.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
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 = (
|
||
Path(__file__).resolve().parent.parent
|
||
/ "language_packs"
|
||
/ "data"
|
||
/ PACK_ID
|
||
/ "lexicon.jsonl"
|
||
)
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def _pack_index() -> dict[str, tuple[str, ...]]:
|
||
"""Load the cognition pack lexicon once and return ``{lemma: semantic_domains}``.
|
||
|
||
Ratified packs are immutable; safe to cache for the process lifetime.
|
||
Returns an empty dict if the pack is unavailable — callers must treat
|
||
a missing pack as "no pack-grounded surface available."
|
||
"""
|
||
if not _PACK_LEXICON_PATH.exists():
|
||
return {}
|
||
out: dict[str, tuple[str, ...]] = {}
|
||
for line in _PACK_LEXICON_PATH.read_text(encoding="utf-8").splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
entry = json.loads(line)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
lemma = entry.get("lemma") or entry.get("surface")
|
||
if not lemma:
|
||
continue
|
||
domains = tuple(entry.get("semantic_domains", ()))
|
||
if domains:
|
||
out[lemma.lower()] = domains
|
||
return out
|
||
|
||
|
||
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 ({resolved_pack_id}): {d1}; {d2}; {d3}. No session evidence yet."
|
||
|
||
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 lemma does not resolve in any of *pack_ids*.
|
||
"""
|
||
resolved = resolve_lemma(lemma, pack_ids)
|
||
if resolved is None:
|
||
return None
|
||
resolved_pack_id, domains = resolved
|
||
key = lemma.strip().lower()
|
||
head = "; ".join(domains[:3])
|
||
return (
|
||
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
|
||
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()
|
||
|
||
|
||
_CORRECTION_TOPIC_STOPWORDS: frozenset[str] = frozenset({
|
||
# The meta-cognition lemma itself — we never echo it as the topic
|
||
# because it's already the subject of the acknowledgement template.
|
||
"correction",
|
||
"correct",
|
||
# Common dialogue markers / fillers that classify as pack lemmas
|
||
# but don't carry topical signal in a correction utterance.
|
||
"be",
|
||
"have",
|
||
})
|
||
|
||
|
||
def _extract_correction_topic_lemma(text: str) -> str | 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 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.
|
||
|
||
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
|
||
lemmas = mounted_lemmas()
|
||
raw = text.lower()
|
||
for ch in ",.;:!?\"'()[]{}":
|
||
raw = raw.replace(ch, " ")
|
||
for token in raw.split():
|
||
if not token:
|
||
continue
|
||
if token in _CORRECTION_TOPIC_STOPWORDS:
|
||
continue
|
||
if token in lemmas:
|
||
return token
|
||
return None
|
||
|
||
|
||
def pack_grounded_correction_surface(text: str | None = None) -> str | None:
|
||
"""ADR-0053 + ADR-0060 — cold-start CORRECTION acknowledgement.
|
||
|
||
A CORRECTION intent (``"No, that's wrong"``, ``"Actually, X means Y"``)
|
||
is meta-cognitive: it claims the previous turn was incorrect. On a
|
||
cold-start session there is no prior turn to apply the correction
|
||
to, so the doctrine-aligned response is **not** to define what
|
||
correction is (that would be the DEFINITION path) but to
|
||
acknowledge receipt and state explicitly that no prior turn exists
|
||
in this session.
|
||
|
||
Surface format (fixed templates, all atoms pack-sourced):
|
||
|
||
- **Without topic** (text=None or no pack-resident lemma found):
|
||
|
||
"correction received — pack-grounded ({pack_id}): {d1}; {d2}; {d3}.
|
||
No prior turn in this session to correct yet."
|
||
|
||
- **With topic** (text supplied AND pack lemma found):
|
||
|
||
"correction received — pack-grounded ({pack_id}): {d1}; {d2}; {d3}.
|
||
Noted topic: {lemma} ({td1}; {td2}).
|
||
No prior turn in this session to correct yet."
|
||
|
||
Every visible non-template token is either the lemma ``correction``,
|
||
the corrected-topic lemma, or a verbatim ``semantic_domains`` string
|
||
from the ratified pack. No inference; no rewording.
|
||
|
||
The trailing disclosure (``No prior turn in this session to correct
|
||
yet.``) is the constant trust-boundary label distinguishing this
|
||
cold-start acknowledgement from the post-correction teaching
|
||
repair path (``teaching/correction.py``) which engages once a
|
||
prior turn exists.
|
||
|
||
Returns ``None`` if the pack is unavailable or has no entry for
|
||
``correction`` — callers fall through to the universal disclosure
|
||
unchanged.
|
||
"""
|
||
index = _pack_index()
|
||
domains = index.get("correction")
|
||
if not domains:
|
||
return None
|
||
head = "; ".join(domains[:3])
|
||
topic_lemma = _extract_correction_topic_lemma(text) if text else None
|
||
if topic_lemma is not None:
|
||
# 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 (
|
||
f"correction received — pack-grounded ({PACK_ID}): {head}. "
|
||
f"Noted topic: {topic_lemma} ({topic_head}). "
|
||
f"No prior turn in this session to correct yet."
|
||
)
|
||
return (
|
||
f"correction received — pack-grounded ({PACK_ID}): {head}. "
|
||
f"Noted topic: {topic_lemma}. "
|
||
f"No prior turn in this session to correct yet."
|
||
)
|
||
return (
|
||
f"correction received — pack-grounded ({PACK_ID}): {head}. "
|
||
f"No prior turn in this session to correct yet."
|
||
)
|
||
|
||
|
||
_PROCEDURE_TOPIC_STOPWORDS: frozenset[str] = frozenset({
|
||
# Pack-resident lemmas that classify but carry no topical signal
|
||
# in a procedure utterance — dialogue fillers / copulae.
|
||
"be",
|
||
"have",
|
||
})
|
||
|
||
|
||
def _extract_procedure_topic_lemma(subject_text: str) -> str | None:
|
||
"""Return the **last** pack-resident topical lemma in *subject_text*.
|
||
|
||
Procedure subjects emerge from the intent classifier as verb
|
||
phrases (e.g. ``"define a concept"``, ``"correct an error"``,
|
||
``"verify a claim"``). The procedure verb tends to be the
|
||
first pack-resident lemma; the *topic* of the procedure tends
|
||
to be the last. Selecting the last pack-resident lemma
|
||
captures the user's actual subject of interest without requiring
|
||
POS tagging or syntactic analysis.
|
||
|
||
Deterministic: tokens are processed left-to-right; the *last*
|
||
token that is pack-resident AND not in the stopword set wins.
|
||
|
||
Stopwords filter only dialogue fillers (``be`` / ``have``);
|
||
pack-resident verbs (``define``, ``verify``, ``correct``, etc.)
|
||
are NOT stopworded — when a procedure utterance contains only
|
||
one pack-resident lemma and that lemma is the verb, the verb
|
||
is the topical anchor by elimination.
|
||
"""
|
||
if not subject_text or not isinstance(subject_text, str):
|
||
return None
|
||
lemmas = mounted_lemmas()
|
||
raw = subject_text.lower()
|
||
for ch in ",.;:!?\"'()[]{}":
|
||
raw = raw.replace(ch, " ")
|
||
last_match: str | None = None
|
||
for token in raw.split():
|
||
if not token:
|
||
continue
|
||
if token in _PROCEDURE_TOPIC_STOPWORDS:
|
||
continue
|
||
if token in lemmas:
|
||
last_match = token
|
||
return last_match
|
||
|
||
|
||
def pack_grounded_procedure_surface(subject_text: str) -> str | None:
|
||
"""ADR-0061 — cold-start PROCEDURE pack-grounded surface.
|
||
|
||
A PROCEDURE intent (``"How do I X?"``, ``"How can I Y?"``) requests
|
||
step-by-step guidance. Procedural chains are not part of the
|
||
reviewed teaching corpus today (teaching chains cover CAUSE and
|
||
VERIFICATION intents only — see
|
||
``chat.teaching_grounding._VALID_INTENTS``). Rather than fall
|
||
through to the universal disclosure on every procedure question,
|
||
this composer emits a pack-grounded acknowledgement that surfaces
|
||
the topical lemma of the procedure and notes explicitly that
|
||
step-by-step guidance is not yet ratified — preserving honesty
|
||
while grounding the user's topic in pack semantics.
|
||
|
||
Surface format (fixed template, all atoms pack-sourced):
|
||
|
||
"procedure-grounded ({pack_id}): {lemma} ({d1}; {d2}).
|
||
Step-by-step guidance for {lemma} is not yet ratified
|
||
in this session."
|
||
|
||
The trailing clause is the constant trust-boundary label,
|
||
analogous to ``"No prior turn in this session to correct yet."``
|
||
in the CORRECTION acknowledgement (ADR-0053 / ADR-0060).
|
||
|
||
Returns ``None`` if no pack-resident lemma is found in
|
||
*subject_text* — callers fall through to the universal disclosure
|
||
unchanged (preserves the ADR-0053 honesty contract for the
|
||
fully-unknown case).
|
||
"""
|
||
lemma = _extract_procedure_topic_lemma(subject_text)
|
||
if lemma is None:
|
||
return None
|
||
# 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 ({resolved_pack_id}): {lemma} ({head}). "
|
||
f"Step-by-step guidance for {lemma} is not yet ratified in this session."
|
||
)
|
||
|
||
|
||
def pack_grounded_comparison_surface(
|
||
lemma_a: str, lemma_b: str
|
||
) -> str | None:
|
||
"""ADR-0050 — deterministic pack-grounded surface for COMPARISON intent.
|
||
|
||
Returns a surface that composes each lemma's pack semantic_domains
|
||
side-by-side, with no rewording or inference:
|
||
|
||
"{a} (d_a1; d_a2) contrasts with {b} (d_b1; d_b2) — pack-grounded
|
||
({pack_id}). No session evidence yet."
|
||
|
||
Up to two semantic_domains per side are emitted to keep the surface
|
||
compact. All visible tokens are either the lemmas themselves or
|
||
verbatim pack strings; the verb "contrasts with" is the fixed
|
||
COMPARISON template constant (mirroring the relation predicate
|
||
`contrasts_with` already humanised by ``humanize_predicate``).
|
||
|
||
Returns ``None`` when:
|
||
- either lemma is empty or not a string,
|
||
- either lemma is not present in the pack,
|
||
- the two lemmas are identical (a comparison between a term
|
||
and itself carries no contrastive evidence — defer to the
|
||
single-lemma ``pack_grounded_surface`` path or to the
|
||
universal disclosure).
|
||
"""
|
||
if not lemma_a or not isinstance(lemma_a, str):
|
||
return None
|
||
if not lemma_b or not isinstance(lemma_b, str):
|
||
return None
|
||
key_a = lemma_a.strip().lower()
|
||
key_b = lemma_b.strip().lower()
|
||
if not key_a or not key_b:
|
||
return None
|
||
if key_a == key_b:
|
||
return None
|
||
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"— {tag}. No session evidence yet."
|
||
)
|