core/tests/test_curriculum_serve.py
Shay 013f36cc13 fix(curriculum-serve): narrow the commit gate to routability, not shape (ADR-0262 §5.3)
The curriculum composer committed on question SHAPE — any `Does …?` text —
and then, being fail-closed, always returned a surface. With the flag on,
"Does the build pass?" would have been taken away from the rest of dispatch
and answered "I haven't been taught the or pass"; "Does anyone know the
time?" likewise. Found by checking the flag's readiness before recommending
ratification; never live, since the flag is default-off.

The asymmetry is the lesson: the deduction composer may commit on shape
because a sentence-initial "therefore" IS a signal of intent — text shaped
that way is an argument. `Does …?` is one of the most common ways to open
any English question and signals nothing. A fail-closed composer is only as
safe as its commit gate is narrow.

The gate is now `is_curriculum_question`: claim the turn only when the
question parses to three tokens AND its terms are vocabulary a served
subject actually teaches. Everything past the gate stays fail-closed —
including `ambiguous_reading` (both terms taught, two subjects claim them)
and `out_of_curriculum` (terms taught, relation unknown), which are real
curriculum questions with honest answers. The DECIDER is unchanged, so the
lane still records untaught-vocabulary probes as declined: the curriculum
path declines them AND does not speak for them.

[Verification]: tests/test_curriculum_serve.py 28 passed (+8: five ordinary
`Does …?` questions pass through untouched, three routable ones still
answered); core test --suite deductive 268 passed; curriculum lane 32/32
wrong=0 with its pinned report SHA unchanged.
2026-07-24 15:43:47 -07:00

247 lines
10 KiB
Python

