Merge pull request #658 from AssetOverflow/docs/stage2-bus-and-q1-scoping

docs(scoping): epistemic disclosure bus (Stage 2 / VERIFIED v1) + Q1 question-articulation briefs
This commit is contained in:
Shay 2026-06-08 15:10:34 -07:00 committed by GitHub
commit a024ac4d9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 991 additions and 0 deletions

View file

@ -0,0 +1,281 @@
# Q1 — Epistemic Question Articulation (v1) — companion scoping brief
**Date:** 2026-06-08 · **Status:** scoping (NO CODE) · **HOLD for review** ·
**Branch:** `docs/stage2-bus-and-q1-scoping`
**What this brief is.** A *companion* to the design-of-record — the session doc
`docs/sessions/2026-06-08-epistemic-question-articulation-first-skill-of-contemplation.md`
(535 lines), which already specifies the typed-question model, the minimal-sufficient
question, the pre-question limitation pass / intake gate (§1.5), and the organ shape
(`core/epistemic_questions/`). **This brief does not duplicate that.** It adds the five
things the session doc points at but does not pin down against shipped code, so that
when Q1 is built it lands on the real substrate without surprises:
1. the four ASK-relevant dispositions mapped onto the **shipped** failure-family
registry, including the **reclassification finding** that `ask` exposes (§1);
2. the **grounded-rendering wrong=0 invariant** — the question-side analogue of "no
fabricated source" (§2);
3. the **structured-residue-on-refusal cost** — readers emit a reason *string*
today, not the typed residue a *named* question needs (§3);
4. **Q2 answer-binding must re-enter the gate** — augment input and re-run, never
mutate the model mid-flight (§4);
5. **bus integration**`QUESTION_NEEDED` is a reserved tenant on Doc 1's
Epistemic Disclosure Bus, not a parallel delivery path (§5).
> Primary frontier: [[stage2-epistemic-disclosure-bus-verified-v1-scoping-2026-06-08]]
> (Stage 2 / VERIFIED is the first implementation; Q1 is the **second** bus tenant,
> scoped now only so the bus reserves its seat). Q1 derivation can be built
> off-serving early; Q1 *delivery* waits on the bus.
**Why Q1 matters (one line, from the intake argument).** A question is CORE's
**intake mechanism for resolvable missing state**; the limitation pass is the gate
that decides whether intake is appropriate (session-doc §1.5). Mis-handle it and CORE
either *refuses when it should ask* (a structural inability to receive the unlocking
datum) or *asks when it should propose* (a wasted channel). Q1 is therefore
general-intelligence infrastructure, which is why the organ lives in `core/`.
---
## 1. The four dispositions, mapped to the SHIPPED registry — and what `ask` re-audits
The limitation pass (session-doc §1.5.3) yields eight `LimitationKind`s and six
`ResolutionAction`s. Only **two** kinds become a question. The discipline binding
this brief and Doc 1: `LimitationAssessment` is a **consolidating view** over the
shipped `core/comprehension_attempt/failure_family.py` registry + the contemplation
terminals — **not a fourth taxonomy** (session-doc §1.5.7; CLAUDE.md "no parallel
decision paths"). So the ASK scope must be expressed as a *derivation from* the
families that already ship, family by family.
The registry today (`REGISTRY`, verified on branch) partitions every organ refusal
reason into `must_remain_refused` vs `proposal_allowed` vs `input_shape`. Introducing
`ask` re-reads that partition, and it **cuts both ways**:
| Shipped family (examples) | Today | `LimitationKind` | With `ask`, disposition |
|---|---|---|---|
| `cmb_non_positive_net`, `cmb_non_integer`, `non_integer_solution`, `negative_solution` | refuse | `hard_boundary` | **refuse** (unchanged — math/logic impossibility) |
| `cmb_combine_ambiguous` | refuse | `ambiguous_structure` | **ask** ("together, opposing, or separate?") |
| `cmb_underdetermined` (`cmb_missing_second_rate`), `rate_underdetermined` (`missing_rate`/`missing_time`/`missing_quantity`), `ungrounded_base` | refuse | `missing_information` | **ask** (the missing datum) |
| `cmb_unsupported_*`, `unsupported_rate_duration`, `missing_category_pair` | propose | `capability_gap` | **propose** (unchanged — CORE lacks the transform) |
| `answer_key_contradiction` | report | `contradiction` | **report** (unchanged) |
| `input_shape` (`not_combined_rate_shaped`, …) | step aside | `input_shape` | **step aside** (unchanged) |
| **`missing_total_count`, `missing_weighted_total`** | **propose** | **`missing_information`** | **ask***reclassification* |
### 1.1 The reclassification finding (decide with tests, never silently re-key)
Two shipped families are currently `proposal_allowed = True`
`proposal_target="r2_gold_fixture"`:
```text
missing_total_count ("propose a total-count-constraint gold fixture")
missing_weighted_total ("propose a weighted-total-constraint gold fixture")
```
But by the limitation taxonomy these are **`missing_information`, not
`capability_gap`**: the user *could state the total* and unblock solving — CORE does
not lack the transform, it lacks a datum. The shipped registry classifies them as
coverage gaps **because `ask` does not exist yet** — proposing a fixture was the only
non-refuse move available. Once `ask` exists they are mis-classified and should **ask,
not propose**.
This is a real change to shipped behaviour, so it is **scope-it-don't-silently-re-key**
territory: decided in the Q1 derivation slice (Q1-B) **with tests**, not edited into
`failure_family.py` in passing. The test must assert the *specific* path: a
`missing_total_count` attempt now yields `ResolutionAction.ask_question` with a typed
`MissingSlot`, and no longer emits a proposal. (Contrast the families that stay
`propose`: `cmb_unsupported_reciprocal` is a genuine capability gap — no datum the user
can supply makes CORE able to do reciprocal work-rate arithmetic.)
Shay's rule, already encoded in the registry comments: **proposals are for structural
capability gaps, not under-specified inputs.** `ask` is what finally lets the registry
honour that rule for `missing_*`.
---
## 2. The grounded-rendering wrong=0 invariant (the hard one)
The question organ has its **own** wrong=0 obligation, and it is the question-side
analogue of "no fabricated source / no ungrounded named operand":
> **A question may not name an entity, slot, unit, relation, or missing fact unless
> that element was grounded in the attempted comprehension trace.** When the grounded
> terms lack the field a question template needs, degrade to a **generic** question or
> emit `question_unrenderable` — never a named guess.
```text
Anna and <unnamed> work together. Anna paints 3 rooms/hour. How many rooms in 4 hours?
grounded: {Anna, rooms/hour, 4 hours, "second agent (unnamed)"}
WRONG ask: "What is Ben's rate?" ← "Ben" was never grounded — fabricated
OK ask: "What is the second person's rate in rooms per hour?" ← generic, all terms grounded
```
This is the precise surface where the R4 (CMB) reader's adversarial passes kept
catching real wrong=0 bugs (11 in the CMB-c reader alone) — fabricating structure the
text did not license. The **same** discipline applies to asking: a fabricated entity in
a question is as much a wrong=0 breach as a fabricated operand in an answer. So the
question renderer is a wrong=0-adjacent surface and gets the same adversarial,
read-only verification treatment when built.
`renderability_gap` is therefore a **first-class** `LimitationKind` (session-doc
§1.5.3), with its own disposition (**ask generic / refuse-to-name**) — not an
afterthought inside the `ask` path.
---
## 3. Structured residue on refusal — the substrate gap Q1 must close first
Today the comprehension organs surface, per attempt
(`core/comprehension_attempt/model.py::ComprehensionAttempt`):
```python
refusal_reason: str | None # e.g. "cmb_missing_second_rate" ← a string
setup_signature: str | None
answer: int | None
evidence: tuple[SourceSpanLink, ...] # source spans — partial, not typed slots
```
That is **enough to classify** the limitation (the reason string keys the failure
family) but **not enough to render a *named* question.** To ask "What is the second
person's rate **in rooms per hour**?" the renderer needs typed partial-parse residue:
*which* slot is missing, *what unit/type* its answer must take, and *which grounded
terms* may appear in the prose. The string `"cmb_missing_second_rate"` and a tuple of
source spans do not carry that.
So Q1 v1 has a **prerequisite substrate slice**: organs must emit, on the refusals
that map to `ask`, a typed residue — sketched in the session doc as `MissingSlot`
(slot name, expected answer type/unit, binding target) and `grounded_terms` (the
renderability source). This is additive and off-serving:
- It rides on the existing `ComprehensionAttempt` (add typed-residue fields; do not
fork a parallel record).
- It is emitted **only** for the `ask`-mapped families (§1) — `hard_boundary` /
`capability_gap` / `input_shape` refusals need no residue (they don't ask).
- Without it, every question degrades to **generic** by the §2 invariant — which is
*safe* (wrong=0-preserving) but low-value. The residue is what earns *named*,
minimal-sufficient questions without fabricating.
This is the honest cost the session doc implies: **named questions are gated on a
typed-residue upgrade to the readers**, not free once the question organ exists.
---
## 4. Answer-binding (Q2) must re-enter the gate — not mutate the model
When a user answers a CORE question, the binding step (the session doc's Q2 /
`AnswerBinding`) is **not** "patch the missing slot into the existing model and
continue." It is:
```text
parse typed answer → augment the ORIGINAL problem input with the bound datum
→ RE-RUN the owner organ (read → solve → verify) from scratch
→ the re-run re-enters the limitation gate
```
Two reasons this is load-bearing, both wrong=0:
- **Immutability (CLAUDE.md coding-style).** A `ComprehensionAttempt` is frozen;
binding produces a *new* augmented input and a *new* attempt, never an in-place
mutation of the first.
- **The answer is itself input, and input is never trusted.** A bound answer can be
ambiguous, out of range, or *introduce a new limitation* (e.g. it makes the system
over-determined, or contradicts a stated quantity). Re-entering the gate means the
augmented problem is re-classified — it may now `solve`, but it may also `ask again`,
`refuse`, or `report a contradiction`. A mutate-and-continue path would skip that
re-classification and could commit a wrong answer the fresh gate would have caught.
Q2 is **out of scope for Q1 v1** (non-claim, §6) — but the binding contract must be
*written down now* so the Q1 question carries the typed target a future re-run needs
(the `MissingSlot.binding_target`), rather than a free-text slot a mutate-path would
assume.
---
## 5. Bus integration — `QUESTION_NEEDED` is a reserved tenant, not a side channel
Q1 does not get its own delivery path to the user. A served question is governed for
the surface **exactly as a served answer is**, through Doc 1's Epistemic Disclosure
Bus (`EpistemicState + LimitationAssessment → ServedDisposition`):
```text
LimitationAssessment(missing_information | ambiguous_structure)
→ ResolutionAction.ask_question
→ ServedDisposition(DISPOSITION=ask, ...)
→ QUESTION_NEEDED (a RESERVED tenant on the bus; see Doc 1 §0 matrix)
→ render (grounded-only, §2) → served surface
```
Consequences for sequencing:
- **Doc 1 must reserve the `ask` disposition / `QUESTION_NEEDED` seat now** (it does —
Doc 1 §0, §7) so the bus is not designed too narrowly around VERIFIED.
- **Q1 *derivation* (limitation pass + missing-slot residue + renderer) can be built
off-serving early** — it imports no serving path, so it cannot regress the GSM8K
seal, and it can be validated against organ holdouts.
- **Q1 *delivery* (a question actually reaching a served surface) waits on the bus**
being live (Stage 2 builds it). Until then, questions are produced and tested
off-serving; nothing new reaches the user.
This keeps Q1 from becoming a parallel "clarification feature" bolted beside the
served path — the exact anti-pattern the consolidation discipline forbids.
---
## 6. What Q1 v1 is NOT (non-claims)
- **Not** a freeform clarification / chat-style "can you tell me more?" — every
question is typed, minimal-sufficient, single-slot, and grounded-only (§2;
session-doc §1.4 minimal-sufficient rules).
- **Not** a dialogue manager / multi-turn conversation state machine.
- **Not** Q2 answer-binding — that is a separate, later slice; Q1 only *produces* the
question and records the typed target (§4).
- **Not** a silent re-key of `missing_total_count` / `missing_weighted_total` — the
reclassification is decided in Q1-B with tests (§1.1).
- **Not** a change to any served surface — Q1 derivation is off-serving; delivery
waits on Doc 1's bus (§5). No serving metric, pinned SHA, or `CLAIMS.md` moves.
- **Not** a fourth disposition taxonomy — `LimitationAssessment` consolidates the
shipped failure-family registry + contemplation terminals (§1; session-doc §1.5.7).
---
## 7. Build order (Q1-A..D) — off-serving, each its own PR (scoping only here)
> Sequenced **after** Stage 2 establishes the bus, except Q1-A/B which are
> off-serving and can begin in parallel once this brief is approved.
1. **Q1-A — Limitation pass (`LimitationAssessment`) as a consolidating view.** The
pre-question classifier (session-doc §1.5): derive `LimitationKind` /
`ResolutionAction` from the shipped failure families + contemplation terminals;
add **only** the new `ask_question` action and the `renderability_gap` guard. Tests
assert the §1 mapping, including that math/contradiction families stay non-ask.
2. **Q1-B — Typed missing-slot residue (the §3 substrate slice) + the
reclassification.** Add typed residue (`MissingSlot`, `grounded_terms`) to the
`ask`-mapped organ refusals; decide `missing_total_count` / `missing_weighted_total`
`ask` with the specific tests from §1.1. Off-serving.
3. **Q1-C — The renderer with the grounded-rendering invariant (§2).** Render minimal,
single-slot, grounded-only questions; degrade to generic / `question_unrenderable`
when residue is insufficient. Adversarial read-only verification on this surface
(it is wrong=0-adjacent). Off-serving.
4. **Q1-D — Bus delivery (`QUESTION_NEEDED` tenant).** Wire the rendered question to
Doc 1's bus as the second disposition. **Requires the bus to be live** (Stage 2
S2-A..C). Until then Q1-A..C ship and are validated off-serving with nothing
reaching a served surface.
Q2 answer-binding (§4) is a **separate later batch**, explicitly out of Q1 v1.
---
## 8. Verification lanes (when built)
- `core test --suite full -q` — the §1 family-mapping tests (Q1-A), the
reclassification tests (Q1-B), the grounded-rendering / `question_unrenderable`
tests (Q1-C).
- The router-organ-hygiene invariant (`tests/test_router_organ_hygiene.py`) — the
limitation pass must not let any organ claim a foreign-text limitation (it must map
to `input_shape` / `step_aside`).
- Adversarial read-only verification on the Q1-C renderer (the wrong=0-adjacent
surface), the same discipline that paid off across the CMB ladder.
- `scripts/verify_lane_shas.py` + `scripts/generate_claims.py --check` — must stay
**green and unchanged** (Q1 is off-serving; the GSM8K seal does not move).
> **Stop boundary.** This brief is scoping. No code lands until review approves it and
> the Q1-A..D order. The design of record remains the session doc; this brief only
> pins Q1 to the shipped substrate. HOLD for review.

View file

@ -0,0 +1,493 @@
# Stage 2 — the Epistemic Disclosure Bus (VERIFIED v1) — scoping
**Date:** 2026-06-08 · **Status:** scoping (NO CODE) · **HOLD for review** ·
**Branch:** `docs/stage2-bus-and-q1-scoping`
**Pivot this doc records.** The comprehension-organ ladder (R1→R4, all merged and
frozen — [[combined-rate-capability-ledger-2026-06-08]]) answered *"what can CORE
comprehend off-serving?"* Stage 2 turns to a different question:
```text
What may CORE disclose through the served surface — and under what governed disposition?
```
This is a **scoping document for an implementation frontier**, not the
implementation. It defines the bus, names VERIFIED as the only tenant built in
v1, reserves the future tenants so the bus is not designed too narrowly, and —
most importantly — records the one honest constraint that already killed a
naive version of this work, so v1 does not revive a dead path.
> Companion brief: [[q1-epistemic-question-articulation-v1-scoping-2026-06-08]]
> (the `ASK` tenant, scoped now so the bus reserves a seat for it). Off-serving
> mirror / design-of-record: the session doc
> `docs/sessions/2026-06-08-epistemic-question-articulation-first-skill-of-contemplation.md`
> §1.5 (the pre-question limitation pass / intake gate).
---
## 0. Why a *bus*, not a *VERIFIED feature*
The earlier framing ("build the VERIFIED producer and widen math serving") is
correct but too small. VERIFIED is one **disposition** of one general machine.
The machine is:
```text
EpistemicState + LimitationAssessment → ServedDisposition
(what is true) (what is blocking) (what reaches the user, and how)
```
The same machine that decides *"this answer is VERIFIED → disclose it at a wider
reach"* also decides *"this is missing information → ASK a question"*, *"this is a
capability gap → emit a PROPOSAL"*, *"this is out of scope → disclose the SCOPE
BOUNDARY"*, *"this contradicts the supplied key → REPORT it"*. If we build a
bespoke VERIFIED path, the question organ (Doc 2) and every later disposition have
to re-derive the same served-surface governance. So Stage 2 builds the **bus**
and lights up **one** tenant.
This is not new infrastructure invented here. The bus *is* ADR-0206's
`govern_response` / `shape_surface` seam, already wired into serving and already
inert at the math end (§2). Stage 2 = **add a VERIFIED reach to the seam that
exists**, behind a kill-switch, gated by a validate-first probe — nothing parallel.
### The two axes
A `ServedDisposition` is a point in a 2-D space, both axes already present in code:
- **DISPOSITION***what kind of move* the served surface makes:
`{ commit, disclose, ask, report, explain, refuse }`.
These mirror the contemplation terminals and the failure-family policy
(`must_remain_refused` / `proposal_allowed` / `answer_key_contradiction` /
`input_shape`) — the bus is a **consolidating view** over them, never a fourth
taxonomy (see §7 and session-doc §1.5.7).
- **REACH***how far past fully-grounded fact* a disclosure may go:
the existing `core/response_governance/policy.py::ReachLevel`
`STRICT < APPROXIMATE < EXTRAPOLATE < CREATIVE`.
| | STRICT (today) | APPROXIMATE (Step E, built) | wider (reserved) |
|---|---|---|---|
| **commit** | fully-grounded answer | — | — |
| **disclose** | — | `[approximate]` cognition estimate | VERIFIED math answer **(v1 target)** |
| **ask** | — | — | QUESTION_NEEDED *(reserved — Doc 2)* |
| **report** | contradiction report | — | — |
| **explain** | — | — | SCOPE_BOUNDARY disclosure *(reserved)* |
| **refuse** | typed refusal | typed refusal | typed refusal |
VERIFIED v1 occupies exactly one new cell: **disclose — under a distinct
`[verified]` mode — an answer a comprehension organ produced and an independent
canonical comparison confirmed.** Everything else in the table is either already
shipped (STRICT commit/refuse/report; APPROXIMATE cognition estimate) or reserved.
### A caution on the REACH axis — VERIFIED is a disclosure *claim*, not an approximation
The `DISPOSITION × REACH` framing is right for scoping, but VERIFIED **stretches**
the REACH axis and the design must not paper over it. `ReachLevel` orders responses
by *how far past fully-grounded fact* they speculate (`STRICT < APPROXIMATE <
EXTRAPOLATE < CREATIVE`). VERIFIED is **not more speculative than STRICT** it is
*more **licensed***: an answer past the strict/gold line that an independent
canonical comparison has confirmed. It is therefore a **disclosed-surface license /
verification claim**, not a point further out on the speculation scale.
> **Decision (locks S2-1 in principle).** VERIFIED is a disclosed-surface license,
> not an approximation. If it is implemented as a `ReachLevel` value for
> compatibility with the existing seam, it **must** carry distinct semantics **and
> distinct disclosure text** from `APPROXIMATE` — never the `[approximate]` prefix.
The principled decomposition separates the two ideas onto their own axes:
```text
Disposition = commit | disclose | ask | report | explain | refuse
DisclosureClaim = verified | approximate | scope_boundary | question_needed ← the claim a disclosure makes
ReachLevel = strict | approximate | extrapolate | creative ← how far past fact it reaches
```
`DisclosureClaim` is the new idea VERIFIED really introduces; "verified" is a claim
about *verification status*, not a reach. For **v1 the smallest coherent
implementation** may collapse this into a distinct `ReachLevel.VERIFIED` (so the
existing `policy.py` / `shape_surface` seam carries it without a new axis) — that is
acceptable **provided** it gets its own admissible-set, rationale, and `[verified]`
prefix. The exact enum shape (distinct `ReachLevel.VERIFIED` vs. a separate
`DisclosureClaim` axis) is the one remaining S2-A choice; the *principle*
distinct from APPROXIMATE — is settled here.
---
## 1. The 8 serving questions (the scoping agenda)
The pivot question above decomposes into eight. This doc commits to answering all
eight for VERIFIED v1; the reserved tenants answer them later, on the same bus.
> These are reconstructed from the design discussion (the verbal agenda was lost
> to compaction). They are the doc's contract; correct/extend on review.
- **SQ-1. What is the unit of disclosure governance?** A per-response
`ServedDisposition = (DISPOSITION, REACH, disclosed_surface?)`, produced by the
bus from `(EpistemicState, LimitationAssessment, license/verification evidence)`.
- **SQ-2. What states may be disclosed, and at what reach?** The DISPOSITION×REACH
matrix above. v1 adds exactly one cell (VERIFIED → disclose).
- **SQ-3. What earns the right to widen past STRICT/gold?** *Only* `VERIFIED`, and
only via a **canonical-comparison proof of correctness** — not a statistical
license, not gold agreement (§3). VERIFIED is "the only state that will license
widening past gold" (ADR-0206 §4; `policy.py` RESERVED_STATES comment).
- **SQ-4. How is the producer's independence guaranteed?** Independence must live
in the **reading**, not the solving (§3.2). This is the lesson that killed the
fold-reader path and the reason R1R4 organs are the unblock.
- **SQ-5. How is a widened answer presented?** Via the existing `shape_surface`:
STRICT is identity (verbatim commit); a wider mode surfaces a **disclosed**
alternative with a prefix. VERIFIED v1 discloses under its **own distinct
`[verified]` mode** — never the `[approximate]` prefix, and never a silent
overwrite of the STRICT surface (§0 caution, §4).
- **SQ-6. What is the blast radius / kill-switch?** A new default-`False`
`RuntimeConfig.verified_serving_enabled`, sibling to `estimation_enabled`
(§5). Off ⇒ byte-identical to today.
- **SQ-7. How do we prove no regression?** A **validate-first holdout probe**
(INV-25 discipline) runs *before* any widening is built; the sealed GSM8K lanes
and pinned SHAs stay untouched and must show wrong=0 preserved (§6).
- **SQ-8. How do we measure that widening *helped*?** Extend the
`evals/capability_index/` yardstick with a served-usefulness scorecard
`{wrong, over_refusal, mode_misclassification}` — wrong=0 alone proves we did no
harm; it does not prove we did any good (§8).
The implementation sub-questions S2-0..S2-6 (§9) and the build order S2-A..D (§10)
operationalise these.
---
## 2. Substrate — what is already built (do not re-research, do not rebuild)
The disclosure machine is **far more built than the headline suggests**, and the
math end is **wired but deliberately inert**. Verified against the tree on this
branch:
- **`core/response_governance/policy.py`** — `ReachLevel` (4 levels),
`ReachPolicy` (frozen: `level`, `admissible_states`, `rationale`,
`license_ratio`), `govern_response(*, epistemic_state, license_decision,
stakes) -> ReachPolicy`, `shape_surface(policy, *, committed_surface,
decode_state, disclosed_alternative=None) -> str`. STRICT path of
`shape_surface` is the **identity transform**; the wider branch prepends a
`_DISCLOSURE_PREFIX` to a *disclosed alternative*. `STRICT_POLICY` and
`APPROXIMATE_POLICY` both admit only `{DECODED}` (APPROXIMATE keeps the same
admissible set on purpose — a less-grounded state is *disclosed*, never
committed). This is **live wiring**, not dead code.
- **`core/epistemic_state.py`** — `EpistemicState` (15 members). `VERIFIED =
"verified"` is a **RESERVED** state today; `policy.py::RESERVED_STATES`
documents the unlock: *"needs canonical-comparison pass (soundness != correctness);
the ONLY state that will license widening past gold."*
- **Cognition end is ALREADY widened (Step E / ADR-0206 §5).**
`govern_response` returns `APPROXIMATE_POLICY` iff a genuine licensed
`Action.SERVE` `LicenseDecision` is passed; the converse-guess estimate then
rides `shape_surface` out as a disclosed `[approximate]`. Gated by
`RuntimeConfig.estimation_enabled = False` (`core/config.py:336`). **This is the
exact pattern VERIFIED v1 copies** — different producer, same bus, same
kill-switch shape.
- **Math end is wired and INERT.** `generate/derivation/verify.py`:
`select_self_verified(..., policy=...)` (line 208) takes a `ReachPolicy`; a
wider reach may resolve a disagreement STRICT refuses **only** via
`_canonically_verified(...)` (line 188) — which **`return None` today**. So the
widening is *structurally* inert: disagreement refuses regardless of `policy`,
and the absolute math `wrong == 0` holds **by construction, not convention**.
This `None` is the precise, tested integration point a VERIFIED producer plugs
into.
- **Yardstick**`evals/capability_index/index.py`: `DomainResult{correct,
wrong, refused}` with `coverage`/`accuracy`; `CapabilityIndex` hard-gates
`capability_score` to 0 on any `wrong` (`assert_mode_valid`). The served-
usefulness scorecard extends this, it does not replace it.
**Consequence.** Stage 2 v1 is not "build a disclosure system." It is: write a
**VERIFIED producer**, wire it into the one `return None`, add a kill-switch and a
yardstick column, and prove it on a holdout *before* flipping the switch. The bus
already exists.
---
## 3. The VERIFIED producer — the one hard part, and its honest constraint
### 3.1 The dead-path constraint (read before designing anything)
There is a **load-bearing prior result** that v1 must not contradict:
[[VERIFIED-canonical-comparison-scoping-2026-06-06]] already **killed**, with a
validate-first probe and *before any build*, the obvious VERIFIED producer — a
certifier over the existing GSM8K fold/derivation readers:
- The serving `verify` is **solver-replay soundness**, not correctness — it proves
the solver executed the graph faithfully, not that the *parse* is right.
- No independent second reader helped: the R1 graph reader is **nested** in
candidate-graph (`0/44` complementary coverage on the refused set), and the one
genuinely complementary reader (fold-derivation) is **~98% wrong** on the
refused set (`2 correct / 87 WRONG` on holdout_dev). A certifier strict enough
to reject the 87 rejects the 2 too — the mis-reads carry no shallow structural
signature, because *separating them is the comprehension problem itself*.
- **Verdict:** "Math serving is comprehension-bound… Re-open **only** if a
genuinely complementary, independently-validated reader lands."
**That reader has now landed.** R1R4 are exactly "genuinely complementary,
independently-validated readers": each carries its **own** `_canonical_outcome`
gold oracle, disjoint from the GSM8K candidate-graph path (off-serving, AST-checked
to import no `generate.derivation` / `core.reliability_gate`). So the dead-path
verdict does not block VERIFIED v1 — **it scopes it.** v1's VERIFIED producer is a
comprehension-organ answer, *not* a GSM8K-fold-reader certifier. The dead path
stays dead; we walk a different, narrower one.
### 3.2 Independence must be in the READING, not the solving
The single most important design lesson, and the one most easily violated:
> Back-substitution / constraint re-checking catches **solve** errors. It does
> **not** catch **read** errors. If the reader mis-parsed the problem, the
> candidate answer can satisfy the (wrongly-read) constraints perfectly and still
> be wrong. A second *solver* over one *reading* is **fake** independence.
So VERIFIED for a comprehension-organ answer requires **two** things, and the first
is the one that carries wrong=0:
1. **A conservative reader that refuses on doubt** (R2/R4 already do this: they
step aside or refuse on anything not cleanly in-shape). wrong=0 lives here.
2. **A canonical-comparison check** layered on top: an *independent* re-derivation
to canonical normal form **plus** back-substitution of the candidate into the
problem's *stated* quantities — and **no organ boundary fired**. The check is a
*necessary pre-filter*, never the sole gate (convergence is evidential, not a
proof; ADR-0206-scoping §"Candidate mechanisms" item 2).
R2 (finite-integer constraint satisfaction) is the **cleanest first class**: a
solved count must satisfy the stated total/weighted constraints exactly —
back-substitution is a genuine correctness check against the problem's own
structure, with no gold. R4 (combined-rate) is the **second** class. Neither
touches the GSM8K candidate-graph.
### 3.3 What "VERIFIED" must mean (the contract, scoped not built)
`EpistemicState.VERIFIED` is emitted for a candidate answer **iff all** hold:
```text
1. A conservative comprehension organ (R2 first, R4 second) produced the answer
— i.e. it did NOT refuse / step aside / hit a boundary.
2. An independent canonical re-derivation converges on the same canonical value.
3. Back-substitution into the problem's STATED quantities satisfies every
constraint exactly (integer-exact; never round).
4. No organ boundary (unit-mismatch, ambiguity, underdetermined, non-integer,
non-positive-net, over-determined) fired anywhere in the chain.
```
The obligation is **real only if it can meaningfully fail** (CLAUDE.md
"Schema-Defined Proof Obligations"): the v1 contract's first test must be that it
**rejects a sound-but-wrong answer** (the `20/5 == 4`-style class — a faithful
solve of a wrong read). A VERIFIED predicate that passes such a case is decoration,
not proof, and must not be wired to the seam.
---
## 4. How a VERIFIED answer reaches the user — disclose, never silently widen
VERIFIED v1 rides the **existing** `shape_surface`. Two non-negotiables:
- It only ever resolves a case STRICT would **refuse** (a disagreement /
past-gold case). It never *changes* an answer STRICT already commits.
- It **discloses**. Even when correctness is proven, v1 surfaces the answer through
the disclosed-alternative path of `shape_surface`, not as an unmarked commit, so
the served record always shows the reach that produced it. (Whether the
user-visible prefix is empty for proven-correct answers is an S2-B decision; the
*plumbing* is the disclosed path regardless.)
The bus contract for v1:
```text
govern_response(epistemic_state=VERIFIED, verification=<canonical-comparison evidence>)
→ ReachPolicy(level=<distinct VERIFIED mode>, admissible_states={DECODED, VERIFIED})
shape_surface(policy, committed_surface=<organ answer>, decode_state=VERIFIED, ...)
→ "[verified] <answer>" # distinct mode + distinct prefix — NEVER [approximate]
```
**S2-1 decision (settled in this doc, not deferred):**
> **VERIFIED must not reuse `ReachLevel.APPROXIMATE` or the `[approximate]`
> disclosure prefix. VERIFIED receives a distinct disclosed-surface mode/level with
> its own deterministic prefix, e.g. `[verified]`. APPROXIMATE is for bounded
> estimates; VERIFIED is for independently confirmed answers under the
> canonical-comparison contract.**
The two are not neighbours on one scale: `APPROXIMATE` means *"useful but less
certain — disclosed as approximate"*; `VERIFIED` means *"independently confirmed
under the canonical-comparison contract."* Reusing APPROXIMATE's prefix would put a
proven answer behind an "I'm not sure" label — semantically wrong. The only choice
left to S2-A is the **enum shape** (a distinct `ReachLevel.VERIFIED` value, or a
separate `DisclosureClaim` axis per §0); both honour the decision above. A distinct
`ReachLevel.VERIFIED` is the acceptable smallest-coherent v1 **iff** it carries its
own admissible-set, rationale, and `[verified]` prefix.
---
## 5. The kill-switch (blast radius)
Add a sibling to `estimation_enabled`:
```python
# core/config.py, RuntimeConfig
verified_serving_enabled: bool = False
```
- **Off (default).** `govern_response` for math never receives a VERIFIED state;
`_canonically_verified` keeps returning `None`; every serving call is
byte-identical to today. The flag's *absence of effect* when off is itself a
test (the live-wiring discipline `policy.py` already follows).
- **On.** Only the VERIFIED producer's narrow class can widen, and only via
disclosure. wrong=0 is still structural: the producer returns a derivation or
`None`, and `None` still refuses.
No flag flips in this batch (scoping only). The flag lands with S2-C and is
**not** turned on in production until S2-D's holdout passes.
---
## 6. Proving no regression — validate-first, sealed lanes untouched
This is the wrong=0 discipline made concrete, and it is **ordered before the build**:
1. **Validate-first holdout probe (S2-D-pre, runs BEFORE widening is wired).**
Mirroring the 2026-06-06 probe that killed the fold-reader path: run the VERIFIED
producer (R2 first) over an **independent holdout gold lane** (INV-25 discipline —
gold from a *separate* oracle than the one the producer uses internally) and
confirm `wrong == 0` on the *widened* set. If the probe shows any wrong, the
producer is not buildable at wrong=0 for that class — **stop**, exactly as the
fold-reader path stopped. No code-first optimism.
2. **GSM8K seal is untouched.** The candidate-graph serving path, its pinned
eval-lane SHAs (`scripts/verify_lane_shas.py`), and `CLAIMS.md`
(`scripts/generate_claims.py --check`) are **not modified by VERIFIED v1**. v1's
producer is an off-serving organ; it does not import `generate.derivation` /
`core.reliability_gate`, so it *structurally cannot* move the sealed metric.
3. **Re-pin is a deliberate reviewed act, with the eval delta as truth test.** If
v1 ever does emit a served `reach_level` on a frozen lane, re-pinning the SHAs is
an explicit reviewed step; the sealed run must show wrong=0 preserved **and** the
new served class (ADR-0206-scoping §"Recommended arc" item 4).
---
## 7. Consolidation discipline — the bus is a VIEW, not a 4th taxonomy
Binding constraint inherited from session-doc §1.5.7 and CLAUDE.md ("no parallel
correction/decision paths"):
CORE already encodes served disposition in three overlapping places —
- the **failure-family registry** (`core/comprehension_attempt/failure_family.py`:
`must_remain_refused` / `proposal_allowed` / `input_shape` / `owner`),
- the **contemplation terminals** (`SOLVED_VERIFIED` / `REFUSED_KNOWN_BOUNDARY` /
`PROPOSAL_EMITTED` / `CONTRADICTION_DETECTED` / …),
- and the **reach policy** (`ReachLevel` / `admissible_states`).
The bus's `ServedDisposition` must **derive from and unify** these, adding only the
genuinely new disclosure cell (VERIFIED → disclose) — never a fourth source of
truth. Concretely, the DISPOSITION axis maps onto what already ships:
```text
commit ← STRICT-admissible DECODED answer (today)
disclose ← APPROXIMATE estimate (today) + VERIFIED answer (v1, new)
report ← answer_key_contradiction terminal
refuse ← must_remain_refused families / STRICT non-admissible
ask ← reserved (Doc 2: missing_information / ambiguous_structure)
explain ← reserved (scope_boundary)
```
If a Stage-2 PR finds itself defining a new enum that re-states the terminal set or
the family policy, that is the smell of a fourth taxonomy — stop and derive instead.
---
## 8. Measuring that widening *helped* (the yardstick extension)
wrong=0 proves v1 did no **harm**. It cannot prove v1 did any **good** — a producer
that emits VERIFIED for *nothing* is trivially wrong=0. So extend
`evals/capability_index/`:
- Keep `DomainResult{correct, wrong, refused}` and the wrong-gated
`capability_score` exactly as-is (the anti-gaming headline).
- Add a **served-usefulness scorecard** alongside it:
`{ wrong, over_refusal, mode_misclassification }`
- `wrong` — must stay 0 (the hard gate; already enforced).
- `over_refusal` — cases the VERIFIED producer *could* have correctly disclosed
but refused (the coverage v1 is buying). Measured against the holdout gold.
- `mode_misclassification` — cases routed to the wrong disposition (disclosed
when it should have refused/asked, or vice-versa). This is the bus-level
analogue of the intake-gate failure modes in session-doc §1.5.1.
- **Sealed-eval scoring is UNCHANGED.** The scorecard is a *new* lane over the
organ holdouts; it does not touch the GSM8K sealed report or its digest.
The headline v1 success metric is: **`over_refusal` on the R2 (then R4) class drops,
`wrong` stays 0, `mode_misclassification` stays 0.**
---
## 9. The implementation sub-questions (S2-0..S2-6)
To be answered by the S2-A..D PRs, not here:
- **S2-0. Contract.** Exact `ServedDisposition` dataclass + the VERIFIED predicate
signature, with the meaningful-fail test (rejects sound-but-wrong) written first.
- **S2-1. Reach. (SETTLED in §0/§4 — distinct from APPROXIMATE.)** VERIFIED gets a
distinct disclosed-surface mode and a distinct `[verified]` prefix, never
`[approximate]`. The only S2-A choice left is the **enum shape**: a distinct
`ReachLevel.VERIFIED` value vs. a separate `DisclosureClaim` axis — both must carry
their own admissible-set, rationale, and prefix.
- **S2-2. Producer class order.** R2 constraint-satisfaction first; R4 second;
nothing else in v1.
- **S2-3. Independence proof.** How the canonical re-derivation is made provably
*reading-independent*, not just a second solver over one reading (§3.2).
- **S2-4. Kill-switch + wiring.** `verified_serving_enabled`; the `None`
producer wiring at `verify.py:188`; off-by-default byte-identity test.
- **S2-5. Holdout probe.** The INV-25 validate-first lane; gold from a separate
oracle; run **before** S2-C wires anything live.
- **S2-6. Yardstick.** The served-usefulness scorecard lane (§8); sealed scoring
untouched.
---
## 10. Build order (S2-A..D) — each its own wrong=0-gated PR
1. **S2-A — Contract + meaningful-fail test (no producer).** Define the VERIFIED
predicate's obligation and the `ServedDisposition`/reach decision; ship the test
that rejects a sound-but-wrong answer. No serving change.
2. **S2-B — VERIFIED producer for the R2 class (off-serving).** Canonical
re-derivation + back-substitution + boundary-clear check, emitting
`EpistemicState.VERIFIED`. Still not wired to serving.
3. **S2-C — Wire + kill-switch.** Replace the `verify.py:188` `return None` with a
call to the producer guarded by `verified_serving_enabled` (default off); add
the off-by-default byte-identity test. Flag stays off.
4. **S2-D — Validate-first holdout, then (only if green) flip.** Run the INV-25
probe (§6 item 1); if `wrong == 0` on the widened set holds, enable the flag for
the R2 class and add the served-usefulness scorecard. If not green, the producer
is not buildable at wrong=0 for that class — record and stop, as the fold-reader
path was stopped.
R4 repeats S2-B..D as a follow-on once R2 is proven end-to-end.
---
## 11. What VERIFIED v1 is NOT (non-claims)
- **Not** a re-opening of the GSM8K fold-reader certifier (dead per §3.1).
- **Not** a statistical/Wilson license substituting for proof — math serving is
absolute wrong=0, not disclosed-estimate like the cognition path (the
`_canonically_verified` docstring forbids this explicitly).
- **Not** a reuse of `APPROXIMATE` — VERIFIED carries its own distinct disclosure
mode and `[verified]` prefix; a proven answer never wears the `[approximate]`
label (§0 caution, §4 S2-1 decision).
- **Not** the ASK / PROPOSE / REPORT / SCOPE tenants — those are **reserved** seats
on the bus, built later (Doc 2 is the ASK scope). The intake-gate excitement
(session-doc §1.5) must **not** widen v1: ASK is a future tenant, not Stage 2 v1.
- **Not** a change to the sealed GSM8K serving metric, pinned SHAs, or `CLAIMS.md`.
- **Not** a fourth disposition taxonomy — it is a consolidating view (§7).
- **Not** a flag flipped in this batch. This doc is scoping; no code, HOLD for review.
---
## 12. Verification lanes (when built)
- `core test --suite full -q` — VERIFIED contract + meaningful-fail (S2-A),
off-by-default byte-identity (S2-C).
- The new served-usefulness scorecard lane over the R2/R4 organ holdouts (S2-D).
- `scripts/verify_lane_shas.py` + `scripts/generate_claims.py --check` — must stay
**green and unchanged** through S2-A..C (proof the seal is untouched).
- The INV-25 validate-first holdout probe (S2-D) — the go/no-go gate.
> **Stop boundary.** This document is scoping. No code lands until review approves
> the agenda and the S2-A..D order. The companion Q1 brief
> ([[q1-epistemic-question-articulation-v1-scoping-2026-06-08]]) reserves the ASK
> seat on this bus; it, too, is scoping-only.

View file

@ -32,6 +32,8 @@ solve, not a guess.
```text
A question is not language. A question is a typed request for missing state.
The first skill of contemplation is: ask the smallest question that unblocks the proof.
But the FIRST move is upstream of the question: a limitation pass classifies the gap —
only missing/ambiguous information becomes a question (the intake gate; see §1.5).
question ≠ proposal (missing information in the world vs missing capability in CORE)
Question generation must be deterministic, slot-directed, template-driven — never vibes.
```
@ -115,6 +117,221 @@ solver*, route to `PROPOSAL_EMITTED`.
---
## 1.5 — The pre-question limitation pass (session refinement)
A refinement raised later in the same session. The trigger question:
> Do we have anything, in the process of contemplation and asking the question,
> about identifying the **limitations** of what the contemplation is *about*?
Yes — and it must be explicit, and it must come **first**. Before contemplation
asks anything, it must identify *the limits of the contemplated situation*. A
question is only one of several dispositions, and asking is wrong unless the
limitation is specifically the kind a question resolves. Otherwise CORE can ask
something grammatical but epistemically wrong — too broad, out of scope, aimed at
the wrong gap, or naming what it never grounded.
So the first contemplative move is **not** *"what question do I ask?"* It is:
```text
What is preventing resolution — and what KIND of limitation is it?
```
### 1.5.1 Why this is load-bearing — it is CORE's intake gate
This is bigger than asking tidy questions. **Limitation assessment is the gate on
CORE's ability to receive necessary information at all.** Get the classification
wrong and the intake path blocks in one of two dangerous ways:
| Failure | What goes wrong | Cost |
|---|---|---|
| **Refuse when it should ask** (`missing_information → terminal refusal`) | CORE never receives the one fact that would make the problem solvable | a *structural* inability to take in the unlocking datum — not a clumsy answer, a lost capability |
| **Ask when it should propose** (`capability_gap → question`) | CORE asks for something the user already supplied / cannot supply | the intake channel is wasted and the user is misled about what's missing |
Example of the first (must ask, not refuse):
```text
Anna and Ben work together. Anna paints 3 rooms/hour. How many rooms in 4 hours?
bad: "I cannot solve this." (refuses → never receives Ben's rate)
correct: "What is the second person's painting rate in rooms per hour?"
```
Example of the second (must propose, not ask):
```text
Anna can finish in 3 hours. Ben can finish in 6 hours. How long together?
bad: "What is Anna's rate?" (the info is present; asking is wrong in KIND)
correct: deferred-capability proposal (reciprocal work-rate); no question
```
**The key phrase to pin in the Q1 scope:**
> Question-asking is the intake mechanism for resolvable missing state.
> Limitation assessment is the gate that determines whether intake is appropriate.
That is what keeps a CORE question from collapsing into generic clarification, and
makes intake part of the deterministic reasoning loop rather than a chat reflex.
### 1.5.2 The flow
The question organ is **downstream** of limitation assessment — not
`failure → question`, but `failure → limitation assessment → question only when appropriate`:
```text
problem / situation
→ contemplate (organ attempts + domain-precedence adjudication)
→ identify the terminal limitation ← the new pass
→ classify the limitation kind
→ choose a resolution action (answer / ask / propose / refuse / report / step aside)
→ if ask: derive missing slot → check renderability → render minimal sufficient question
else: refuse / propose / report / narrow scope / step aside
```
### 1.5.3 The limitation inventory
Only one of these kinds becomes a question. The rest are *not* questions by default.
| Limitation kind | Meaning | Disposition | Example |
|---|---|---|---|
| `missing_information` | a needed datum is absent from the input | **ask** | only one of two rates given → "What is the second rate?" |
| `ambiguous_structure` | the data is present but the relationship is unclear | **ask** | two rates, no cue → "together, opposing, or separate?" |
| `scope_boundary` | the ask exceeds current capability/evidence | **explain (scope)** | "solve *all* combined-rate problems?" → state the v1 envelope |
| `capability_gap` | the info is present; CORE lacks the transform | **propose** | reciprocal work-rate → proposal, not question |
| `hard_boundary` | mathematically/logically impossible or undefined | **refuse** | net rate 0 → refuse, no question |
| `contradiction` | evidence conflicts with a claimed answer | **report** | derived 20 vs key 18 → report (ask only via a later source-authority lane) |
| `renderability_gap` | asking is right, but terms aren't grounded enough to ask safely | **ask generic / refuse-to-name** | unnamed 2nd painter → "the second person's rate", never "Ben's" |
| `input_shape` | not this organ's domain | **step aside** | single-rate text reaching CMB → cede to R3 |
### 1.5.4 The model
```python
LimitationKind = Literal[
"missing_information", "ambiguous_structure", "scope_boundary",
"capability_gap", "hard_boundary", "contradiction",
"renderability_gap", "input_shape",
]
ResolutionAction = Literal[
"answer", "ask_question", "emit_proposal",
"refuse_known_boundary", "report_contradiction", "step_aside",
]
@dataclass(frozen=True)
class LimitationAssessment:
limitation_kind: LimitationKind
epistemic_state: EpistemicState # reuse core/epistemic_state.py — not a new enum
resolution_action: ResolutionAction
owner_organ: str | None
blocking_reason: str # the failure-family key (the partition key)
grounded_terms: tuple[str, ...] # what the trace actually grounded (renderability source)
missing_slots: tuple[MissingSlot, ...]
unsupported_capabilities: tuple[str, ...]
contradictions: tuple[str, ...]
scope_boundary: str | None
```
The `epistemic_state` / `resolution_action` split keeps state ("I do not have
enough information" = `UNDETERMINED`) distinct from action ("ask the minimal
sufficient question" = `ask_question`).
### 1.5.5 The intake rule — ask only when ALL of these hold
```text
1. The limitation is missing or ambiguous external/problem information.
2. The missing slot is typed.
3. The expected answer form is known.
4. The question can be rendered from grounded terms only.
5. The answer, if provided, would re-enter the gate and could unblock solving.
```
If any fail: refuse, report, propose, narrow scope, or step aside — do **not** ask.
### 1.5.6 This is bigger than math (lived / agentic intake)
The same gate governs intake for any goal-directed situation:
```text
No goal → ask for the goal.
No constraint → ask for the constraint.
No observation → ask what happened.
No source → ask which source to trust.
No metric → ask what success means.
Capability gap → do NOT ask; propose / build capability.
Hard boundary → do NOT ask; refuse / report the boundary.
```
Without limitation assessment, CORE cannot properly *receive* information, because
it does not know what kind of information is admissible or useful. This is why the
organ lives in `core/` — intake discipline is general-intelligence infrastructure,
not a math feature.
### 1.5.7 Implementation discipline (notes added in review)
Three sharp edges to honour when this is built, so the refinement does not create
new hazards:
- **`LimitationAssessment` is a *consolidating view*, not a fourth taxonomy.** We
already encode disposition in three overlapping places: the **failure-family
registry** (`must_remain_refused` / `proposal_allowed` / `input_shape` /
`owner`), the **contemplation terminals** (`SOLVED_VERIFIED` /
`REFUSED_KNOWN_BOUNDARY` / `PROPOSAL_EMITTED` / `CONTRADICTION_DETECTED` / …),
and now `LimitationKind` / `ResolutionAction`. `ResolutionAction` ≈ the terminal
set we already ship; `LimitationKind` ≈ the *why* the failure family half-encodes.
The pass must **derive** disposition from those and *unify* them — adding only the
genuinely new `ask_question` and the `renderability_gap` guard — never stack a
parallel fourth source of truth (CLAUDE.md: no parallel correction/decision paths).
Concrete mapping to the shipped registry:
```text
hard_boundary ← must_remain_refused math/logic families (cmb_non_positive_net, cmb_non_integer, …)
ambiguous_struct ← must_remain_refused ambiguity families (cmb_combine_ambiguous)
missing_info ← must_remain_refused underdetermined (cmb_underdetermined, rate_underdetermined)
AND (see next note) the proposal_allowed missing_* families
capability_gap ← proposal_allowed capability families (cmb_unsupported_*, unsupported_rate_duration)
contradiction ← answer_key_contradiction
input_shape ← input_shape (step_aside)
```
- **Introducing `ask` re-audits the *shipped* failure families, and it cuts both
ways.** It splits `must_remain_refused` (the math/contradiction families stay
hard-refuse; the ambiguous/underdetermined-*input* families become **ask** — and
per the intake argument above, a refuse-when-should-ask there is a real loss, not
a label quibble). It also pulls from `proposal_allowed`: `missing_total_count` /
`missing_weighted_total` are *missing-information* (the user could state the
total), not *capability gaps* — once `ask` exists they look mis-classified and
should ask, not propose. This is a real change to shipped behaviour, so it is
**scope-it-don't-silently-re-key** territory, decided in the Q1 derivation slice
with tests, not edited into the registry in passing.
- **`renderability_gap` is a first-class limitation, and the guard is the
question-equivalent of "no fabricated source":** *a question may not name an
entity, slot, unit, or relation that was not grounded in the attempted
comprehension trace.* When `grounded_terms` lacks the field a template needs,
degrade to a generic question or emit `question_unrenderable` — never a named
guess. This is exactly the surface where the CMB reader's adversarial passes kept
catching real wrong=0 bugs; the same discipline applies to asking.
### 1.5.8 Where this meets Stage 2 (the disclosure bus)
This is the off-serving mirror of the Stage 2 served-surface bus. The bus maps:
```text
EpistemicState + LimitationAssessment → ServedDisposition
VERIFIED → disclose answer (widened reach) ← Stage 2 v1 (only one implemented)
UNDETERMINED + missing_information → ASK_QUESTION ← reserved (Q1 tenant)
UNDETERMINED + capability_gap → PROPOSAL ← reserved
UNDETERMINED + scope_boundary → SCOPE_BOUNDARY disclosure ← reserved (RESERVED EpistemicState)
CONTRADICTED → REPORT_CONTRADICTION ← reserved
```
So Stage 2 should be scoped *generally* enough to reserve these dispositions (so a
question is governed for the surface exactly as an answer is), while implementing
only `VERIFIED` first. `QUESTION_NEEDED` then becomes the second tenant on the same
bus: `LimitationAssessment(missing_information) → QUESTION_NEEDED → render → bus`.
---
## 2. The organ — `core/epistemic_questions/`
Located in `core/` (not `generate/`) because question articulation will