core/chat/pack_grounding.py
Shay 79f1678923 feat: ADR-0086 + ADR-0087 + 100-register catalog — cognition lane closure
Three load-bearing pieces:

1. ADR-0086 — UNKNOWN-intent pack-resident token surface
   New deterministic composer `pack_grounded_unknown_surface` in
   chat/pack_grounding.py.  When intent classification returns UNKNOWN
   but the prompt contains pack-resident lemmas (via cross-pack
   resolver), surface those lemmas with their semantic_domains
   instead of falling to the bare _UNKNOWN_DOMAIN_SURFACE.  Wired
   into chat/runtime.py::_maybe_pack_grounded_surface as the
   last typed-intent branch before the OOV fallback.  Null-lift
   invariant pinned: fully-OOV prompts still emit the universal
   disclosure byte-identically.  Closes four cognition-eval term
   misses: unknown_logos_019 (public), unknown_evidence_042 (dev),
   unknown_spirit_041 + unknown_word_018 (holdout).  Side effect:
   evals/results/phase2_pack_measurements.json refusal_rate drops
   from 0.25 → 0.125 across all three identity packs (no longer
   refusing on these prompts).

2. ADR-0087 — PROCEDURE selector + trailing-clause subject echo
   Two coupled changes in chat/pack_grounding.py:
   (a) Numeric-determiner downrank in _extract_procedure_topic_lemma:
       tokens whose primary semantic_domain starts with
       "quantitative.numeric." are demoted; non-numeric resident
       candidates always win.  So "compare two terms" anchors on
       `compare` not `two`.
   (b) Trailing clause echoes the full normalized subject_text
       rather than just the selected lemma, so OOV head nouns like
       "terms" reach the surface even when only the procedure verb
       is pack-resident.  Closes procedure_compare_011.

3. 100-register catalog
   New packs/register/_catalog.json — canonical machine-readable
   spec for all 100 registers (7 currently-ratified + 93 drafted)
   organized into 9 voice groups (depth/tone/stance/posture/domain/
   cultural/affective/functional/composite).  Each entry is a
   complete production input — realizer_overrides, marker palettes
   (openings/transitions/closings), depth_preference, description,
   author_notes.  All realizer_overrides use only legal keys per
   scripts/ratify_register_packs.py::_KNOWN_OVERRIDE_KEYS.
   Companion packs/register/CATALOG.md documents the production
   loop: materialize → widen REGISTER_IDS → ratify → smoke.

Cognition-eval lifts (all three splits):
  public:  term_capture 91.7% → 100.0%  (+8.3pp)
  holdout: term_capture 83.3% → 100.0%  (+16.7pp)
  dev:     term_capture 78.6% → 100.0%  (+21.4pp)
  surface_groundedness: 100% preserved on all splits
  intent_accuracy / versor_closure: 100% preserved on all splits

Tests:
  tests/test_pack_grounded_unknown.py     — 14 tests (composer
    direct + runtime engagement + null-lift invariant)
  tests/test_adr_0087_procedure_selector.py — 12 tests (selector
    numeric downrank + trailing-clause echo + regression guard)
  Existing test suites unaffected — cognition lane 120 passed / 1
  skipped both before and after.  Full lane net −3 failures vs
  pristine main (39 → 36 — none introduced).
2026-05-21 00:08:12 -07:00

