core/tests/test_partial_surface.py
Shay 51aad0c2cd feat(adr-0065): OOV cliff → five-tier honesty gradient (Phase 2.1 + 2.2)
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.
2026-05-18 16:41:45 -07:00

145 lines
5.5 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.

"""Phase 2.2 — partial-grounding tier tests.
The contract these tests pin:
- When exactly one of the two compared lemmas resolves in a
mounted pack, the partial composer emits a hedged surface
grounding the known side and disclaiming the OOV side.
- When both resolve OR neither resolves, the composer returns
``None`` — the caller routes through the full pack-grounded
composer or the OOV invitation respectively.
- The OOV token is sanitised through ``safe_display``.
- Live runtime: COMPARISON with mixed-residency lemmas tags
``grounding_source="partial"``.
- Terminal punctuation on secondary_subject does not defeat
resolution.
"""
from __future__ import annotations
import pytest
from chat.partial_surface import partial_comparison_surface
from chat.runtime import ChatRuntime
# ---------------------------------------------------------------------------
# Pure-function contract
# ---------------------------------------------------------------------------
def test_oov_first_known_second_emits_partial() -> None:
result = partial_comparison_surface("photosynthesis", "knowledge")
assert result is not None
surface, known_side = result
assert known_side == "b"
assert "knowledge" in surface
assert "photosynthesis" in surface
assert "Whatever 'photosynthesis' is" in surface
assert "pack-grounded (en_core_cognition_v1)" in surface
assert "PackMutationProposal" in surface
def test_known_first_oov_second_emits_partial() -> None:
result = partial_comparison_surface("knowledge", "photosynthesis")
assert result is not None
surface, known_side = result
assert known_side == "a"
assert "Whatever 'photosynthesis' is" in surface
assert "I can ground 'knowledge'" in surface
def test_both_known_returns_none() -> None:
"""Both lemmas resolve — caller should route through the full
pack-grounded comparison composer instead."""
assert partial_comparison_surface("knowledge", "truth") is None
assert partial_comparison_surface("parent", "child") is None
def test_both_oov_returns_none() -> None:
"""Neither lemma resolves — partial-grounding has nothing to
anchor on. Caller routes to the OOV invitation."""
assert partial_comparison_surface("photosynthesis", "mitochondria") is None
assert partial_comparison_surface("aaa", "bbb") is None
def test_identical_lemmas_return_none() -> None:
"""Same-lemma comparison has no contrastive evidence at any tier."""
assert partial_comparison_surface("knowledge", "knowledge") is None
@pytest.mark.parametrize("bad", ["", " ", None])
def test_empty_or_invalid_lemma_returns_none(bad) -> None:
assert partial_comparison_surface(bad, "knowledge") is None # type: ignore[arg-type]
assert partial_comparison_surface("knowledge", bad) is None # type: ignore[arg-type]
def test_surface_is_deterministic() -> None:
a = partial_comparison_surface("photosynthesis", "knowledge")
b = partial_comparison_surface("photosynthesis", "knowledge")
assert a == b
def test_oov_side_is_safe_displayed() -> None:
"""The OOV token comes from user input and must pass through the
safe-display sanitiser; control chars do not leak."""
result = partial_comparison_surface("evil\x00token", "knowledge")
assert result is not None
surface, _ = result
assert "\x00" not in surface
# ---------------------------------------------------------------------------
# Live runtime — COMPARISON with mixed-residency lemmas
# ---------------------------------------------------------------------------
def test_runtime_comparison_known_oov_routes_to_partial() -> None:
rt = ChatRuntime()
resp = rt.chat("Compare knowledge and photosynthesis.")
assert resp.grounding_source == "partial"
assert "photosynthesis" in resp.surface
assert "knowledge" in resp.surface
def test_runtime_comparison_oov_known_routes_to_partial() -> None:
rt = ChatRuntime()
resp = rt.chat("Compare photosynthesis and knowledge.")
assert resp.grounding_source == "partial"
assert "knowledge" in resp.surface
def test_runtime_comparison_both_known_routes_to_pack() -> None:
"""Mixed residency triggers partial; both-known still hits the
full pack-grounded comparison composer (ADR-0050)."""
rt = ChatRuntime()
resp = rt.chat("Compare knowledge and truth.")
assert resp.grounding_source == "pack"
assert "contrasts with" in resp.surface
def test_runtime_comparison_both_oov_routes_to_oov() -> None:
rt = ChatRuntime()
resp = rt.chat("Compare photosynthesis and mitochondria.")
assert resp.grounding_source == "oov"
def test_runtime_comparison_cross_pack_known_routes_to_pack() -> None:
"""Cross-pack comparison (cognition × relations) still pack-grounds
when both lemmas resolve — the partial tier is a fallback, not
an interception."""
rt = ChatRuntime()
resp = rt.chat("Compare knowledge and parent.")
assert resp.grounding_source == "pack"
def test_runtime_terminal_punctuation_does_not_defeat_resolution() -> None:
"""The intent classifier may leave a trailing period on
secondary_subject ('Compare A and B.'). The runtime strips
terminal sentence punctuation at the COMPARISON boundary so
resolution finds the underlying lemma."""
rt = ChatRuntime()
resp = rt.chat("Compare knowledge and truth.")
# Without normalization, "truth." was OOV → fired partial.
# With normalization, "truth" resolves → pack.
assert resp.grounding_source == "pack"