feat(pack-grounding): selector-ready gloss wiring via PackSurfaceCandidate

Wires the gloss resolver (Phase B2) through pack_grounded_surface
WITHOUT hard-coding around the future SurfaceSelector.  Per the
2026-05-19 design review:

  > don't let glosses hard-code around the selector.  If they ship
  > first, keep the integration deliberately narrow:
  > pack_grounded_surface() may use glosses temporarily, but the
  > data model should already look like future SurfaceCandidate
  > input.  That avoids a second migration.

The integration uses a typed intermediate dataclass that matches the
selector's expected candidate shape:

  chat/pack_surface_candidate.py
    @dataclass(frozen=True, slots=True)
    class PackSurfaceCandidate:
        surface: str               # rendered final string
        grounding_source: str      # "pack" today
        pack_id: str               # provenance
        gloss: str | None          # reviewed natural-language form
        semantic_domains: tuple    # audit-trail content
        lemma: str
        pos: str
        is_user_facing_safe: bool  # honesty flag for selector
        is_fluent_sentence: bool   # gloss-backed vs. dotted-disclosure

When the SurfaceSelector lands:
  - This type becomes one variant in the selector's typed candidate
    union (alongside RefusalCandidate, TeachingCandidate, OOVCandidate).
  - pack_grounded_surface() becomes a provider that emits the
    candidate; the selector picks across providers' candidates by
    ranked authority + is_fluent_sentence preference.
  - No data migration — only the rendering step relocates.

Surface composition (chat/pack_grounding.py):

  build_pack_surface_candidate(lemma) -> PackSurfaceCandidate
    1. resolve_lemma(lemma) — required (None when OOV).
    2. resolve_gloss(lemma) — when present AND same pack as lexicon,
       compose POS-framed fluent sentence:
         "Truth is a claim or state grounded by evidence and
          coherent judgment. Pack-grounded (en_core_cognition_v1)."
       sets is_fluent_sentence=True.
    3. Else fallback to original ADR-0048 dotted-disclosure form:
         "truth — pack-grounded (en_core_cognition_v1):
          cognition.truth; logos.core; epistemic.ground.
          No session evidence yet."
       sets is_fluent_sentence=False.

  pack_grounded_surface(lemma) -> str | None
    Renders the candidate's surface field.  Returns None for OOV.
    Both fluent and disclosure surfaces carry the
    "pack-grounded ({pack_id})" provenance marker so existing
    substring-permissive tests continue to pass through the
    transition.

POS-framed sentence templates (_frame_gloss):
    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}."

Phase C glosses (sitting in /tmp from the 5-subagent parallel
dispatch) are authored to fit these frames exactly.

NO GLOSSES SHIP IN THIS COMMIT.  This is the wiring; the content
arrives in the Phase C commit.  Today every pack still emits the
original dotted-disclosure form because no glosses.jsonl exists yet
on any pack.

Verification:
  97/97 affected tests green (pack grounding, resolver, glosses,
  procedure surface, correction topic, meta pack).
  Cognition eval byte-identical on both splits.
  Live probe with no glosses: surface format identical to pre-fix.
This commit is contained in:
Shay 2026-05-19 07:26:46 -07:00
parent 24daebf3c1
commit 46ac737767
2 changed files with 232 additions and 22 deletions

View file

