core/tests/test_transitive_surface.py
Shay 9e6fa4be75
feat(adr-0083): transitive (multi-hop) teaching-grounded surface (#63)
Strict superset of ADR-0062's depth-1 composer.  `max_depth` is the
number of follow-up hops appended beyond the initial chain:

  max_depth=0  → byte-identical to single-chain surface
  max_depth=1  → byte-identical to ADR-0062 composed
  max_depth=2  → byte-identical to ADR-0062 when no second hop
                 survives, strict superset when one does

The composer surfaces content the realizer was silently dropping
from chains already ratified in `cognition_chains_v1`.  Example
live lift on `"Why does light exist?"`:

  composed: "light reveals truth, which grounds knowledge."
  transitive(2): "...which grounds knowledge, which requires evidence."

Cycle-safe at every depth via a single visited-set; single-corpus
traversal in v1 (cross-corpus transitive deferred to a follow-up
ADR alongside ADR-0064's cross-pack model).

Both flags default False — every existing surface is preserved
byte-identically.  When both `composed_surface` and
`transitive_surface` are True, transitive wins.

Implementation:
- `core/config.py`: `transitive_surface: bool = False`,
  `transitive_max_depth: int = 2`.
- `chat/teaching_grounding.py`: `_resolve_followup` shared helper
  refactored out of the depth-1 composer (no behavioural change),
  plus new `teaching_grounded_surface_transitive(subject,
  intent_tag, *, max_depth)`.
- `chat/runtime.py`: dispatch order — transitive > composed > single.

Verification:
- tests/test_transitive_surface.py: 16 new tests covering pure-fn
  contract, visited-set cycle guard at every depth, runtime
  integration, and the cognition-lane null-drop invariant at
  `max_depth=2` (public + holdout splits).
- tests/test_composed_surface.py: 11/11 pass after the helper
  refactor (ADR-0062 behaviour preserved).
- `core test --suite smoke`: 67 pass.
- `core test --suite cognition`: 120 pass, 1 skipped.
- `core test --suite teaching`: 17 pass.
- `core eval cognition`: 100 / 91.7 / 100 / 100 (byte-identical).
2026-05-20 14:11:40 -07:00

269 lines
10 KiB
Python

"""ADR-0083 — transitive (multi-hop) teaching-grounded surface.
Strict superset of ADR-0062's depth-1 composer. ``max_depth`` is the
number of follow-up hops appended beyond the initial chain. This
test file pins:
- Pure function: ``None`` when no chain; depth-0 == single-chain;
depth-1 == ADR-0062 composed; depth-2 is a strict superset when
a second hop survives.
- Visited-set guard: blocks cycles at every depth.
- Single-corpus traversal: cross-corpus follow-ups are refused in v1.
- Determinism: same input → same surface bytes.
- Runtime: ``transitive_surface=True`` supersedes
``composed_surface``; flags observable on frozen config.
- Cognition lane: metrics byte-identical flag OFF vs ON at
``max_depth=2`` on both public and holdout splits (null-drop
invariant — transitive mode adds clauses, never drops tokens).
"""
from __future__ import annotations
from dataclasses import replace
from core.config import RuntimeConfig
from chat.runtime import ChatRuntime
from chat.teaching_grounding import (
teaching_grounded_surface,
teaching_grounded_surface_composed,
teaching_grounded_surface_transitive,
)
from generate.intent import IntentTag
# ---------------------------------------------------------------------------
# Pure-function contract
# ---------------------------------------------------------------------------
def test_transitive_returns_none_when_no_chain() -> None:
out = teaching_grounded_surface_transitive(
"zzznotalemma", IntentTag.CAUSE, max_depth=2,
)
assert out is None
def test_transitive_depth_zero_matches_single_chain() -> None:
"""``max_depth=0`` appends zero follow-ups → byte-identical to the
single-chain surface for any (subject, intent) the corpus knows."""
for lemma, intent in (
("light", IntentTag.CAUSE),
("memory", IntentTag.VERIFICATION),
("knowledge", IntentTag.CAUSE),
):
trans = teaching_grounded_surface_transitive(lemma, intent, max_depth=0)
single = teaching_grounded_surface(lemma, intent)
assert trans is not None
assert single is not None
assert trans == single, f"depth-0 != single for {lemma}/{intent}"
def test_transitive_depth_one_matches_composed() -> None:
"""``max_depth=1`` appends one follow-up → byte-identical to
ADR-0062's depth-1 composer."""
for lemma, intent in (
("light", IntentTag.CAUSE),
("memory", IntentTag.VERIFICATION),
("knowledge", IntentTag.CAUSE),
):
trans = teaching_grounded_surface_transitive(lemma, intent, max_depth=1)
composed = teaching_grounded_surface_composed(lemma, intent)
assert trans is not None
assert composed is not None
assert trans == composed, (
f"depth-1 != composed for {lemma}/{intent}"
)
def test_transitive_depth_two_is_strict_superset_for_light_cause() -> None:
"""``light cause``: chain ``light reveals truth``, follow-ups
``truth grounds knowledge`` (hop 1) and ``knowledge requires
evidence`` (hop 2) — depth-2 emits both ``knowledge`` AND
``evidence`` clauses, strict superset of ADR-0062."""
depth2 = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
composed = teaching_grounded_surface_composed("light", IntentTag.CAUSE)
assert depth2 is not None
assert composed is not None
assert depth2 != composed
assert composed in depth2[: len(composed) - 1] or "knowledge" in depth2
assert "evidence" in depth2
# Each appended hop adds a ", which " linker — depth-2 has two.
assert depth2.count(", which ") == 2
def test_transitive_depth_two_includes_every_intermediate_object() -> None:
"""Pack-grounded discipline: the initial subject and every hop
object appear as a clause subject in the depth-2 surface."""
depth2 = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
assert depth2 is not None
for token in ("light", "truth", "knowledge", "evidence"):
assert token in depth2, f"{token} missing from depth-2 surface"
def test_transitive_depth_two_includes_every_intermediate_domain() -> None:
"""Every hop's object semantic_domains appear verbatim."""
from chat.pack_grounding import _pack_index
pack = _pack_index()
depth2 = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
assert depth2 is not None
for obj in ("truth", "knowledge", "evidence"):
domains = pack[obj]
assert any(d in depth2 for d in domains[:1]), (
f"no domain of {obj!r} in depth-2 surface"
)
def test_transitive_cycle_guard_blocks_one_step_cycle() -> None:
"""``memory verification`` → ``memory requires recall``; the only
follow-up candidate ``recall cause`` is ``recall reveals memory``
which would re-introduce ``memory`` (1-step cycle). Even at
``max_depth=4`` no hop survives → degrades to single-chain."""
trans = teaching_grounded_surface_transitive(
"memory", IntentTag.VERIFICATION, max_depth=4,
)
single = teaching_grounded_surface("memory", IntentTag.VERIFICATION)
assert trans == single
def test_transitive_visited_set_blocks_deeper_cycle() -> None:
"""``light cause`` reaches ``evidence`` at hop 2. No chain in the
cognition corpus has subject ``evidence`` and a cause / verification
object outside ``{light, truth, knowledge, evidence}``, so depth-3
is identical to depth-2 (the depth-3 candidate, if any, must be
refused by the visited-set guard or the per-hop terminator)."""
depth2 = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
depth3 = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=3,
)
assert depth2 == depth3
def test_transitive_is_deterministic() -> None:
a = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
b = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
assert a == b
def test_transitive_preserves_trust_label() -> None:
trans = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
assert trans is not None
assert trans.endswith("No session evidence yet.")
assert "teaching-grounded (cognition_chains_v1)" in trans
def test_transitive_negative_max_depth_clamps_to_zero() -> None:
"""Misconfigured negative max_depth clamps to 0 (single-chain)."""
out = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=-5,
)
single = teaching_grounded_surface("light", IntentTag.CAUSE)
assert out == single
# ---------------------------------------------------------------------------
# Runtime integration via the config flags
# ---------------------------------------------------------------------------
def test_runtime_default_does_not_engage_transitive() -> None:
"""Both flags default False → single-chain surface."""
rt = ChatRuntime(config=RuntimeConfig())
response = rt.chat("Why does light exist?")
expected = teaching_grounded_surface("light", IntentTag.CAUSE)
assert response.surface == expected
def test_runtime_transitive_supersedes_composed() -> None:
"""When both flags are on, transitive wins (strict superset of
composed at default ``max_depth=2``)."""
cfg = replace(
RuntimeConfig(),
composed_surface=True,
transitive_surface=True,
)
rt = ChatRuntime(config=cfg)
response = rt.chat("Why does light exist?")
expected = teaching_grounded_surface_transitive(
"light", IntentTag.CAUSE, max_depth=2,
)
assert response.surface == expected
# Strict superset over ADR-0062 on this prompt.
assert response.surface != teaching_grounded_surface_composed(
"light", IntentTag.CAUSE,
)
def test_runtime_transitive_at_depth_one_matches_composed() -> None:
cfg = replace(
RuntimeConfig(),
transitive_surface=True,
transitive_max_depth=1,
)
rt = ChatRuntime(config=cfg)
response = rt.chat("Why does light exist?")
expected = teaching_grounded_surface_composed("light", IntentTag.CAUSE)
assert response.surface == expected
def test_runtime_flags_observable_on_frozen_config() -> None:
cfg = replace(
RuntimeConfig(),
transitive_surface=True,
transitive_max_depth=3,
)
assert cfg.transitive_surface is True
assert cfg.transitive_max_depth == 3
default = RuntimeConfig()
assert default.transitive_surface is False
assert default.transitive_max_depth == 2
# ---------------------------------------------------------------------------
# Cognition-lane null-drop invariant (transitive adds clauses, never drops)
# ---------------------------------------------------------------------------
def test_cognition_lane_metrics_unchanged_with_transitive_flag() -> None:
"""At ``max_depth=2``, every expected_term and
expected_surface_contains assertion that passed flag-OFF must still
pass flag-ON. If a future change ever drops tokens in transitive
mode, this test fails as the deliberate regression it is."""
from evals.framework import get_lane, run_lane
lane = get_lane("cognition")
watched = (
"intent_accuracy",
"surface_groundedness",
"term_capture_rate",
"versor_closure_rate",
)
on_cfg = replace(
RuntimeConfig(),
transitive_surface=True,
transitive_max_depth=2,
)
for split in ("public", "holdout"):
off = run_lane(lane, version="v1", split=split,
config=RuntimeConfig()).metrics
on = run_lane(lane, version="v1", split=split,
config=on_cfg).metrics
for m in watched:
assert off[m] == on[m], (
f"ADR-0083 null-drop invariant broken on split={split!r} "
f"metric={m!r}: OFF={off[m]} vs ON={on[m]}. "
f"Transitive surface should add clauses, never drop tokens."
)