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:
parent
24daebf3c1
commit
46ac737767
2 changed files with 232 additions and 22 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
82
chat/pack_surface_candidate.py
Normal file
82
chat/pack_surface_candidate.py
Normal 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"]
|
||||||
Loading…
Reference in a new issue