@ -83,39 +83,167 @@ def _pack_index() -> dict[str, tuple[str, ...]]:
return out 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 build_pack_surface_candidate(
lemma: str,
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
):
"""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"
surface = (
f"{_frame_gloss(key, gloss_pos, gloss_text)} "
f"Pack-grounded ({resolved_pack_id})."
)
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.
head = "; ".join(domains[:3])
surface = (
f"{key} — pack-grounded ({resolved_pack_id}): {head}. "
f"No session evidence yet."
)
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( def pack_grounded_surface(
lemma: str, lemma: str,
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS, pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
) -> str | None: ) -> str | None:
"""Return a deterministic pack-grounded surface for *lemma*, or ``None``. """Return a deterministic pack-grounded surface for *lemma*, or ``None``.
The surface format is fixed: Two surface forms, selected by gloss presence:
"{lemma} — pack-grounded ({resolved_pack_id}): {d1}; {d2}; {d3}. No session evidence yet." With gloss (preferred, lexicon-resident):
"{Lemma} is {gloss}. Pack-grounded ({pack_id})."
(frame varies by POS see :func:`_frame_gloss`)
ADR-0063 *resolved_pack_id* is the pack in *pack_ids* whose lexicon Without gloss (dotted-domain disclosure original ADR-0048 form):
contained *lemma* (first-match-wins). Cognition lemmas keep emitting "{lemma} — pack-grounded ({pack_id}): {d1}; {d2}; {d3}. No session evidence yet."
``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 Both forms carry the ``"pack-grounded ({pack_id})"`` provenance
pack are emitted; both come directly from the ratified pack lexicon, marker so substring-permissive tests continue to pass through
with no rewording. the transition.
Returns ``None`` when: The intermediate :class:`PackSurfaceCandidate` is the
- the lemma is empty or not a string, selector-ready shape; this function renders the candidate to a
- the lemma does not resolve in any of *pack_ids*. 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.
""" """
resolved = resolve_lemma(lemma, pack_ids) candidate = build_pack_surface_candidate(lemma, pack_ids)
if resolved is None: return candidate.surface if candidate is not None else 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: def is_pack_lemma(lemma: str) -> bool:

View file

@ -0,0 +1,82 @@
"""Typed surface candidate intermediate — selector-ready shape.
This module defines the dataclass that ``pack_grounded_surface`` and
related composers build BEFORE rendering to a final string. It is the
"deliberately narrow" integration the 2026-05-19 design review asked
for: gloss-backed surface delivery today, drop-in compatible with the
future ``SurfaceSelector`` registry.
When the selector lands:
- This candidate type becomes one variant in the selector's typed
candidate union (alongside RefusalCandidate, TeachingCandidate,
OOVCandidate, etc.).
- ``pack_grounded_surface()`` becomes a *provider* that emits this
candidate; the selector picks among providers' candidates by
ranked grounding authority.
- No data migration: the field layout below already matches the
review's specification (surface + grounding_source + intent slots
+ provenance + is_user_facing_safe + is_fluent_sentence).
Until the selector lands, ``pack_grounded_surface()`` builds and then
renders the candidate inline. The rendering step is the only thing
that needs relocation, not the candidate shape.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class PackSurfaceCandidate:
"""A pack-grounded surface candidate carrying its provenance.
Fields match the typed-candidate lattice the 2026-05-19 review
described so the future ``SurfaceSelector`` consumes this directly.
"""
# The final user-facing string (rendered). This IS what would
# appear in ChatResponse.surface when this candidate wins.
surface: str
# The ranked grounding-source tag — same enum the runtime emits.
# Today: "pack" for both gloss-backed and dotted-disclosure paths
# (the selector may later split "pack_gloss" / "pack_domains" if
# the ranked authority lattice wants finer-grained ordering).
grounding_source: str
# The pack that ratified the lemma — provenance, not text.
pack_id: str
# The reviewed natural-language gloss when present; None when the
# pack ships no gloss for this lemma and the candidate falls back
# to dotted-domain disclosure.
gloss: str | None
# The semantic_domains list verbatim from the pack's lexicon.
# Always present — this is the audit-trail content that survives
# gloss revision.
semantic_domains: tuple[str, ...]
# The looked-up lemma (lowercase, stripped).
lemma: str
# POS tag from the lexicon (NOUN/VERB/ADJ/ADV/...). Drives the
# sentence frame in renderer code.
pos: str
# Honesty flags the selector consults.
# True when the candidate emits a single sentence with terminal
# punctuation and no placeholder markers. Today this is True for
# gloss-backed and dotted-domain surfaces alike.
is_user_facing_safe: bool = True
# True when the candidate carries a reviewed gloss (vs. dotted
# domain disclosure). The selector may prefer fluent candidates
# over disclosure-style ones at the same grounding_source rank.
is_fluent_sentence: bool = False
__all__ = ["PackSurfaceCandidate"]