core/chat/pack_grounding.py
Shay bf7f7895fe feat(adr-0061): PROCEDURE intent routes to pack-grounded surface
Pre-ADR-0061 every "How do I X?" question fell through to the
universal disclosure even when X was a pack-resident lemma.  The
teaching corpus carries CAUSE/VERIFICATION chains only — procedural
knowledge is fundamentally different in kind from propositional
claims and deserves its own ratification path (deliberately out of
scope; a future parallel `procedure_chains_v1.jsonl` schema is
discussed in the ADR's non-goals).

ADR-0061 adds the honest cold-start fallback: ground the topic in
pack semantic_domains and note explicitly that ratified step-by-step
guidance does not exist yet.

Surface format:
  "procedure-grounded ({pack_id}): {lemma} ({d1}; {d2}).
   Step-by-step guidance for {lemma} is not yet ratified
   in this session."

Selector — **last** pack-resident lemma in the verb-phrase subject:
  "define a concept" → concept    (object beats verb)
  "verify a claim"   → verify     (verb wins when object is OOV)
  "correct an error" → correct
  "learn this"       → learn
  "do stuff"         → None       (falls through to universal disclosure)

Stopwords: only `be` and `have` (dialogue fillers).  Procedure verbs
are deliberately NOT stopworded so the verb-as-fallback rule fires
when the object is OOV — keeps surface coverage.

Trust-boundary invariants:
  - Every visible non-template token is lemma / pack-domain / template.
  - Deterministic: same subject_text → same bytes.
  - Returns None for fully-unknown utterances → universal disclosure
    fires.  Never fabricates surface from nothing (ADR-0053 contract).
  - "not yet ratified" trust-label preserved.

Cognition lane lift:
  public  : intent 100% / surface 100% / term 91.7% / versor 100%      (unchanged)
  holdout : intent 100% / surface 94.7%→100.0% / term 79.2%→83.3% / versor 100%

Two cases fixed:
  - procedure_define_010 ("How do I define a concept?") — surface +
    term `concept` now captured.
  - procedure_verify_034 ("How do I verify a claim?") — surface only
    (case has no expected_terms; the verb fallback grounds it).

Combined effect: holdout `surface_groundedness` closes to 100%; 4 of
5 architectural holdout misses now resolved (this ADR + ADR-0060 +
the supersede from epistemology v1).  Remaining 2 are UNKNOWN-intent
cases (unknown_spirit_041, unknown_word_018) — out of scope; deserve
their own ADR with distinct selector semantics.

- chat/pack_grounding.py — `_extract_procedure_topic_lemma` helper +
  `pack_grounded_procedure_surface` composer.
- chat/runtime.py — import + dispatch branch for `IntentTag.PROCEDURE`.
- tests/test_procedure_surface.py — 15 tests pin: extraction
  (last-wins / verb-by-elimination / be+have skipped / None on empty /
  strips punctuation / case-insensitive); surface (contains lemma /
  contains domains / pack_id / "not yet ratified" label / None for
  no-pack-lemma / deterministic); end-to-end through ChatRuntime.

Lanes (regression): smoke 67 / cognition 121 / teaching 17 /
procedure 15 — all green.

The non-negotiable field invariant (versor_condition < 1e-6) is
unaffected: this ADR changes surface composition only.
2026-05-18 14:22:19 -07:00

363 lines
14 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
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) -> 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."
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).
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``.
"""
if not lemma or not isinstance(lemma, str):
return None
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"No session evidence yet."
)
def is_pack_lemma(lemma: str) -> bool:
"""Return True iff *lemma* has an entry with ``semantic_domains`` in the pack."""
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 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
(``be``, ``have``) that classify as pack lemmas but carry no
topical signal.
Used by ``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.
raw = text.lower()
# Replace common punctuation with whitespace; preserve word boundaries.
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 index:
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:
topic_domains = index.get(topic_lemma, ())
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
index = _pack_index()
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 index:
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
index = _pack_index()
domains = index.get(lemma, ())
if not domains:
return None
head = "; ".join(domains[:2])
return (
f"procedure-grounded ({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
index = _pack_index()
domains_a = index.get(key_a)
domains_b = index.get(key_b)
if not domains_a or not domains_b:
return None
head_a = "; ".join(domains_a[:2])
head_b = "; ".join(domains_b[:2])
return (
f"{key_a} ({head_a}) contrasts with {key_b} ({head_b}) "
f"— pack-grounded ({PACK_ID}). No session evidence yet."
)