core/docs/adr/ADR-0262-curriculum-grounded-serving.md
Shay efd3bee9ce chore(governance): stamp nine merged ADRs Accepted + pin the invariant
Nine ADRs (0254, 0256-0263) were merged into main and left stamped Proposed.
Two carried an explicit ratify-on-merge predicate their own merge had already
discharged, and ADR-0256 governs deduction_serving_enabled, which was ratified
True on 2026-07-24 and is serving live traffic. So the governance record
asserted "not yet decided" about a decision already in force.

That is the asymmetry the assessment arc (#113) found one file over in
workbench/api.py: the honest path degrades, the stale record lies. An unwritten
ADR is a visible gap; a Proposed one that is actually in force is a false
statement.

Each stamp records the ACTUAL ratifying act -- "Accepted, ratified by Joshua
Shay via <merge> (<sha>, <date>)" -- derived from the commit that added the file
and verified an ancestor of main, not assumed. Merge authority is Shay's alone
(AGENTS.md: no merge automation), so the merge IS the ratifying act.

No ADR content changed. No flag changed. ADR-0262's stamp says so explicitly:
accepting it does NOT enable curriculum_serving_enabled, which stays False
pending ratified volume -- eleven bands re-measured today, still 24x-73x short.

tests/test_adr_status_governance.py pins two independent invariants in the
smoke (pre-push) suite:

1. A default-ON flag is not governed by a Proposed ADR. The flag -> ADR mapping
   is DERIVED by walking core/config.py for `<name>: bool = True` and reading
   ADR refs from the preceding comment block -- not a hand-written table, which
   would be the same second-copy-of-a-closed-set defect ADR-0256's arc fixed.
2. A ratify-on-merge predicate cannot coexist with Proposed. Self-discharging:
   the file being on main IS the merge having happened.

Plus a vacuity guard, because a derivation that parses zero flags would make
every other assertion pass on an empty set.

Registered two orphans found in passing: test_adr_index.py (5) and
test_ratification_ceremony.py (14) landed in #113 in NO curated suite, so 19
tests -- including the one mechanism that can move curriculum volume -- ran
only under `full`, which gates nothing. Fifth instance of this shape.

Deliberately NOT fixed, recorded in the research doc: the 312-file corpus has
27 unparseable status lines and draft/ratified/active variants. A closed-vocab
assertion would fail on ~35 pre-existing files and get muted, and a muted gate
reads as coverage.

[Verification]: smoke 555 passed in 137.73s (236 baseline + 314 + 5, +1.2s);
governance pin 314 passed in 1.18s standalone and MUTATION-CHECKED -- reverting
ADR-0256 to Proposed fails both invariants independently (2 failed/312 passed);
orphans 19 passed; ruff clean. Canonical Python 3.12.13, uv sync --locked.
2026-07-25 14:38:02 -07:00

191 lines
11 KiB
Markdown
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.

# ADR-0262 — Curriculum-grounded serving: exams answered from what was taught
- **Status:** Accepted — ratified by Joshua Shay via the `feat/curriculum-serve-physics` merge (`0ae54ebb`, 2026-07-24). Accepting this ADR does NOT enable `curriculum_serving_enabled`, which stays False pending ratified curriculum volume (§5.1)
- **Date:** 2026-07-24
- **Arc:** generalization Phase 2 (docs/plans/generalization-arc-2026-07-24.md §4)
- **Governs:** `teaching/curriculum_premises.py`, `chat/curriculum_surface.py`,
`chat/curriculum_serve_license.py`, the `curriculum_<subject>_<family>` bands,
the `evals/curriculum_serve` lane and its independent oracle, and the
`curriculum_serving_enabled` flag.
## 1. Context
The band cascade (ADR-0256 → 0261) decides arguments whose premises are IN the
text. Exams are not shaped like that: the question supplies only the query, and
the premises are supposed to be what the student was taught. Until now CORE had
no path from "a ratified curriculum exists" to "a question is answered from
it" — the 16 domain seed packs and 5 domain-chain corpora were inert.
The generalization plan §4 fixes the epistemology for that path in advance, so
that implementing it cannot quietly become "answer from what the model knows".
This ADR implements §4.
## 2. Decision
Add a second serving composer, `chat/curriculum_surface.py`, behind the
default-off `curriculum_serving_enabled` flag:
1. **Closed question grammar**`Does <term> <relation> <term>?`. One shape.
Anything else refuses `question_shape_out_of_band` rather than guessing
which token is the relation. **The commit gate is routability, not shape**
(§5.4): the composer claims a turn only when the question parses AND its
terms are vocabulary a served subject teaches.
2. **Subject routing by vocabulary** — the question goes to the subject whose
ratified pack vocabulary contains BOTH terms. No match is
`untaught_vocabulary`; more than one is `ambiguous_reading`. The subject is
never guessed.
3. **Premise compilation from the ratified curriculum only**
(`teaching/curriculum_premises.py`). A chain is admitted iff
`review_status == "reviewed"` AND both terms are resident in the packs the
chain declares. Compilation is scoped to the question's relation family.
4. **The same decider** — compiled premises plus the question as a "Therefore"
conclusion are handed to the verb-predicate band (ADR-0260), falling through
to the existential band (ADR-0261), then the ROBDD engine. **No
subject-specific decision code exists anywhere in this path.**
5. **Bands = (subject × relation family)**, e.g. `curriculum_physics_causal`,
gated by the same earned-license machinery as deduction serving through a
second ledger reader (`chat/curriculum_serve_license.py`).
The relation family comes from the CONNECTIVE, not from a chain row's declared
`operator_family`. A question carries a relation word and nothing else; a
family derived from anything the question cannot carry would let premise
compilation and question routing disagree, and a taught edge could then be
missing from the premises compiled to decide it — a wrong answer built out of a
correct curriculum. Two corpus rows currently declare a family their connective
does not imply; under this rule they are read as their connective says.
## 3. Why this is sound
1. **Gold is a function of (curriculum, question).** No case-local hidden text,
no world knowledge. A lane case pins the chain ids it draws on and the
runner FAILS the case if a pinned chain is absent or unratified — so a
curriculum that changes under a case breaks loudly instead of quietly
answering from what remains.
2. **Open-world reading.** A relation the curriculum does not state is
UNKNOWN, never "no". Deciding otherwise would manufacture negative
knowledge from silence, which is the same failure as manufacturing positive
knowledge from silence.
3. **No composition.** The curriculum teaches `force causes acceleration` and
`acceleration causes motion`; it does not teach that causation composes, so
`force causes motion` is UNKNOWN. Transitivity of a causal or modal relation
is a substantive claim about the world, not a logical truth, and nothing
ratified asserts it. The independent oracle reports the shortest path length
with its verdict precisely so the lane can assert that a reachable pair is
still answered UNKNOWN — composition is provably not happening rather than
merely absent.
4. **Face-value relations.** `entropy reveals energy` does not license
`entropy causes energy`. Same family, different relation: the reader mints
distinct atoms per verb group (ADR-0260), so a family-level near-miss cannot
become an entailment.
5. **Independent gold.** `evals/curriculum_serve/oracle.py` shares no code with
the serving path — its own JSONL loader, ratification predicate, family
table, agreement normalization, and verdict rule. It disagreed with the
serving path once during authoring, on exactly the point in (4), and the
ORACLE was the side that was wrong; that disagreement is why the check
exists.
6. **Anti-recall is a lane guard, not a hope.** A split must carry ≥3 probes
whose answer is true in the world and absent from the curriculum, and no
probe may carry a committed gold, or the lane refuses to run.
## 4. License posture
Every `curriculum_*` band is UNEARNED today, so every answer is served
DISCLOSED through the same hedge deduction serving uses. This is not a
temporary shortcut around the license — it is the license working. The reason
is §5.1.
## 5. Findings
### 5.1 The binding constraint on Phase 2 is ratified curriculum VOLUME
A band earns SERVE at θ_SERVE=0.99 only with n ≥ 657 committed cases and a
genuine outcome mix. The question space of a subject is
`|vocabulary|² |vocabulary|`, so volume is reachable (physics: 240,
philosophy_theology: 22,650). **Entailed** cases are not: physics teaches 7
causal and 9 modal relations, so at most 16 distinct questions in the whole
subject can come back ENTAILED. Sampling cannot fix this — repeating the same
16 questions inflates n without adding information, and a band whose entailed
class is 1% of its volume is passed by a pipeline that never entails anything.
Concretely: a balanced band (≈⅓ entailed) needs **≈219 distinct taught
relations per subject × family**. Physics has 7 and 9. That is a ~25× gap, and
it is a curriculum-authoring and ratification task — Shay's call, not an
engineering one.
The honest consequence, recorded rather than worked around: the machinery,
the lane, the oracle, and the license gate all ship and pass; the licenses do
not exist yet; every answer is hedged until they do. The plan's O→S checkpoint
("≥1 subject lane serving wrong=0 with earned licenses") is met on every clause
except the last, and the last one cannot be met from present data.
### 5.2 REFUTED is unreachable from present corpora
Every ratified chain is a positive assertion. Refuting a question needs the
curriculum to teach a negative ("no X causes Y") or an exclusion. Until a
corpus does, `refuted` is a verdict this path can render but never reach. The
serving code handles it; the lane documents it.
### 5.3 The commit gate must be routability, not shape
Shipped first as a shape gate (`Does …?`) with the deduction composer's
fail-closed posture copied verbatim. That was wrong, and flipping the flag
would have exposed it: `Does the build pass?` and `Does anyone know the time?`
would have been taken away from the rest of dispatch and answered with a
remark about curriculum gaps.
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. So the gate became
`is_curriculum_question`: claim the turn only when the question parses to
three tokens AND its terms are vocabulary some served subject 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) — those are real curriculum questions with honest
answers. A question whose vocabulary CORE has never been taught is simply not
this composer's turn, and the DECIDER still records it as
`declined`/`untaught_vocabulary` for the lane.
**Generalizable rule:** a fail-closed composer is only as safe as its commit
gate is narrow. Before copying a fail-closed posture, check whether the new
gate carries the same evidence of intent as the one it was copied from.
### 5.4 There is no biology domain-chain corpus
The plan names "physics and biology" as Phase 2's two subjects because "OOD
lanes, seed packs, and domain chains already exist". For biology only the first
two are true: `evals/foundational_biology_ood` is a **fluency** lane (its own
contract says it measures grammatical English, not truth) and there is no
`biology_chains_*.jsonl`. The four subjects with ratified chains AND mounted
packs are physics, mathematics_logic, systems_software, and
philosophy_theology; the composer serves all four. The second subject should be
chosen from those, or biology's chain corpus authored first.
## 6. Scope-outs
- **Question shapes** beyond `Does <term> <relation> <term>?` — multi-word
terms, "why"/"how" questions, negated questions, and quantified questions all
refuse typed. Each is a separate readable unit with its own lane.
- **Composition**, per §3.3 — a future ADR may license it for a family the
curriculum itself declares transitive, but that requires the declaration to
be an executable claim rather than a classification label, which today it is
not.
- **Premise-set scaling** — a family with more than `MAX_PREMISE_SENTENCES`
(16) ratified chains compiles to an argument the reader refuses, surfacing as
`compiled_premises_unreadable`. No current family exceeds 9. The fix when one
does is to compile the query's k-hop neighborhood rather than the whole
family (sound: fewer premises can lose an entailment, never create one), and
to disclose the narrowing.
## 7. Verification
- `tests/test_curriculum_serve.py` — 20 tests: premise provenance and the
loud-failure guard, family scoping never losing a taught edge, taught-edge
entailment, untaught composition staying UNKNOWN, untaught fact never
REFUTED, relation read at face value, base-form question normalization,
anti-recall declines, 3 typed refusals, non-question pass-through, routing,
the disclosed-until-earned license posture and the authoritative path when a
band IS licensed, and oracle independence including the depth-2 reachability
assertion.
- `evals/curriculum_serve` lane: 32/32, wrong=0, 5 anti-recall probes, both
physics bands exercised. Pinned as `curriculum_serve_v1`.