"""Curriculum-grounded serving (ADR-0262) — the Phase-2 contract.
These tests pin the epistemology, not just the plumbing: that answers come
from the ratified curriculum and nowhere else, that an untaught fact is
UNKNOWN rather than "no", that a chain the curriculum does not teach is never
composed into a claim, and that a question whose vocabulary the subject has
never been taught is declined rather than answered from world knowledge.
"""
from __future__ import annotations
import pytest
from chat.curriculum_serve_license import curriculum_serve_license
from chat.curriculum_surface import (
CurriculumQuery,
CurriculumRefusal,
band_for,
curriculum_grounded_surface,
decide_curriculum_question,
is_curriculum_question,
looks_like_curriculum_question,
read_curriculum_question,
resolve_domain,
resolve_family,
)
from evals.curriculum_serve.oracle import oracle_answer
from teaching.curriculum_premises import (
UnratifiedChain,
compile_premises,
load_curriculum,
resolve_pinned,
)
# --- the curriculum is the only premise source --------------------------------
def test_premises_come_only_from_ratified_chains() -> None:
curriculum = load_curriculum("physics")
premises, chain_ids = compile_premises(curriculum, "causal")
assert premises == (
"force causes acceleration",
"acceleration causes motion",
"work causes energy",
"charge causes field",
"field causes force",
"temperature reveals motion",
"entropy reveals energy",
)
assert all(cid.startswith("physics-") for cid in chain_ids)
def test_pinned_chain_that_is_not_ratified_fails_loudly() -> None:
"""§4.3 provenance: a case whose curriculum moved under it must break, not
quietly answer from what remains."""
curriculum = load_curriculum("physics")
with pytest.raises(UnratifiedChain):
resolve_pinned(curriculum, ("physics-causal-999",))
def test_family_scoping_never_loses_a_taught_edge() -> None:
"""Compilation is family-scoped; every taught edge is reachable from the
family its own connective implies, so scoping cannot hide an answer."""
curriculum = load_curriculum("physics")
for chain in curriculum.chains:
premises, _ = compile_premises(curriculum, chain.family)
assert chain.sentence in premises
# --- the verdicts that matter -------------------------------------------------
def test_taught_edge_is_entailed() -> None:
decision = decide_curriculum_question("Does force cause acceleration?")
assert decision.verdict == "entailed"
assert decision.band == band_for("physics", "causal")
def test_untaught_composition_is_unknown_not_entailed() -> None:
"""The curriculum teaches force→acceleration and acceleration→motion. It
does NOT teach that causation composes, so "force causes motion" is
unsettled. Composing it would be inventing a rule nobody taught."""
assert decide_curriculum_question("Does force cause motion?").verdict == "unknown"
def test_untaught_fact_is_unknown_never_refuted() -> None:
"""Open-world: silence is not denial. No question answerable from a purely
positive curriculum may come back "no"."""
for text in (
"Does mass require energy?", # true in the world, untaught
"Does motion cause force?", # the taught edge runs the other way
"Does work cause motion?", # both terms taught, no relation
):
assert decide_curriculum_question(text).verdict == "unknown", text
def test_relation_is_read_at_face_value() -> None:
""""entropy reveals energy" is taught; "entropy causes energy" is not. Same
family, different relation — the curriculum said one and not the other."""
assert decide_curriculum_question("Does entropy reveal energy?").verdict == "entailed"
assert decide_curriculum_question("Does entropy cause energy?").verdict == "unknown"
def test_question_may_spell_the_relation_in_its_base_form() -> None:
"""The question says "cause", the curriculum says "causes" — normalized by
the SAME closed agreement relation the verb band uses (ADR-0260)."""
assert decide_curriculum_question("Does charge cause field?").verdict == "entailed"
# --- anti-recall ---------------------------------------------------------------
@pytest.mark.parametrize(
"text", ["Does gravity cause acceleration?", "Does current cause field?"]
)
def test_untaught_vocabulary_declines_rather_than_recalling(text: str) -> None:
"""Both are true physics and both are outside every mounted pack. The
DECIDER declines — it has no curriculum to decide them from — and the
composer does not claim the turn at all, so the question reaches the rest
of dispatch instead of being answered with a remark about curriculum."""
decision = decide_curriculum_question(text)
assert decision.verdict == "declined"
assert decision.reason == "untaught_vocabulary"
assert curriculum_grounded_surface(text) is None
@pytest.mark.parametrize(
"text",
[
"Does the build pass?", # ordinary question, untaught terms
"Does it rain often?", # ordinary question, untaught terms
"Does anyone know the time?", # not even the three-token shape
"Does force accelerate?", # two tokens
"Does the force cause acceleration?", # four tokens
],
)
def test_ordinary_does_questions_are_not_claimed(text: str) -> None:
"""The commit gate is ROUTABILITY, not shape. "Does …?" is one of the most
common ways to open any English question; claiming every one of them would
take ordinary turns away from the rest of dispatch and answer them with a
curriculum remark. Only questions whose vocabulary a served subject
actually teaches are this composer's turn."""
assert curriculum_grounded_surface(text) is None
assert not is_curriculum_question(text)
@pytest.mark.parametrize(
"text",
[
"Does force cause acceleration?", # taught, decidable
"Does force explain acceleration?", # taught terms, unknown relation
"Does force cause motion?", # taught, unsettled
],
)
def test_routable_questions_are_claimed_and_answered(text: str) -> None:
"""Past the gate the composer stays fail-closed: every routable question
gets a committed, honest surface — including the one whose relation the
curriculum does not teach, since its TERMS plainly are curriculum terms."""
assert is_curriculum_question(text)
assert curriculum_grounded_surface(text) is not None
# --- typed refusals ------------------------------------------------------------
@pytest.mark.parametrize(
"text, reason",
[
("Does force explain acceleration?", "out_of_curriculum"),
("Does the force cause acceleration?", "question_shape_out_of_band"),
("Does force accelerate?", "question_shape_out_of_band"),
],
)
def test_typed_refusals(text: str, reason: str) -> None:
assert decide_curriculum_question(text).reason == reason
def test_non_question_text_is_not_claimed_at_all() -> None:
"""The composer returns None for anything that is not a ``Does …?``
question, so pre-existing dispatch is byte-identical."""
assert curriculum_grounded_surface("Force causes acceleration.") is None
assert not looks_like_curriculum_question("Is force a cause of acceleration?")
def test_question_reader_shape() -> None:
query = read_curriculum_question("Does force cause acceleration?")
assert isinstance(query, CurriculumQuery)
assert (query.subject, query.verb, query.obj) == ("force", "cause", "acceleration")
assert isinstance(read_curriculum_question("Why does it move?"), CurriculumRefusal)
def test_domain_and_family_routing() -> None:
query = read_curriculum_question("Does force cause acceleration?")
assert isinstance(query, CurriculumQuery)
assert resolve_domain(query) == "physics"
assert resolve_family(query) == "causal"
# --- the license posture -------------------------------------------------------
def test_answers_are_disclosed_until_a_band_earns_serve() -> None:
"""No curriculum band holds a SERVE license yet (ADR-0262 §5: ratified
curriculum volume, not machinery, is the binding constraint), so every
answer is served DISCLOSED — sound, and honest about its track record."""
assert curriculum_serve_license(band_for("physics", "causal")) is None
surface = curriculum_grounded_surface("Does force cause acceleration?")
assert surface is not None
assert surface.startswith("(answered from my curriculum")
def test_an_earned_band_is_served_authoritatively() -> None:
"""The gate is wired, not stubbed: hand it a licensed band and the hedge
disappears."""
class _Licensed:
licensed = True
surface = curriculum_grounded_surface(
"Does force cause acceleration?", license_lookup=lambda band: _Licensed()
)
assert surface is not None
assert not surface.startswith("(answered from my curriculum")
assert "physics curriculum teaches that force causes acceleration" in surface
# --- the independent oracle ----------------------------------------------------
def test_oracle_is_independent_and_agrees() -> None:
"""The oracle shares no code with the serving path; agreement across the
verdict classes is the evidence the compiler reads the curriculum right."""
for subject, relation, obj, expected in (
("force", "cause", "acceleration", "entailed"),
("force", "cause", "motion", "unknown"),
("gravity", "cause", "acceleration", "declined"),
):
assert oracle_answer("physics", subject, relation, obj).verdict == expected
def test_oracle_reports_reachability_without_claiming_it() -> None:
"""force→acceleration→motion is reachable at depth 2 and still UNKNOWN —
the property that proves composition is not being silently performed."""
verdict = oracle_answer("physics", "force", "cause", "motion")
assert verdict.verdict == "unknown"
assert verdict.depth == 2