Phase 2 of the generalization arc, implementing the plan's §4 curriculum-entailment gold contract. "Does force cause acceleration?" is answered from the ratified physics chain corpus and nothing else; "Does gravity cause acceleration?" is declined because gravity is in no pack CORE has been taught; "Does force cause motion?" is unsettled even though the curriculum contains both links, because nothing ratified says causation composes. Path (flag-gated, default off): closed question grammar -> subject routing by ratified vocabulary -> family-scoped premise compilation from reviewed, pack-resident chains -> the SAME argument bands (ADR-0260/0261) -> the ROBDD engine. Zero subject-specific decision code: physics differs from philosophy only in which rows load. Epistemology enforced mechanically, not by convention: - gold is a function of (curriculum, question); cases pin chain ids and the runner FAILS a case whose pinned chain is absent or unratified; - untaught => UNKNOWN, never "no" (open-world; silence is not denial); - an independent oracle (own loader, ratification predicate, family table, agreement rules, verdict rule) re-derives every gold — it disagreed once, on "entropy reveals energy" vs "entropy causes energy", and the ORACLE was the side that was wrong; - anti-recall probes are a lane GUARD: a split without >=3 true-but-untaught probes refuses to run. Findings recorded rather than worked around (ADR-0262 §5): - every curriculum band is UNEARNED and every answer is hedged. A band needs n>=657 with a real outcome mix; physics teaches 7 causal + 9 modal relations, so at most 16 questions in the subject can ever be ENTAILED. A balanced band needs ~219 taught relations per subject×family — a ~25x gap that only ratified curriculum content closes. Phase 2's blocker is curriculum volume, not machinery. - REFUTED is unreachable from present corpora (every chain is positive). - there is NO biology domain-chain corpus; the biology OOD lane measures fluency, not truth. The four subjects with ratified chains and mounted packs are physics, mathematics_logic, systems_software, philosophy_theology — the composer serves all four. [Verification]: curriculum lane 32/32 wrong=0 with 5 anti-recall probes and all three contract guards passing; tests/test_curriculum_serve.py 20 passed; core test --suite deductive 252 passed; lane pinned as curriculum_serve_v1.
211 lines
8.3 KiB
Python
211 lines
8.3 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,
|
|
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
|
|
system has no curriculum to decide them from, and says so."""
|
|
decision = decide_curriculum_question(text)
|
|
assert decision.verdict == "declined"
|
|
assert decision.reason == "untaught_vocabulary"
|
|
surface = curriculum_grounded_surface(text)
|
|
assert surface is not None
|
|
assert "haven't been taught" in surface
|
|
assert "not a claim about the world" in surface
|
|
|
|
|
|
# --- 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
|