Rewrites the Q1-D ASK bus delivery scoping document into the durable decision record for decisions implemented in #668. No code or runtime behavior changes.
15 KiB
Q1-D — ASK bus delivery (QUESTION_NEEDED tenant) — decision record
Date: 2026-06-08 · Status: ACCEPTED + IMPLEMENTED (PR #668, merge commit
07149c20) · Branch: docs/q1-d-ask-bus-delivery-scoping
Decision record, not an open brief. All five decisions below (D1–D5) were ruled and are implemented in PR #668. This doc is retained as the durable record of what was decided and why; the implementation lives in
core/epistemic_questions/delivery.py,generate/contemplation/findings.py(Terminal.QUESTION_NEEDED),core/epistemic_disclosure/limitation.py(the now-totalterminal_for_action), andtests/test_question_delivery.py. Where this doc and the code disagree, the code is authoritative. Two served-surface decisions remain open and are deferred to dedicated scoping docs (see §7):ask_serving_enabledand the registry/carve-out flip.
What this records. The decisions for the fourth and last Q1 rung — delivery.
Q1-A (limitation pass), Q1-B (typed residue + missing_* reclassification,
Q1B_ASK_CARVE_OUT), and Q1-C (the grounded-only renderer, strictly single-slot) all
shipped off-serving; Q1-D (also off-serving) takes the rendered question and routes
it onto Doc 1's Epistemic Disclosure Bus as the second tenant (QUESTION_NEEDED),
behind the first (VERIFIED). Each decision is pinned against the shipped code it
landed on.
Design of record: the session doc
docs/sessions/2026-06-08-epistemic-question-articulation-first-skill-of-contemplation.md(§4 theQUESTION_NEEDEDterminal, §5/§1.5.8 bus integration). Frontier companions: q1-epistemic-question-articulation-v1-scoping-2026-06-08 §5/§7 (Q1-D = bus delivery), stage2-epistemic-disclosure-bus-verified-v1-scoping-2026-06-08 (the bus VERIFIED is first tenant of).
The one-line shape.
LimitationAssessment(ask_question, missing_slots)
→ choose_served_disposition(...) = ServedDisposition.ASK [P0-3, shipped]
→ render_question(assessment) = EpistemicQuestion [Q1-C, shipped]
→ Q1-D: package → Terminal.QUESTION_NEEDED + DeliveredQuestion [THIS RUNG]
→ (off-serving sink: written like a proposal, never auto-served)
The hard constraint from the steer. Q1-D does not render. It consumes only
the EpistemicQuestion the Q1-C renderer produced. This preserves the rung
separation that makes each layer independently auditable:
Q1-B: typed residue (what is missing, as typed slots)
Q1-C: renderability (can it be asked without fabricating? → EpistemicQuestion)
Q1-D: delivery (route the rendered question onto the bus)
If Q1-D re-derived or re-rendered, the wrong=0 grounded-rendering guard (Q1-C
_names_only_grounded) would be bypassed by a second, ungoverned surface — the
exact parallel-path anti-pattern the consolidation discipline forbids.
1. State of the substrate Q1-D lands on (verified on branch)
| Piece | Status | Q1-D dependency |
|---|---|---|
ServedDisposition + choose_served_disposition (core/epistemic_disclosure/disposition.py) |
shipped (P0-3) | ask_question → ASK already mapped; zero runtime consumers |
EpistemicQuestion + render_question (core/epistemic_questions/render.py) |
shipped (Q1-C) | the only input Q1-D consumes; strictly single-slot |
Terminal enum (generate/contemplation/findings.py) |
shipped, 7 members | QUESTION_NEEDED absent — Q1-D adds it (sibling to PROPOSAL_EMITTED) |
LimitationAssessment / MissingSlot (core/epistemic_disclosure/limitation.py) |
shipped (Q1-A/B) | source of the assessment; Q1B_ASK_CARVE_OUT still live |
failure-family REGISTRY (core/comprehension_attempt/failure_family.py) |
shipped | missing_* families still proposal_allowed=True (carve-out) |
contemplation proposal sink (teaching/proposals/) |
shipped | the template for the off-serving question sink |
Two facts set the altitude:
- Nothing consumes
ServedDisposition.ASKorEpistemicQuestiontoday. The bus decision table is a pure mapping with no caller. Q1-D is therefore the first consumer of the ASK disposition — which is exactly why it must be scoped, not improvised. - The bus is itself still off-serving. The VERIFIED tenant (P1) was built as a
proof-object producer in
evals/; the served-surface wiring was deferred to a named decision (verified_serving_enabled). Q1-D inherits the same two-layer split: an off-serving delivery layer buildable now, and a served-surface wiring layer that waits on a ruling.
2. What Q1-D shipped (off-serving) — ACCEPTED
Decision: build the off-serving delivery layer; defer served-surface wiring to a
named gate. Implemented in #668. This mirrors the VERIFIED lane exactly (P1-B
produced proof objects off-serving; serving wiring is a separate, gated decision).
One scope refinement landed with the code: the delivery layer ships standalone —
deliver_ask/emit_question are tested but not yet called from pass_manager
(mirrors P1-B shipping the producer without wiring verify.py); pass-manager wiring is
a deliberate follow-up, folded into the §7 ASK serving-integration scope.
2.1 Terminal.QUESTION_NEEDED — the off-serving sink
Add QUESTION_NEEDED as an eighth contemplation terminal, a sibling of
PROPOSAL_EMITTED (the session doc §4 already specifies this: "not a subtype of
PROPOSAL_EMITTED; sibling terminals"). The contemplation pass, when an
ask_question assessment renders successfully, terminates QUESTION_NEEDED carrying
the DeliveredQuestion artifact — just as PROPOSAL_EMITTED carries the proposal,
and just as proposal-only never auto-installs. The artifact is written to a sink
(teaching/proposals/-analogue, e.g. teaching/questions/ or a
questions/ JSONL), reviewable, never delivered to a user by this rung.
2.2 DeliveredQuestion — the delivery artifact (wraps, never re-renders)
A new frozen dataclass that wraps the Q1-C EpistemicQuestion and adds the
delivery-level fields the assessment already carries — it does not promote the
renderer to the session-doc's richer aspirational model:
DeliveredQuestion (frozen):
question: EpistemicQuestion # verbatim from render_question — never rebuilt
owner_organ: str # from LimitationAssessment.owner_organ
blocking_reason: str # from LimitationAssessment.blocking_reason
answer_binding: AnswerBinding | None # RESERVED (Q2); None in Q1-D
source_attempt_id: str | None # provenance, if the attempt supplied one
AnswerBinding is reserved, not implemented — the seat exists so the Q2
round-trip is wireable without reshaping, but Q1-D binds no answers (session doc §4:
"answer-binding must re-enter the gate", a later batch).
2.3 Off-serving guarantee
The delivery module imports no generate.derivation / core.reliability_gate
(AST-checked, same test shape as Q1-C test_renderer_is_off_serving). It cannot move
the sealed GSM8K metric or any pinned SHA. No served surface, no chat/runtime.py
wiring, no CLAIMS.md change.
3. The decisions — ALL ACCEPTED + IMPLEMENTED (#668)
These were the load-bearing choices Q1-D forced. Each was ruled as its recommendation and is implemented in #668; the rulings are recorded inline (Ruling:) under each.
D1 — Off-serving delivery now, or wait for the bus to be served? — ACCEPTED: now
The Q1 scoping doc (§7.4) said "Q1-D requires the bus to be live (Stage 2 S2-A..C)." But Stage 2 shipped its first tenant off-serving (P1 is a proof-object producer; serving wiring deferred). So the literal precondition ("bus live") never materialised as served — it materialised as off-serving infrastructure.
- Recommend: build Q1-D's off-serving delivery layer now (matches the VERIFIED
precedent), and defer served delivery to a named gate
ask_serving_enabled— the ASK analogue ofverified_serving_enabled. Q1-D off-serving produces and testsQUESTION_NEEDED+DeliveredQuestion; nothing reaches a user. - Alternative: hold Q1-D entirely until the bus has a served surface. Costs the off-serving proof object that the VERIFIED lane found valuable; gains nothing the gate doesn't already protect.
D2 — The unrenderable fallback (the wrong=0-adjacent decision) — ACCEPTED
When Q1-C returns unrenderable (renderability_gap, multi_slot_not_supported,
fabrication_guard, no_missing_slot), Q1-D must not terminate QUESTION_NEEDED
with no question — that would be a contentless ASK, the delivery-side equivalent of a
fabricated answer.
- Recommend: an unrenderable ASK falls back to the family's standing
disposition — for the
missing_*carve-out families that isPROPOSAL_EMITTED(they areproposal_allowed=Truetoday); for an ambiguous-structure family with no renderable slot it isNO_PROGRESS. The rule: anaskclassification that cannot be rendered never produces a dead end and never produces an empty question. This is the delivery-layer guard, and it gets a test that fails if an unrenderable assessment ever reachesQUESTION_NEEDED. - This is why the carve-out (D3) must not flip yet — see below.
D3 — Does Q1-D resolve the Q1B_ASK_CARVE_OUT? — ACCEPTED: no, carve-out stays
Q1-B deliberately kept REGISTRY proposal_allowed=True for missing_total_count
/ missing_weighted_total and added the disclosure-layer Q1B_ASK_CARVE_OUT, with the
stated condition: no silent re-key, no proposal-signal dead zone "until ASK delivery
lands." Q1-D is ASK delivery landing — so does the registry flip now?
- Recommend: no — the carve-out persists until
ask_serving_enabled. Off-servingQUESTION_NEEDEDdoes not yet replace the proposal channel a real user would see. If the registry flippedproposal_allowed → askwhile serving still proposes, the carve-out's own dead-zone hazard reappears (a family classifiedaskwith no served ask path). The flip is therefore gated on serving delivery, not off-serving delivery. Q1-D records this explicitly so the carve-out's resolution condition is unambiguous: the carve-out closes whenask_serving_enabledis the ruling, not when off-servingQUESTION_NEEDEDships. - The D2 fallback (unrenderable ASK → standing disposition) is what makes this safe:
while the carve-out stands, the registry's
proposal_allowed=Trueremains the truthful fallback target.
D4 — Where does the off-serving sink write? — ACCEPTED: teaching/questions/
PROPOSAL_EMITTED writes to teaching/proposals/. The question sink is a sibling.
- Recommend:
teaching/questions/(or aquestions.jsonlbesideproposals.jsonl) — same proposal-only, review-gated, HITL-visible discipline; never auto-served. The artifact is theDeliveredQuestionserialized deterministically (frozen → hashable → replayable). Confirm the path/name; it is a one-line decision, not architecture.
D5 — Single-slot carries through (no fan-out in Q1-D) — ACCEPTED
Q1-C is strictly single-slot (the multi_slot_not_supported refusal shipped in
this same review). Q1-D therefore delivers at most one DeliveredQuestion per
assessment; a multi-slot assessment renders unrenderable and takes the D2 fallback.
One-question-per-slot fan-out is not Q1-D — it is a later rung that must first
solve the "which slot first / minimal-sufficient" ranking (session doc §1.4, rank.py,
Q2+). Stated here so delivery does not silently grow a fan-out.
4. What Q1-D is NOT (non-claims)
- Not a served surface. No question reaches a user;
chat/runtime.pyis untouched; the served-surface wiring is the deferredask_serving_enabledgate (D1). - Not a re-renderer. Q1-D consumes the Q1-C
EpistemicQuestionverbatim; it never builds prose, so the grounded-rendering wrong=0 guard is never bypassed. - Not the registry flip.
missing_*staysproposal_allowed=True; theQ1B_ASK_CARVE_OUTpersists untilask_serving_enabled(D3). - Not Q2 answer-binding.
AnswerBindingis a reserved seat, bound to nothing; the answer round-trip re-enters the limitation gate in a later batch (session doc §4). - Not a fan-out. One slot, one question — multi-slot takes the unrenderable fallback (D5).
- Not a metric/SHA/
CLAIMS.mdmove — off-serving, AST-verified.
5. Verification lanes (as shipped in #668)
tests/test_question_delivery.py(14 tests) —ask_question+ renderable assessment →Terminal.QUESTION_NEEDED+ aDeliveredQuestionwrapping the exact Q1-CEpistemicQuestion; both D2 fallback branches (proposing →PROPOSAL_EMITTED, non-proposing →NO_PROGRESS); the structural guards (aDeliveredQuestioncannot wrap an unrenderable question / be served / carry anAnswerBinding); the idempotent content-addressed sink + no-artifact-on-fallback; the off-serving AST test.tests/test_limitation_assessment.py— the two consolidation tests updated for the now-totalterminal_for_action(ask_question → QUESTION_NEEDED).- Schema-obligation discipline (CLAUDE.md): the D2 guard is enforced twice — in
deliver_askand structurally inDeliveredQuestion.__post_init__— so an unrenderable assessment can never terminateQUESTION_NEEDED; the off-serving test fails if a forbidden import is added. Results: smoke 90/0, affected suites 128/0.
6. Build order (Q1-D, off-serving, one PR — as shipped in #668)
Terminal.QUESTION_NEEDEDadded (sibling ofPROPOSAL_EMITTED).DeliveredQuestiondataclass (+ reservedAnswerBinding); wrapsEpistemicQuestion.- The delivery function
deliver_ask:ask_questionassessment → render (consume Q1-C) → on renderable,QUESTION_NEEDED+ artifact; on unrenderable, the D2 fallback. Plus theemit_questionsink writer. - Off-serving AST test + the D2 guard tests + the sink-write tests.
The served-surface wiring (ask_serving_enabled), the pass_manager call site, and the
registry flip (D3) are a separate, later, ruling-gated step — deferred to the §7
ASK serving-integration scoping doc, NOT this PR. Q2 answer-binding is a separate batch.
7. Outcome — RESOLVED, and what remains open
Resolved (#668): Q1-D's off-serving delivery layer was built —
Terminal.QUESTION_NEEDED + DeliveredQuestion, consuming Q1-C, with the D2
unrenderable-fallback guard and the D3 carve-out left standing — matching the VERIFIED
precedent, keeping wrong=0 honest, moving no metric. The off-serving ASK lane (residue →
renderability → delivery) is now complete.
Open — deferred to dedicated scoping docs (no served-surface code until both are reviewed):
- ASK serving-integration (
ask_serving_enabled): thepass_managerintegration point (wheredeliver_askis actually called), the served-surface behaviour for aQUESTION_NEEDEDreaching a user, theQ1B_ASK_CARVE_OUTretirement gate + registry flip (proposal_allowed → ask), and the no-question/no-proposal dead-zone proof (a family must never lose both signals across the flip). - VERIFIED serving-wiring (
verified_serving_enabled): the gold-free independent reader requirement, holdout gates, proof-producer eligibility, and the explicit ban on eval-gold-backed serving.
Both are the VERIFIED/ASK halves of the same "this is where off-serving stops" line.