1094 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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,
)
from packs.anchor_lens.loader import AnchorLens, UNANCHORED
from packs.register.loader import RegisterPack, UNREGISTERED
PACK_ID: str = "en_core_cognition_v1"
# ADR-0073c — substrate → mounted pack ids for anchor-lens engagement.
# Cognition-tier packs are the primary L1.3 substrate. Micro packs are
# included as a defensive fallback for the few distinct lemmas they
# carry; the engagement path early-exits once an atom-match is found.
_ANCHOR_LENS_SUBSTRATE_PACK_IDS: dict[str, tuple[str, ...]] = {
"grc": ("grc_logos_cognition_v1", "grc_logos_micro_v1"),
"he": ("he_core_cognition_v1", "he_logos_micro_v1"),
"en": (PACK_ID,),
}
_PACK_LEXICON_PATH = (
Path(__file__).resolve().parent.parent
/ "language_packs"
/ "data"
/ PACK_ID
/ "lexicon.jsonl"
)
# ADR-0073c — synthetic English anchor lemmas for cross-lang collapse.
# Holds entry_ids like ``en-collapse-love`` so anchor-lens engagement
# (he_chesed_v1 / he_shalom_v1 / he_tzedek_v1) can resolve English
# prompts like "What is love?" to a target entry_id and walk the
# alignment graph from there. This pack is engagement-only: it is
# intentionally NOT included in ``DEFAULT_RESOLVABLE_PACK_IDS`` so the
# composer does not fabricate a pack-grounded definition for these
# lemmas — only lens-engagement reads from here.
_COLLAPSE_ANCHORS_LEXICON_PATH = (
Path(__file__).resolve().parent.parent
/ "language_packs"
/ "data"
/ "en_collapse_anchors_v1"
/ "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 _frame_gloss(lemma: str, pos: str, gloss: str) -> str:
"""Render a fluent sentence from a (lemma, pos, gloss) triple.
POS-aware sentence frames:
NOUN -> "{Lemma} is {gloss}."
VERB -> "To {lemma} means {gloss}."
ADJ -> "Something is {lemma} when it {gloss}."
ADV -> "{Lemma} indicates {gloss}."
ADP -> "{Lemma} is a relation of {gloss}."
SCONJ -> "{Lemma} introduces {gloss}."
PRON -> "{Lemma} asks for {gloss}."
AUX -> "{Lemma} expresses {gloss}."
INTJ -> "{Lemma} is uttered to {gloss}."
DET -> "{Lemma} specifies {gloss}."
NUM -> "{Lemma} is the cardinal value {gloss}."
* (unknown) -> "{Lemma}: {gloss}." (back-compat fallback)
The glosses are authored to match these frames exactly (see
the subagent briefs and ``language_packs/data/<pack>/glosses.jsonl``).
Capitalization is applied only to the framed surface, never to
the lemma in the lexicon (which stays lowercase by convention).
"""
key = lemma.strip()
cap = key[:1].upper() + key[1:] if key else key
pos_u = (pos or "").upper()
if pos_u == "NOUN":
return f"{cap} is {gloss}."
if pos_u == "VERB":
return f"To {key} means {gloss}."
if pos_u == "ADJ":
return f"Something is {key} when it {gloss}."
if pos_u == "ADV":
return f"{cap} indicates {gloss}."
if pos_u == "ADP":
return f"{cap} is a relation of {gloss}."
if pos_u == "SCONJ":
return f"{cap} introduces {gloss}."
if pos_u == "PRON":
return f"{cap} asks for {gloss}."
if pos_u == "AUX":
return f"{cap} expresses {gloss}."
if pos_u == "INTJ":
return f"{cap} is uttered to {gloss}."
if pos_u == "DET":
return f"{cap} specifies {gloss}."
if pos_u == "NUM":
return f"{cap} is the cardinal value {gloss}."
return f"{cap}: {gloss}."
def _frame_cause_gloss(lemma: str, pos: str, gloss: str) -> str:
"""ADR-0085 — explanation-shaped CAUSE frame from a (lemma, pos, gloss) triple.
POS-aware frames that produce an *existential explanation*
(CAUSE-shape) rather than a definitional predicate-identity:
NOUN -> "{Lemma} exists as {gloss}."
VERB -> "To {lemma} is to {gloss}."
ADJ -> "To be {lemma} is to {gloss}."
* (other) -> falls back to :func:`_frame_gloss` (predicate-identity)
Frame choice rationale: ``Light exists as visible medium that reveal
truth.`` reads as an answer to *"Why does light exist?"* in a way
that the chain-walk surface (``light — teaching-grounded (...):
cognition.illumination; logos.core. light reveals truth (...)``)
does not. No new content material — same gloss, different frame
word. The gloss already implicitly carries the cause-shape
(a definition that says what something *is for*); this frame just
surfaces that shape syntactically.
"""
key = lemma.strip()
cap = key[:1].upper() + key[1:] if key else key
pos_u = (pos or "").upper()
if pos_u == "NOUN":
return f"{cap} exists as {gloss}."
if pos_u == "VERB":
return f"To {key} is to {gloss}."
if pos_u == "ADJ":
return f"To be {key} is to {gloss}."
# Fall back to predicate-identity frame for any POS where there
# is no clean explanation rendering. Better to degrade to a
# definitional answer than to emit an ungrammatical CAUSE frame.
return _frame_gloss(lemma, pos, gloss)
_DEFAULT_DISCLOSURE_DOMAIN_COUNT: int = 3
def _resolve_disclosure_domain_count(
register: RegisterPack,
*,
intent_name: str | None = None,
) -> int:
"""Return clamped disclosure_domain_count for *register*.
Resolution order (ADR-0071 per_intent extension):
1. ``realizer_overrides.per_intent[intent_name].disclosure_domain_count``
2. ``realizer_overrides.disclosure_domain_count`` (flat key)
3. Default ``_DEFAULT_DISCLOSURE_DOMAIN_COUNT`` (= 3)
The ratification gate (``scripts/ratify_register_packs.py``) is the
authoritative trust boundary: only known keys with in-bounds values
can ratify. This clamp is fail-soft defense-in-depth for off-path
callers (test fixtures, ad-hoc CLI) — a malformed override should
not crash the realizer hot path. See ADR-0070.
"""
from collections.abc import Mapping as _Mapping
overrides = register.realizer_overrides
n: object = _DEFAULT_DISCLOSURE_DOMAIN_COUNT
if intent_name is not None:
per_intent: object = (
overrides.get("per_intent") if hasattr(overrides, "get") else None
)
if isinstance(per_intent, _Mapping):
sub: object = per_intent.get(intent_name)
if isinstance(sub, _Mapping) and "disclosure_domain_count" in sub:
candidate = sub["disclosure_domain_count"]
if (
isinstance(candidate, int)
and not isinstance(candidate, bool)
and 1 <= candidate <= 3
):
return candidate
flat = overrides.get("disclosure_domain_count") if hasattr(overrides, "get") else None
if flat is not None:
n = flat
if not isinstance(n, int) or isinstance(n, bool) or n < 1 or n > 3:
return _DEFAULT_DISCLOSURE_DOMAIN_COUNT
return n
@lru_cache(maxsize=8)
def _substrate_lexicon_by_entry_id(pack_id: str) -> dict[str, tuple[str, ...]]:
"""Map ``entry_id -> semantic_domains`` for a substrate pack.
Cached for the process lifetime — ratified packs are immutable.
Returns an empty dict when the pack is unavailable.
"""
lexicon_path = (
Path(__file__).resolve().parent.parent
/ "language_packs"
/ "data"
/ pack_id
/ "lexicon.jsonl"
)
if not lexicon_path.is_file():
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
entry_id = entry.get("entry_id")
if not entry_id:
continue
out[str(entry_id)] = tuple(entry.get("semantic_domains", ()))
return out
@lru_cache(maxsize=1)
def _en_lemma_to_entry_id() -> dict[str, str]:
"""Map ``en lemma -> entry_id`` for anchor-lens engagement.
Merges the cognition pack (``en_core_cognition_v1``) with the
collapse-anchor pack (``en_collapse_anchors_v1``). Cognition entries
win on lemma collision since they carry real semantic_domains.
Cached for the process lifetime — ratified packs are immutable.
"""
out: dict[str, str] = {}
for path in (_COLLAPSE_ANCHORS_LEXICON_PATH, _PACK_LEXICON_PATH):
# cognition path is iterated second so it wins on lemma collision
if not path.is_file():
continue
for line in 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")
entry_id = entry.get("entry_id")
if not lemma or not entry_id:
continue
out[str(lemma).lower()] = str(entry_id)
return out
def _resolve_anchor_lens_mode(
en_lemma: str, anchor_lens: AnchorLens,
) -> str | None:
"""Return the lens's ``cognitive_mode_label`` if it engages on ``en_lemma``.
Engagement rule (single):
1. Resolve ``en_lemma`` to its entry_id in the cognition pack.
2. Walk the alignment graph(s) of every substrate pack matching
``anchor_lens.primary_substrate`` and find substrate lemmas
whose edges target this en entry_id.
3. For each such substrate lemma, check whether its
``semantic_domains`` contains any atom from
``anchor_lens.semantic_domain_preferences``. First match wins.
Returns ``None`` when:
* ``anchor_lens.is_null_lens()`` (the unanchored sentinel and
``default_unanchored_v1`` both early-exit here)
* ``primary_substrate`` is ``"none"`` or has no mounted packs
* the en lemma is not in the cognition pack
* no substrate lemma aligned to this en lemma carries a
preferred atom
The function never reads non-ASCII surface text — it pivots on
entry_ids and atom strings only. Glyph-leak is structurally
impossible from this engagement path.
Lazy import of :func:`alignment.graph.load_alignment` keeps the
alignment subsystem out of cold-import paths.
"""
if anchor_lens.is_null_lens() or not anchor_lens.semantic_domain_preferences:
return None
substrate = anchor_lens.primary_substrate
if substrate == "none":
return None
substrate_packs = _ANCHOR_LENS_SUBSTRATE_PACK_IDS.get(substrate, ())
if not substrate_packs:
return None
en_entry_id = _en_lemma_to_entry_id().get(en_lemma.strip().lower())
if not en_entry_id:
return None
from alignment.graph import load_alignment
preferred = set(anchor_lens.semantic_domain_preferences)
for pack_id in substrate_packs:
graph = load_alignment(pack_id)
if len(graph) == 0:
continue
substrate_index = _substrate_lexicon_by_entry_id(pack_id)
for edge in graph.edges:
if edge.target_id != en_entry_id:
continue
source_atoms = substrate_index.get(edge.source_id, ())
if not source_atoms:
continue
if any(atom in preferred for atom in source_atoms):
return anchor_lens.cognitive_mode_label
return None
def _maybe_append_anchor_lens_annotation(
surface: str, en_lemma: str, anchor_lens: AnchorLens,
) -> str:
"""Append ``[lens({lens_id}):{mode_label}]`` when lens engages.
Annotation goes between the existing trailing period and the end of
string, e.g.:
"...pack-grounded (en_core_cognition_v1)."
"...pack-grounded (en_core_cognition_v1) [lens(grc_logos_v1):systematic]."
Surface without a trailing period gets the annotation suffixed
directly. No-op when the lens does not engage.
Audit invariant: the annotation is pure ASCII (lens_id and mode
label both bounded to 64 ASCII chars by the loader).
"""
mode = _resolve_anchor_lens_mode(en_lemma, anchor_lens)
if mode is None:
return surface
annotation = f"[lens({anchor_lens.lens_id}):{mode}]"
if surface.endswith("."):
return f"{surface[:-1]} {annotation}."
return f"{surface} {annotation}"
def build_pack_surface_candidate(
lemma: str,
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
*,
register: RegisterPack = UNREGISTERED,
anchor_lens: AnchorLens = UNANCHORED,
):
"""Return a :class:`PackSurfaceCandidate` for *lemma*, or ``None``.
This is the selector-ready intermediate that
:func:`pack_grounded_surface` renders to a string. Two grounding
paths feed it:
1. Reviewed gloss (preferred) — when the pack ships a gloss for
the lemma AND the lemma is ratified in the same pack's
lexicon (verified by :func:`resolve_gloss`), the candidate
carries the gloss and ``is_fluent_sentence=True``.
2. Dotted-domain disclosure (fallback) — when no gloss exists
for the lemma, the candidate falls back to the original
"{lemma} — pack-grounded (...): d1; d2; d3. No session
evidence yet." structured form. ``is_fluent_sentence=False``.
When the future :class:`SurfaceSelector` lands, it will consume
this candidate directly without re-rendering; the surface field
is already the final user-facing string.
"""
from chat.pack_resolver import resolve_gloss
from chat.pack_surface_candidate import PackSurfaceCandidate
resolved = resolve_lemma(lemma, pack_ids)
if resolved is None:
return None
resolved_pack_id, domains = resolved
key = lemma.strip().lower()
# Try the gloss path first. resolve_gloss enforces lexicon
# residency, so a gloss that snuck into glosses.jsonl without a
# matching lexicon entry is rejected.
gloss_entry = resolve_gloss(lemma, pack_ids)
if gloss_entry is not None and gloss_entry[0] == resolved_pack_id:
_, gloss_pos, gloss_text = gloss_entry
# Fall back to the pack-resident POS when the gloss carries
# no POS (older gloss files). POS drives the sentence frame.
if not gloss_pos:
# Pull POS from the lexicon entry if we can.
from chat.pack_resolver import _pack_lexicon_for # noqa
# _pack_lexicon_for only stores domains today; POS is not
# in its cached dict. Default to NOUN frame as the safest
# fallback — most lemmas with glosses are nouns.
gloss_pos = "NOUN"
# Use lowercase "pack-grounded" mid-sentence so existing
# substring assertions in tests/test_pack_grounding.py (and
# downstream) continue to match. The marker is a provenance
# tag, not a sentence-starting word.
surface = (
f"{_frame_gloss(key, gloss_pos, gloss_text)} "
f"pack-grounded ({resolved_pack_id})."
)
# ADR-0073c — anchor-lens annotation when lens engages on
# this en lemma via the substrate alignment graph. No-op
# under UNANCHORED / default_unanchored_v1 (null-lift).
surface = _maybe_append_anchor_lens_annotation(
surface, key, anchor_lens,
)
return PackSurfaceCandidate(
surface=surface,
grounding_source="pack",
pack_id=resolved_pack_id,
gloss=gloss_text,
semantic_domains=tuple(domains),
lemma=key,
pos=gloss_pos,
is_user_facing_safe=True,
is_fluent_sentence=True,
)
# Dotted-domain disclosure fallback. Slice width is the register's
# disclosure_domain_count (ADR-0070); default (3) preserves the
# pre-R3 surface byte-for-byte under the unregistered sentinel and
# under null registers (default_neutral_v1).
n = _resolve_disclosure_domain_count(register)
head = "; ".join(domains[:n])
surface = (
f"{key} — pack-grounded ({resolved_pack_id}): {head}. "
f"No session evidence yet."
)
# ADR-0073c — anchor-lens annotation appended after the trailing
# period of the disclosure surface. No-op under UNANCHORED /
# default_unanchored_v1 (null-lift).
surface = _maybe_append_anchor_lens_annotation(
surface, key, anchor_lens,
)
return PackSurfaceCandidate(
surface=surface,
grounding_source="pack",
pack_id=resolved_pack_id,
gloss=None,
semantic_domains=tuple(domains),
lemma=key,
pos="", # POS unknown via this code path
is_user_facing_safe=True,
is_fluent_sentence=False,
)
def pack_grounded_surface(
lemma: str,
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
*,
register: RegisterPack = UNREGISTERED,
anchor_lens: AnchorLens = UNANCHORED,
) -> str | None:
"""Return a deterministic pack-grounded surface for *lemma*, or ``None``.
Two surface forms, selected by gloss presence:
With gloss (preferred, lexicon-resident):
"{Lemma} is {gloss}. Pack-grounded ({pack_id})."
(frame varies by POS — see :func:`_frame_gloss`)
Without gloss (dotted-domain disclosure — original ADR-0048 form):
"{lemma} — pack-grounded ({pack_id}): {d1}; {d2}; {d3}. No session evidence yet."
Both forms carry the ``"pack-grounded ({pack_id})"`` provenance
marker so substring-permissive tests continue to pass through
the transition.
The intermediate :class:`PackSurfaceCandidate` is the
selector-ready shape; this function renders the candidate to a
string for current callers. When :class:`SurfaceSelector` lands
the candidate is what the selector consumes directly.
Returns ``None`` when the lemma is empty or doesn't resolve.
"""
candidate = build_pack_surface_candidate(
lemma, pack_ids, register=register, anchor_lens=anchor_lens,
)
return candidate.surface if candidate is not None else None
def gloss_aware_cause_surface(
lemma: str,
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
*,
register: RegisterPack = UNREGISTERED,
anchor_lens: AnchorLens = UNANCHORED,
) -> str | None:
"""ADR-0085 — CAUSE-intent surface composed from the lemma's gloss.
When the subject lemma has a ratified gloss (via :func:`resolve_gloss`
+ lexicon residency check), emit an explanation-shaped sentence:
"{Lemma} exists as {gloss}. pack-grounded ({pack_id})."
Returns ``None`` when the lemma has no gloss in any resolvable
pack. Callers (the runtime CAUSE dispatch in :mod:`chat.runtime`)
fall through to the existing chain-walk
``teaching_grounded_surface*`` path on ``None`` — so this composer
is *additive*: it adds a gloss-driven answer for lemmas that have
one, without removing the chain-walk for those that don't.
Why an explanation frame, not the existing definition frame:
The composer at :func:`pack_grounded_surface` already emits
``"{Lemma} is {gloss}."`` for DEFINITION / RECALL intent. That's
predicate-identity shape — fine for *"What is light?"*. CAUSE
intent (*"Why does light exist?"*) wants an explanation shape;
``"{Lemma} exists as {gloss}."`` reads as an answer to the *why*
question in a way the definition frame does not, without inventing
any new content material (same gloss text, different frame word).
See :func:`_frame_cause_gloss` for POS-aware variations.
Scope limits (ADR-0085 §Scope limits):
- CAUSE intent only. VERIFICATION still goes through the
chain-walk (yes/no shape — would need a different frame).
- Single-lemma subjects only. Compound or anaphoric subjects
fall through to chain-walk.
- Pack-grounded provenance marker still leaks into the surface
(``pack-grounded (pack_id).``) — that's the surface-vs-envelope
ADR's job to remove, not this one's.
- Anchor-lens annotation appended via the existing
:func:`_maybe_append_anchor_lens_annotation` helper to preserve
ADR-0073c lens engagement on this surface path.
"""
from chat.pack_resolver import resolve_gloss, resolve_lemma
key = (lemma or "").strip()
if not key:
return None
resolved = resolve_lemma(key, pack_ids)
if resolved is None:
return None
resolved_pack_id, _domains = resolved
gloss_entry = resolve_gloss(key, pack_ids)
if gloss_entry is None or gloss_entry[0] != resolved_pack_id:
return None
_, gloss_pos, gloss_text = gloss_entry
if not gloss_pos:
# Same fallback as build_pack_surface_candidate — most glossed
# lemmas without explicit POS are nouns.
gloss_pos = "NOUN"
surface = (
f"{_frame_cause_gloss(key, gloss_pos, gloss_text)} "
f"pack-grounded ({resolved_pack_id})."
)
surface = _maybe_append_anchor_lens_annotation(
surface, key.lower(), anchor_lens,
)
# ``register`` parameter is accepted for signature parity with the
# other pack_grounded_*_surface composers but does not yet drive a
# CAUSE-specific override. Register engagement on this path is a
# follow-up (would consult realizer_overrides.per_intent["cause"]).
_ = register
return surface
_RELATION_CONFIRMATION_DISPLAY: dict[str, str] = {
"reveals": "reveals",
"grounds": "grounds",
"supports": "supports",
"requires": "requires",
"causes": "causes",
"precedes": "precedes",
"follows": "follows",
}
def pack_grounded_relation_confirmation_surface(
subject_lemma: str,
relation: str | None,
object_lemma: str | None,
*,
negated: bool = False,
) -> str | None:
"""Return a deterministic surface for a confirmed relation claim.
C2 handles prompts like ``"Light reveals truth, right?"`` by
stripping the terminal confirmation tag before intent classification.
This composer preserves the resulting proposition without requiring
a reviewed teaching chain for every relation variant. It only
emits when both endpoint lemmas resolve in mounted packs and the
relation is in the closed display table above.
"""
if not subject_lemma or not object_lemma or not relation:
return None
rel = _RELATION_CONFIRMATION_DISPLAY.get(relation.strip().lower())
if rel is None:
return None
subject_key = subject_lemma.strip().lower()
object_key = object_lemma.strip().lower()
subject_resolved = resolve_lemma(subject_key)
object_resolved = resolve_lemma(object_key)
if subject_resolved is None or object_resolved is None:
return None
subject_pack_id, subject_domains = subject_resolved
object_pack_id, object_domains = object_resolved
if negated:
predicate = f"does not {rel[:-1] if rel.endswith('s') else rel}"
else:
predicate = rel
return (
f"{subject_key} {predicate} {object_key}. "
f"pack-grounded ({subject_pack_id}; {object_pack_id}): "
f"{subject_domains[0]}; {object_domains[0]}."
)
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",
# Polarity markers (en_core_polarity_v1) — pack-resident dialogue
# tokens that carry NO topical signal in a correction utterance.
# "No, my parent disagrees" — ``no`` is the correction marker
# itself, not the topic. Without these stopwords the topic
# extractor would short-circuit on ``no`` and miss ``parent``.
"no",
"yes",
"maybe",
"perhaps",
"hardly",
"indeed",
"surely",
"definitely",
})
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,
*,
register: RegisterPack = UNREGISTERED,
) -> 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** non-numeric 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.
ADR-0087 — numeric-determiner downrank. Tokens whose primary
semantic_domain starts with ``quantitative.numeric.`` (cardinals
like ``two``, ``three``) are demoted: a non-numeric resident
candidate always wins, and the numeric is only selected when it
is the sole resident token in the subject. Closes
``procedure_compare_011`` (*"How do I compare two terms?"*),
where the pre-ADR-0087 last-wins selector picked the cardinal
determiner ``two`` over the operation verb ``compare``.
"""
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
last_numeric_fallback: str | None = None
for token in raw.split():
if not token:
continue
if token in _PROCEDURE_TOPIC_STOPWORDS:
continue
if token in lemmas:
resolved = resolve_lemma(token)
primary = resolved[1][0] if resolved is not None and resolved[1] else ""
if primary.startswith("quantitative.numeric."):
last_numeric_fallback = token
continue
last_match = token
return last_match if last_match is not None else last_numeric_fallback
def pack_grounded_procedure_surface(
subject_text: str,
*,
register: RegisterPack = UNREGISTERED,
) -> 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 {subject_text} is not yet ratified
in this session."
The trailing clause echoes the user's verb-phrase subject so that
OOV head nouns (e.g. ``"terms"`` in *"compare two terms"*) still
reach the surface even when only the procedure verb is pack-resident.
ADR-0087 — this preserves the user's actual topic of interest in
the trust-boundary label without inventing pack atoms for OOV
words.
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])
# ADR-0087 — echo the full subject_text in the trailing clause
# (normalized: lowercase + punctuation stripped) so OOV head nouns
# reach the surface. Falls back to the lemma alone if the
# normalized echo would be empty.
echo = subject_text.lower()
for ch in ",.;:!?\"'()[]{}":
echo = echo.replace(ch, " ")
echo = " ".join(echo.split())
topic_phrase = echo if echo else lemma
return (
f"procedure-grounded ({resolved_pack_id}): {lemma} ({head}). "
f"Step-by-step guidance for {topic_phrase} is not yet ratified in this session."
)
_UNKNOWN_TOKEN_STOPWORDS: frozenset[str] = frozenset({
# Pack-resident lemmas that classify but carry no topical signal in
# an UNKNOWN-intent prompt — dialogue fillers / copulae. Mirrors
# the CORRECTION / PROCEDURE stopword conventions.
"be",
"have",
})
def pack_grounded_unknown_surface(
text: str | None,
*,
max_tokens: int = 3,
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
register: RegisterPack = UNREGISTERED,
) -> str | None:
"""ADR-0086 — UNKNOWN-intent pack-resident token surface.
When the intent classifier returns :class:`IntentTag.UNKNOWN` (the
prompt does not match any known dialogue shape) but the prompt
text contains one or more pack-resident lemmas, surface those
lemmas with their semantic_domains rather than emitting the bare
``_UNKNOWN_DOMAIN_SURFACE`` disclosure.
Surface format (fixed template, all atoms pack-sourced)::
"Pack-resident tokens — pack-grounded ({tag}): {tok1} ({d_a1}; {d_a2}),
{tok2} ({d_b1}; {d_b2}). No session evidence yet."
Where ``{tag}`` is either ``{pack_id}`` (single pack) or
``{pack_a} × {pack_b}`` (cross-pack), matching the convention of
:func:`pack_grounded_comparison_surface`.
Token selection rules (deterministic, left-to-right):
1. Lowercase the prompt, strip standard punctuation.
2. Skip dialogue-filler stopwords (``be``, ``have``).
3. Take the first :func:`resolve_lemma`-resolving token, then the
next distinct one, up to ``max_tokens`` (default 3).
4. Per-token domain disclosure is capped at 2 to keep multi-token
surfaces compact; matches :func:`pack_grounded_comparison_surface`.
Returns ``None`` when:
- ``text`` is empty / not a string,
- zero prompt tokens resolve in any mounted pack.
The ``None`` fallback preserves the bare ``_UNKNOWN_DOMAIN_SURFACE``
for fully-OOV prompts — null-lift invariant: prompts that contained
zero pack-resident lemmas pre-ADR-0086 still emit the universal
disclosure byte-identically.
Closes the four UNKNOWN-intent term_capture misses in the cognition
eval lane: ``unknown_logos_019`` (public), ``unknown_evidence_042``
(dev), ``unknown_spirit_041`` / ``unknown_word_018`` (holdout).
"""
if not text or not isinstance(text, str):
return None
raw = text.lower()
for ch in ",.;:!?\"'()[]{}":
raw = raw.replace(ch, " ")
seen: set[str] = set()
hits: list[tuple[str, str, tuple[str, ...]]] = []
for token in raw.split():
if not token or token in seen:
continue
if token in _UNKNOWN_TOKEN_STOPWORDS:
continue
resolved = resolve_lemma(token, pack_ids)
if resolved is None:
continue
resolved_pack_id, domains = resolved
if not domains:
continue
seen.add(token)
hits.append((token, resolved_pack_id, domains))
if len(hits) >= max_tokens:
break
if not hits:
return None
pack_ids_in_surface: list[str] = []
for _, pid, _ in hits:
if pid not in pack_ids_in_surface:
pack_ids_in_surface.append(pid)
if len(pack_ids_in_surface) == 1:
tag = f"pack-grounded ({pack_ids_in_surface[0]})"
else:
tag = f"pack-grounded ({' × '.join(pack_ids_in_surface)})"
items = ", ".join(
f"{tok} ({'; '.join(domains[:2])})"
for tok, _, domains in hits
)
# ``register`` accepted for signature parity with the other
# pack_grounded_*_surface composers; UNKNOWN-intent does not yet
# consult realizer_overrides. Follow-up if a register variant
# wants to dial token count or per-token domain width.
_ = register
return f"Pack-resident tokens — {tag}: {items}. No session evidence yet."
def pack_grounded_comparison_surface(
lemma_a: str,
lemma_b: str,
*,
register: RegisterPack = UNREGISTERED,
) -> 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."
)