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.
247 lines
10 KiB
Python
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
|