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.
184 lines
6.8 KiB
Python
184 lines
6.8 KiB
Python
"""Phase 2.1 — OOV "teach me" surface tests.
|
|
|
|
The contract these tests pin:
|
|
|
|
- OOV tokens with a supported intent produce a deterministic
|
|
learning-invitation surface tagged ``grounding_source="oov"``.
|
|
- Known tokens still ground through the existing pack/teaching
|
|
paths byte-identically.
|
|
- The OOV surface names the unknown token, lists mounted packs,
|
|
and points at the reviewed PackMutationProposal path — never
|
|
invents meaning, never auto-mutates.
|
|
- UNKNOWN-intent prompts still get the universal disclosure
|
|
(the classifier itself failed to extract a confident subject).
|
|
- User-text passes through the safe-display sanitiser; control
|
|
chars do not leak into surfaces.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from chat.oov_surface import (
|
|
is_oov_for_packs,
|
|
oov_learning_invitation_surface,
|
|
)
|
|
from chat.runtime import ChatRuntime
|
|
from generate.intent import IntentTag
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pure-function contract
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("intent_tag", [
|
|
IntentTag.DEFINITION,
|
|
IntentTag.RECALL,
|
|
IntentTag.CAUSE,
|
|
IntentTag.VERIFICATION,
|
|
IntentTag.COMPARISON,
|
|
IntentTag.PROCEDURE,
|
|
IntentTag.CORRECTION,
|
|
])
|
|
def test_oov_token_with_supported_intent_emits_invitation(
|
|
intent_tag: IntentTag,
|
|
) -> None:
|
|
surface = oov_learning_invitation_surface("photosynthesis", intent_tag)
|
|
assert surface is not None
|
|
assert "photosynthesis" in surface
|
|
assert "haven't learned" in surface
|
|
assert "en_core_cognition_v1" in surface
|
|
assert "en_core_relations_v1" in surface
|
|
assert "PackMutationProposal" in surface
|
|
assert intent_tag.name.lower() in surface
|
|
|
|
|
|
def test_known_token_returns_none() -> None:
|
|
"""If the token IS resolvable, the composer returns None so the
|
|
caller routes through pack-grounded / teaching-grounded paths."""
|
|
assert oov_learning_invitation_surface("light", IntentTag.DEFINITION) is None
|
|
assert oov_learning_invitation_surface("parent", IntentTag.CAUSE) is None
|
|
assert oov_learning_invitation_surface("knowledge", IntentTag.VERIFICATION) is None
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["", " ", None])
|
|
def test_empty_or_invalid_token_returns_none(bad) -> None:
|
|
assert oov_learning_invitation_surface(bad, IntentTag.DEFINITION) is None # type: ignore[arg-type]
|
|
|
|
|
|
def test_unknown_intent_returns_none() -> None:
|
|
"""UNKNOWN intent means the classifier could not extract a
|
|
confident subject; emitting an invitation for an unparsed prompt
|
|
would be misleading."""
|
|
assert oov_learning_invitation_surface("photosynthesis", IntentTag.UNKNOWN) is None
|
|
|
|
|
|
def test_empty_pack_list_returns_none() -> None:
|
|
"""With no mounted packs there is no learnable destination;
|
|
the invitation has no targets to suggest."""
|
|
surface = oov_learning_invitation_surface(
|
|
"photosynthesis", IntentTag.DEFINITION, pack_ids=(),
|
|
)
|
|
assert surface is None
|
|
|
|
|
|
def test_surface_is_deterministic() -> None:
|
|
a = oov_learning_invitation_surface("photosynthesis", IntentTag.DEFINITION)
|
|
b = oov_learning_invitation_surface("photosynthesis", IntentTag.DEFINITION)
|
|
assert a == b
|
|
assert a is not None
|
|
|
|
|
|
def test_surface_includes_explicit_intent_name() -> None:
|
|
for tag, expected_token in [
|
|
(IntentTag.DEFINITION, "definition"),
|
|
(IntentTag.CAUSE, "cause"),
|
|
(IntentTag.VERIFICATION, "verification"),
|
|
(IntentTag.PROCEDURE, "procedure"),
|
|
]:
|
|
surface = oov_learning_invitation_surface("xyzunknown", tag)
|
|
assert surface is not None
|
|
assert f"(intent: {expected_token})" in surface
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Safety — user text is sanitised at the safe_display boundary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_control_characters_in_token_are_sanitised() -> None:
|
|
surface = oov_learning_invitation_surface(
|
|
"evil\x00\x07token", IntentTag.DEFINITION,
|
|
)
|
|
assert surface is not None
|
|
# Control bytes must not survive — safe_display strips/escapes them.
|
|
assert "\x00" not in surface
|
|
assert "\x07" not in surface
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_oov_for_packs predicate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_is_oov_for_packs_round_trips() -> None:
|
|
assert is_oov_for_packs("photosynthesis") is True
|
|
assert is_oov_for_packs("light") is False
|
|
assert is_oov_for_packs("parent") is False
|
|
assert is_oov_for_packs("") is False
|
|
assert is_oov_for_packs(" ") is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Live runtime — OOV converts cliff into gradient
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_runtime_definition_on_oov_emits_invitation() -> None:
|
|
rt = ChatRuntime()
|
|
resp = rt.chat("What is photosynthesis?")
|
|
assert resp.grounding_source == "oov"
|
|
assert "photosynthesis" in resp.surface
|
|
assert "PackMutationProposal" in resp.surface
|
|
|
|
|
|
def test_runtime_cause_on_oov_emits_invitation() -> None:
|
|
"""A CAUSE prompt on an OOV subject must hit the OOV branch, not
|
|
fall through to the universal disclosure. Previously these went
|
|
silent because the CAUSE/VERIFICATION branch early-returned None
|
|
when no teaching chain existed."""
|
|
rt = ChatRuntime()
|
|
resp = rt.chat("Why does mitochondria exist?")
|
|
assert resp.grounding_source == "oov"
|
|
assert "mitochondria" in resp.surface
|
|
|
|
|
|
def test_runtime_known_subject_still_grounds_pack() -> None:
|
|
"""Known cognition lemmas still ground through the pack path —
|
|
OOV branch is a fall-through only, not a replacement."""
|
|
rt = ChatRuntime()
|
|
resp = rt.chat("What is light?")
|
|
assert resp.grounding_source == "pack"
|
|
assert "light" in resp.surface
|
|
|
|
|
|
def test_runtime_known_subject_still_grounds_teaching() -> None:
|
|
"""Reviewed teaching chains still route through teaching-grounded."""
|
|
rt = ChatRuntime()
|
|
resp = rt.chat("Why does parent exist?")
|
|
assert resp.grounding_source == "teaching"
|
|
assert "parent" in resp.surface
|
|
|
|
|
|
def test_runtime_unknown_intent_still_emits_universal_disclosure() -> None:
|
|
"""If the classifier returns UNKNOWN intent, there is no clean
|
|
subject to invite an operator to teach. Universal disclosure
|
|
remains the right fall-through."""
|
|
rt = ChatRuntime()
|
|
resp = rt.chat("Define mitochondria.") # classifier returns UNKNOWN here
|
|
# Either UNKNOWN intent → universal disclosure ("none"), OR if
|
|
# the classifier improves to read "Define mitochondria." as
|
|
# DEFINITION the prompt should switch to OOV invitation. Both
|
|
# are acceptable.
|
|
assert resp.grounding_source in {"none", "oov"}
|