Replaces the flat "I don't know — insufficient grounding" disclosure
with a deterministic gradient that names specific vocabulary gaps
and gives operators concrete next steps.
P2.1 — OOV "teach me" surface (chat/oov_surface.py).
When the intent classifier extracts a clean subject lemma but that
lemma is not resident in any mounted lexicon pack, the runtime now
emits a deterministic learning-invitation surface tagged
``grounding_source="oov"`` instead of the universal disclosure.
Surface format (fixed template):
"I haven't learned '{token}' yet (intent: {intent}).
Mounted lexicon packs: {pack_list}.
Teach me via a reviewed PackMutationProposal."
The OOV token passes through ``core._safe_display.safe_display``
before persistence — user-input sanitization at the trust boundary.
No vocabulary is invented; no domain is inferred. Honours the
ADR-0027 proposal-only invariant: the surface invites a reviewed
pack mutation, never silently mutates any pack.
Refactored ``_maybe_pack_grounded_surface`` so every existing
intent branch (COMPARISON / CAUSE / VERIFICATION / CORRECTION /
PROCEDURE / DEFINITION+RECALL) falls through on a None composer
result instead of early-returning. The OOV invitation is the
deterministic fall-through for any clean-subject prompt whose
subject doesn't resolve.
P2.2 — Partial-grounding tier (chat/partial_surface.py).
When exactly one of two COMPARISON lemmas resolves, the runtime
emits a hedged surface that grounds the known side verbatim and
disclaims the OOV side explicitly:
"Whatever '{oov}' is, I can ground '{known}' — pack-grounded
({pack_id}): {d1}; {d2}. I cannot ground the comparison
without learning '{oov}' — teach me via a reviewed
PackMutationProposal."
Tagged ``grounding_source="partial"``. Falls through to OOV
invitation when both lemmas are OOV, and to full pack-grounded
COMPARISON when both resolve — partial is the middle tier in the
five-tier gradient.
Also normalises trailing sentence punctuation on
intent.secondary_subject at the COMPARISON boundary so prompts
like "Compare A and B." (with the period) still resolve B
correctly.
Five-tier gradient (vault → teaching → pack → partial → oov → none).
Test debt retired: four pre-existing tests asserted "OOV → universal
disclosure", which is exactly the contract P2.1/P2.2 inverted.
Rewritten to the new contract. Plus test_procedure_surface.py
gained a test for the OOV gradient on procedure intents.
Verification:
tests/test_oov_surface.py 22 passed
tests/test_partial_surface.py 16 passed
Cognition eval byte-identical:
public 100% / 100% / 91.7% / 100%
holdout 100% / 100% / 83.3% / 100%
Curated lanes all green.
115 lines
4.3 KiB
Python
115 lines
4.3 KiB
Python
"""chat/partial_surface.py — Phase 2.2: partial-grounding tier.
|
|
|
|
When a prompt contains both an OOV token AND a pack-resident token,
|
|
the runtime today has two choices:
|
|
|
|
1. ``pack_grounded_*`` composers — require *both* tokens to resolve
|
|
(ADR-0050 COMPARISON: identical-lemma → None; OOV-lemma → None).
|
|
2. The OOV invitation (P2.1) — names one unknown token but ignores
|
|
the known one entirely.
|
|
|
|
Both miss a real signal: the known token is actually grounded, the
|
|
relation is partially representable, and the operator deserves to
|
|
see *which* side is OOV instead of a flat "I don't know one of these".
|
|
|
|
This module composes a **partial-grounding** surface that:
|
|
|
|
- Grounds the pack-resident token verbatim from its lexicon
|
|
(same atoms a full pack-grounded surface would emit).
|
|
- Names the OOV token explicitly under a "whatever ... is" hedge —
|
|
no synthesis, no inferred meaning, no domain guess.
|
|
- States the contract: the relation cannot be grounded until the
|
|
OOV token is ratified into a pack.
|
|
- Tags ``grounding_source="partial"`` so audit and downstream
|
|
aggregation distinguish this from full pack-grounded
|
|
surfaces or the universal disclosure.
|
|
|
|
Today's scope is the COMPARISON intent (two subject lemmas, one OOV +
|
|
one known). CAUSE/VERIFICATION extract a single subject; if it's
|
|
OOV the OOV invitation surface (P2.1) is the right surface — there
|
|
is no second lemma to partially ground against. Future ADRs can
|
|
extend partial-grounding to other intent shapes as the classifier
|
|
grows multi-lemma extraction.
|
|
|
|
Trust boundary:
|
|
- The partial surface composes only the known-side lexicon atoms,
|
|
the (safely-displayed) OOV token, and a fixed template.
|
|
- No vocabulary is invented; no meaning is inferred for the OOV
|
|
side.
|
|
- The trailing instruction points at the reviewed pack-mutation
|
|
path — partial grounding never auto-mutates state.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from chat.pack_resolver import DEFAULT_RESOLVABLE_PACK_IDS, resolve_lemma
|
|
from core._safe_display import safe_display
|
|
|
|
|
|
def partial_comparison_surface(
|
|
lemma_a: str,
|
|
lemma_b: str,
|
|
*,
|
|
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
|
) -> tuple[str, str] | None:
|
|
"""Return ``(surface, known_side)`` where ``known_side`` is ``"a"``
|
|
or ``"b"`` depending on which lemma resolved, or ``None``.
|
|
|
|
The surface format is fixed:
|
|
|
|
"Whatever '{oov}' is, I can ground '{known}'
|
|
— pack-grounded ({pack_id}): {d1}; {d2}.
|
|
I cannot ground the comparison without learning '{oov}' —
|
|
teach me via a reviewed PackMutationProposal."
|
|
|
|
The composer returns ``None`` when:
|
|
|
|
- either lemma is empty / not a string,
|
|
- both lemmas resolve (route through the full
|
|
``pack_grounded_comparison_surface`` instead),
|
|
- neither lemma resolves (route through the OOV invitation;
|
|
partial-grounding has nothing to anchor on),
|
|
- the two lemmas are identical strings (same-lemma comparison
|
|
carries no contrastive evidence at any tier).
|
|
"""
|
|
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 or key_a == key_b:
|
|
return None
|
|
|
|
resolved_a = resolve_lemma(key_a, pack_ids)
|
|
resolved_b = resolve_lemma(key_b, pack_ids)
|
|
# Partial-grounding requires exactly one side to resolve.
|
|
if resolved_a is None and resolved_b is None:
|
|
return None
|
|
if resolved_a is not None and resolved_b is not None:
|
|
return None
|
|
|
|
if resolved_a is not None:
|
|
known_lemma = key_a
|
|
known_pack_id, known_domains = resolved_a
|
|
oov_lemma = key_b
|
|
known_side = "a"
|
|
else:
|
|
assert resolved_b is not None
|
|
known_lemma = key_b
|
|
known_pack_id, known_domains = resolved_b
|
|
oov_lemma = key_a
|
|
known_side = "b"
|
|
|
|
safe_oov = safe_display(oov_lemma)
|
|
head = "; ".join(known_domains[:2])
|
|
surface = (
|
|
f"Whatever '{safe_oov}' is, I can ground '{known_lemma}' "
|
|
f"— pack-grounded ({known_pack_id}): {head}. "
|
|
f"I cannot ground the comparison without learning '{safe_oov}' "
|
|
f"— teach me via a reviewed PackMutationProposal."
|
|
)
|
|
return (surface, known_side)
|
|
|
|
|
|
__all__ = ["partial_comparison_surface"]
|