Compare commits

..

2 commits

Author SHA1 Message Date
Shay
fc70b9a8f5 docs(epistemic): Q1-D — convert scoping brief to accepted/implemented decision record
#668 landed (merge 07149c20). Rewrite from 'five decisions for ruling' to the
durable decision record: D1-D5 ACCEPTED + IMPLEMENTED, with the standalone-delivery
refinement noted and the two remaining served-surface decisions (ask_serving_enabled
+ verified_serving_enabled) deferred to dedicated scoping docs.
2026-06-08 19:31:01 -07:00
Shay
57a0e02148 docs(epistemic): Q1-D ASK bus delivery scoping (NO CODE, HOLD)
Scopes the fourth and final Q1 rung — delivery. Q1-D routes the Q1-C rendered
EpistemicQuestion onto Doc 1's disclosure bus as the QUESTION_NEEDED tenant
(sibling of PROPOSAL_EMITTED), off-serving, consuming the renderer verbatim
(no re-render). Pins five decisions for ruling: D1 off-serving-now vs wait;
D2 the unrenderable-fallback wrong=0 guard; D3 the Q1B_ASK_CARVE_OUT stays
until ask_serving_enabled; D4 the off-serving sink path; D5 strictly single-slot
(no fan-out). Recommends build-off-serving-now, defer served delivery to a
named ask_serving_enabled gate — matching the VERIFIED precedent.
2026-06-08 19:27:45 -07:00
16 changed files with 6 additions and 2242 deletions

View file

@ -335,15 +335,6 @@ class RuntimeConfig:
# default); the engine never raises its own ceiling.
estimation_enabled: bool = False
# ASK serving gate enable flag. When True, ASK serving is allowed.
# Default False (dark).
ask_serving_enabled: bool = False
# VERIFIED serving gate enable flag. When True, VERIFIED serving is allowed.
# Default False (dark).
verified_serving_enabled: bool = False
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
DEFAULT_ETHICS_PACK: str = "default_general_ethics_v1"

View file

@ -19,9 +19,6 @@ Shipped so far (all off-serving — nothing here imports ``generate.derivation``
``EpistemicState × LimitationAssessment × DisclosureClaim ServedDisposition``.
Mapping scaffold only no rendering, no bus, no ``verify.py``; nothing consumes
it yet.
* :mod:`~core.epistemic_disclosure.ask_serving` a narrow Q1-D served-ASK artifact
adapter. It validates already-rendered question artifacts and returns a typed
decision; it does not render prose and does not acquire runtime contemplation.
* :mod:`~core.epistemic_disclosure.verified_contract` (P1-A) the VERIFIED contract:
the obligation, the proof shape, the validator, and the single sanctioned route to
``EpistemicState.VERIFIED`` / ``DisclosureClaim.VERIFIED``. Contract only no
@ -30,10 +27,6 @@ Shipped so far (all off-serving — nothing here imports ``generate.derivation``
from __future__ import annotations
from core.epistemic_disclosure.ask_serving import (
ServedAskDecision,
evaluate_served_ask,
)
from core.epistemic_disclosure.disclosure_claim import (
DEFAULT_DISCLOSURE_CLAIM,
DisclosureClaim,
@ -71,7 +64,6 @@ __all__ = [
"LimitationKind",
"MissingSlot",
"ResolutionAction",
"ServedAskDecision",
"ServedDisposition",
"VerificationObligation",
"VerificationProof",
@ -81,7 +73,6 @@ __all__ = [
"assess_from_family",
"choose_served_disposition",
"disclosure_for_verification",
"evaluate_served_ask",
"evaluate_verification",
"terminal_for_action",
]

View file

@ -1,171 +0,0 @@
"""Stage 2 ASK served-surface artifact adapter.
This module is intentionally narrow: it validates a pre-rendered Q1-D
``DeliveredQuestion`` artifact and decides whether that artifact is eligible to
be exposed as a served ASK/QUESTION_NEEDED surface. It does not acquire
contemplation results from runtime and does not render question prose.
Validation enforces the Q1-D artifact contract:
- top-level JSON object only;
- ``status == "question_only"``;
- ``requires_review is True``;
- ``served is False``;
- ``answer_binding`` is absent or ``None``;
- ``question`` is an object;
- ``question.text`` is a non-empty string;
- ``question.slot_name`` is a non-empty string;
- ``question_path`` exists on disk and differs from ``proposal_path``.
Any validation failure fails closed to the caller's fallback surface and
standing disposition. The served text is consumed from the artifact exactly; no
runtime prose construction or mutation happens here.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.epistemic_disclosure.disposition import ServedDisposition, choose_served_disposition
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_questions.serving_gate import ask_serving_enabled
from core.epistemic_state import EpistemicState
_MISSING = object()
@dataclass(frozen=True, slots=True)
class ServedAskDecision:
"""The adapter's served-ASK decision."""
served: bool
terminal: str
surface: str
disposition: ServedDisposition
def _terminal_value(contemplation_result: Any) -> str:
terminal = getattr(contemplation_result, "terminal", None)
if terminal is None:
return "NO_PROGRESS"
return str(getattr(terminal, "value", terminal))
def _fallback_disposition(terminal: str) -> ServedDisposition:
if terminal == "PROPOSAL_EMITTED":
return ServedDisposition.PROPOSE
if terminal == "SOLVED_VERIFIED":
return ServedDisposition.COMMIT
return ServedDisposition.REFUSE
def _fallback_decision(contemplation_result: Any, fallback_surface: str) -> ServedAskDecision:
terminal = _terminal_value(contemplation_result)
return ServedAskDecision(
served=False,
terminal=terminal,
surface=fallback_surface,
disposition=_fallback_disposition(terminal),
)
def _validate_question_artifact(data: Any, *, question_path: Path, proposal_path: Any) -> str | None:
"""Return the valid question text, or ``None`` for any contract violation."""
if not isinstance(data, dict):
return None
if data.get("status") != "question_only":
return None
if data.get("requires_review") is not True:
return None
served = data.get("served", _MISSING)
if served is _MISSING or served is not False:
return None
answer_binding = data.get("answer_binding", _MISSING)
if answer_binding is not _MISSING and answer_binding is not None:
return None
question = data.get("question")
if not isinstance(question, dict):
return None
text = question.get("text")
if not isinstance(text, str) or not text.strip():
return None
slot_name = question.get("slot_name")
if not isinstance(slot_name, str) or not slot_name.strip():
return None
if proposal_path is not None and str(question_path) == str(proposal_path):
return None
return text.strip()
def evaluate_served_ask(
config: Any,
contemplation_result: Any,
fallback_surface: str,
) -> ServedAskDecision:
"""Evaluate whether a Q1-D question artifact may be surfaced as ASK.
This is a bus/disposition adapter, not a renderer and not the runtime
acquisition path. The caller supplies a contemplation result that already
points to a delivered question artifact. When the gate is disabled or any
artifact invariant fails, the adapter returns the fallback surface and the
standing fallback disposition.
"""
if not ask_serving_enabled(config):
return _fallback_decision(contemplation_result, fallback_surface)
if _terminal_value(contemplation_result) != "QUESTION_NEEDED":
return _fallback_decision(contemplation_result, fallback_surface)
question_path_value = getattr(contemplation_result, "question_path", None)
proposal_path_value = getattr(contemplation_result, "proposal_path", None)
if not question_path_value or question_path_value == proposal_path_value:
return _fallback_decision(contemplation_result, fallback_surface)
question_path = Path(question_path_value)
if not question_path.is_file():
return _fallback_decision(contemplation_result, fallback_surface)
try:
payload = json.loads(question_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return _fallback_decision(contemplation_result, fallback_surface)
question_text = _validate_question_artifact(
payload,
question_path=question_path,
proposal_path=proposal_path_value,
)
if question_text is None:
return _fallback_decision(contemplation_result, fallback_surface)
limitation = LimitationAssessment(
limitation_kind="missing_information",
resolution_action="ask_question",
epistemic_state=EpistemicState.UNDETERMINED,
owner_organ=payload.get("owner_organ"),
blocking_reason=str(payload.get("blocking_reason", "")),
)
disposition = choose_served_disposition(
epistemic_state=EpistemicState.UNDETERMINED,
limitation=limitation,
)
return ServedAskDecision(
served=True,
terminal="QUESTION_NEEDED",
surface=question_text,
disposition=disposition,
)
__all__ = ["ServedAskDecision", "evaluate_served_ask"]

View file

@ -1,33 +0,0 @@
"""VERIFIED serving gate helper — default-dark, no served-surface wiring.
This module centralizes the future kill-switch predicate for VERIFIED serving.
It is default-dark / fail-closed: if the config field is missing or malformed,
it must evaluate to False.
This helper only centralizes the future kill-switch predicate so that future serving code
has one audited predicate. It does not wire any served-surface or implement served
VERIFIED behavior. Missing field means False.
Note that eval-gold-backed producers (such as the verification producer in
evals/constraint_oracle/verified_producer.py) are not serving-eligible.
"""
from __future__ import annotations
from typing import Any
from core.config import DEFAULT_CONFIG, RuntimeConfig
def verified_serving_enabled(config: RuntimeConfig | Any | None = None) -> bool:
"""Return whether served VERIFIED delivery is explicitly enabled.
Missing attribute means False. This is the load-bearing dark-gate invariant:
the served VERIFIED path cannot light merely because the helper exists or because
an older RuntimeConfig instance lacks the future field.
"""
cfg = DEFAULT_CONFIG if config is None else config
return bool(getattr(cfg, "verified_serving_enabled", False))
__all__ = ["verified_serving_enabled"]

View file

@ -1,32 +0,0 @@
"""ASK serving gate helper — default-dark, no served-surface wiring.
This module is the first code slice after the ASK serving-integration scoping brief.
It intentionally does **not** call ``deliver_ask``/``emit_question``, does not import
``chat.runtime``, and does not expose any user-facing surface. It only centralizes the
kill-switch read so future serving code has one audited predicate.
The planned config field is ``RuntimeConfig.ask_serving_enabled``. During this dark-gate
slice the predicate is conservative: absent field == ``False``. That lets the helper land
without widening behavior and preserves the current default for every existing
``RuntimeConfig`` instance.
"""
from __future__ import annotations
from typing import Any
from core.config import DEFAULT_CONFIG, RuntimeConfig
def ask_serving_enabled(config: RuntimeConfig | Any | None = None) -> bool:
"""Return whether served ASK delivery is explicitly enabled.
Missing attribute means ``False``. That is the load-bearing dark-gate invariant:
the served ASK path cannot light merely because the helper exists or because an
older ``RuntimeConfig`` instance lacks the future field.
"""
cfg = DEFAULT_CONFIG if config is None else config
return bool(getattr(cfg, "ask_serving_enabled", False))
__all__ = ["ask_serving_enabled"]

View file

@ -1,199 +0,0 @@
# ASK serving-integration — `ask_serving_enabled` — scoping brief
**Date:** 2026-06-08 · **Status:** scoping (NO CODE) · **HOLD for review** ·
**Branch:** `docs/serving-integration-scoping`
**What this brief is.** The scope for the *one* served-surface decision the ASK lane
deferred. The off-serving ASK lane is complete and on main:
```text
Q1-B typed residue + ask classification (LimitationAssessment.missing_slots)
Q1-C grounded-only rendering (render_question → EpistemicQuestion)
Q1-D off-serving delivery artifact (deliver_ask → QUESTION_NEEDED + sink)
```
`deliver_ask` / `emit_question` exist and are tested, but **nothing calls them from a
live path** — they are produced-but-not-emitted, exactly as P1-B's verified producer
is built-but-not-served. This brief scopes the step that closes that gap: letting a
`QUESTION_NEEDED` actually reach a user, behind a named gate. **No code here** — this
is the decision surface for review.
> Companions: [[q1-d-ask-bus-delivery-scoping-2026-06-08]] (the off-serving rung, §7
> defers exactly this), [[verified-serving-wiring-scoping-2026-06-08]] (the VERIFIED
> half of the same "where off-serving stops" line). Design of record: the session doc
> §5 / §1.5.8 (the disclosure bus).
---
## 1. The five things this decision must pin
The steer named five; each is grounded against shipped code below.
```text
1. ask_serving_enabled — the kill switch
2. the pass_manager integration point — where deliver_ask is actually called
3. the Q1B_ASK_CARVE_OUT retirement gate + registry flip
4. served-surface behaviour for a QUESTION_NEEDED reaching a user
5. the no-question/no-proposal dead-zone proof
```
---
## 2. The integration point (grounded)
`generate/contemplation/pass_manager.py::contemplate(text, *, proposal_root=...)` is
the live contemplation entry. When every attempt is refused it calls
`_classify_all_refused(text, attempts, findings, proposal_root)`, which is where
`emit_proposal` fires for a `proposal_allowed` family and terminates
`PROPOSAL_EMITTED`. **That is the exact analogous site for ASK:** when a refused
attempt's family maps to `ask_question` (via `assess_from_attempt`), call `deliver_ask`
and — on a renderable result — `emit_question` to the sink and terminate
`QUESTION_NEEDED`; on the D2 fallback, fall through to today's proposal/refuse path.
`contemplate()` is called from `chat/runtime.py` at three sites (827, 901, 1364) — so
the contemplation `Terminal` is already on the served path. The served question text
travels the same way `PROPOSAL_EMITTED` already does.
**Two-layer split (the recommendation), mirroring VERIFIED:**
- **Layer A — pass-manager emission (off-serving still).** `contemplate` emits
`QUESTION_NEEDED` + writes the `teaching/questions/` artifact, exactly as it emits
`PROPOSAL_EMITTED` today. This changes the *contemplation* terminal but **reaches no
user** — the teaching loop is off-serving. This layer is buildable behind no gate
(it only adds a terminal the pass can reach), but see §3: it interacts with the
carve-out and so should still wait for the gate, to avoid double-emission churn.
- **Layer B — served delivery (the gated surface).** `chat/runtime.py`, gated by
`ask_serving_enabled`, renders the `DeliveredQuestion.question.text` to the user as
the served response when the terminal is `QUESTION_NEEDED`. This is the only layer
that actually asks the user anything.
Decision to confirm: **do Layer A and Layer B land together behind one gate, or does
Layer A land first (pass emits, nothing served) and Layer B follow?** Recommend
**together, behind `ask_serving_enabled`** — Layer A alone creates the double-emission
state in §3 with no compensating benefit.
---
## 3. The kill switch + the carve-out retirement gate (the coupled core)
### 3.1 `ask_serving_enabled`
Add `ask_serving_enabled: bool = False` to `core/config.py`, the sibling of the
existing `estimation_enabled = False` kill-switch pattern. Default **off**: the
served question path is dark until deliberately enabled, holdout-gated (§5).
### 3.2 Why the carve-out flip is *coupled* to the gate (not to Q1-D)
Q1-B introduced `Q1B_ASK_CARVE_OUT` for `missing_total_count` / `missing_weighted_total`:
the disclosure layer classifies them `ask_question`, but the shipped `REGISTRY` keeps
`proposal_allowed = True` so the proposal pile keeps working. The carve-out's
*retirement condition* is written into `limitation.py`: *"Once ASK is serving, flip
`proposal_allowed = False` on these two families, drop the carve-out set, amend the
test."* The operative word is **serving** — not "delivery exists" (Q1-D already shipped
that off-serving). So:
```text
carve-out retires ⟺ ask_serving_enabled is the ruling
AND a QUESTION_NEEDED is actually served for these families
AND the §4 dead-zone proof holds
```
Until then, `proposal_allowed` stays `True`. During the gate's "off" state both signals
coexist (the off-serving question artifact + the proposal) — intentional, no loss.
### 3.3 The flip, as a single reviewed act
When `ask_serving_enabled` is turned on for these families, in **one** change:
1. `proposal_allowed = False` for `missing_total_count` / `missing_weighted_total`.
2. Drop them from `Q1B_ASK_CARVE_OUT` (empty the set, or remove the constant).
3. Amend the `proposal_allowed` invariant test + the carve-out test.
4. The §4 dead-zone proof test must already be green.
This is the "conscious act, not a silent re-key" the carve-out was built to force.
---
## 4. The no-question/no-proposal dead-zone proof (the wrong=0-adjacent guard)
**The hazard.** The flip removes the proposal signal for these families. If, for some
input class, the family classifies `ask_question` BUT the question is unrenderable
(D2), AND the proposal is now off, the family would terminate `NO_PROGRESS` with **no
served question and no proposal** — a dead zone where a user-resolvable gap produces
*nothing*. That is the ASK-side wrong=0 hazard: not a false answer, but a silent loss
of a capability that previously at least proposed.
**The proof obligation (before any flip).** For every family being flipped, prove that
**no input class lands in the dead zone** — i.e. for every reachable assessment of that
family, the question renders (so `QUESTION_NEEDED` is served), OR the proposal is still
on. Concretely:
- The `missing_*` families have pinned single slots in `_FAMILY_TO_MISSING_SLOTS` with
mapped types (`count_int` / `measured_unit_int`) → they **always render** today, so
the dead zone is currently empty. The proof must show this is *structural*, not
incidental: a test that asserts `deliver_ask` returns `QUESTION_NEEDED` (never a
fallback) for every reachable assessment of the flipped families.
- If any future residue change could make one of these unrenderable (multi-slot,
unmapped type), the flip must be blocked for that family until either the renderer
covers it or the proposal stays on.
**The rule:** `proposal_allowed` may flip `True → False` for a family **only** if a
test proves every reachable ask of that family renders. The dead-zone proof is a
precondition of the flip, enforced like the D2 guard (it must *fail* if a fallback path
is reachable for a flipped family).
---
## 5. Served-surface behaviour + holdout gating
### 5.1 What a served `QUESTION_NEEDED` looks like
When `ask_serving_enabled` and the terminal is `QUESTION_NEEDED`, the served surface
returns the `DeliveredQuestion.question.text` (the grounded-only rendered question) as
the response — distinct from a committed answer, an `[approximate]` disclosure, or a
refusal. It is an **intake request**: the disposition is `ServedDisposition.ASK`
(already mapped in `disposition.py`). The question names nothing ungrounded (Q1-C
guarantee), so it cannot leak a fabricated entity even on the served path.
Open sub-decision: **the served prefix/marker.** VERIFIED gets a distinct `[verified]`
prefix; APPROXIMATE gets `[approximate]`. ASK should get its own surface marker (a
question is neither). Recommend a distinct, tested marker; pin the exact string at
build, not here.
### 5.2 Holdout gate (no quiet widening)
Like VERIFIED, ASK serving must be proven on a holdout before it widens live: a
validate-first probe over a held-out set confirming (a) served questions are
grounded-only (no fabrication escapes on the served path), (b) no family in the flip
set hits the dead zone, (c) the GSM8K serving seal is byte-identical (ASK is
off-the-metric — it asks, it does not answer — but the probe proves it). Only then does
`ask_serving_enabled` go on, one surface at a time.
---
## 6. What this is NOT
- **Not** a dialogue manager / multi-turn state machine — one grounded question, then
the existing flow; the answer round-trip is Q2 (`AnswerBinding`, a separate batch).
- **Not** a re-render — the served path emits the Q1-D `DeliveredQuestion.question.text`
verbatim; no second prose surface.
- **Not** a GSM8K-metric move — ASK asks, it never answers; the pinned SHAs and
`CLAIMS.md` are untouched. The holdout probe proves it.
- **Not** the carve-out flip *yet* — the flip is the terminal act of *this* decision,
gated on `ask_serving_enabled` + the §4 dead-zone proof, not on Q1-D.
---
## 7. The questions for the ruling
1. **Gate:** add `ask_serving_enabled = False` (sibling of `estimation_enabled`)? (rec: yes)
2. **Layering:** land pass-emission (Layer A) and served delivery (Layer B) together
behind the one gate? (rec: yes — Layer A alone only adds double-emission churn)
3. **Carve-out flip:** retire `Q1B_ASK_CARVE_OUT` + flip `proposal_allowed` as a single
reviewed act, gated on the gate AND the §4 dead-zone proof? (rec: yes)
4. **Dead-zone proof:** require a passing "every reachable ask of a flipped family
renders" test as a precondition of any flip? (rec: yes — this is the ASK wrong=0 guard)
5. **Served marker:** a distinct ASK surface marker (not `[verified]` / `[approximate]`)? (rec: yes)
6. **Holdout:** validate-first probe (grounded-only on served path + no dead zone +
GSM8K seal byte-identical) before the gate goes on? (rec: yes)
No served-surface code until this brief is reviewed.

View file

@ -1,144 +0,0 @@
# ASK Serving Integration Scoping — ask_serving_enabled, QUESTION_NEEDED, and Carve-Out Retirement
## 1. Current State
Currently, the ASK capability exists strictly off-serving.
- **Residue Capture (Q1-B):** The `core/epistemic_disclosure/limitation.py` module captures `LimitationAssessment` with typed ASK residue including `MissingSlot` and `grounded_terms`. Specifically, `missing_total_count` and `missing_weighted_total` are classified as ask-oriented inside the disclosure layer.
- **Carve-Out Safety (Q1-B):** The transitional carve-out constant `Q1B_ASK_CARVE_OUT` is defined in `core/epistemic_disclosure/limitation.py`. The registry in `core/comprehension_attempt/failure_family.py` preserves `proposal_allowed=True` for these carve-out families. This guarantees that the proposal-pile signal remains active and uninterrupted until served ASK is fully wired and verified.
- **Grounded Rendering (Q1-C):** The `core/epistemic_questions/render.py` module safely renders `EpistemicQuestion` structures. It enforces structural rendering, ensuring no ungrounded problem-entity names or internal `snake_case` tokens escape to the user. Multi-slot, unmapped, or unsafe cases fall back to `question_unrenderable`.
- **Off-serving Delivery (Q1-D):** The delivery infrastructure in `core/epistemic_questions/delivery.py` defines `DeliveredQuestion` and ships the `Terminal.QUESTION_NEEDED` tenant. The resulting off-serving artifacts are written directly to the `teaching/questions/` sink. No served/user-facing surfaces are exposed.
- **Default-Dark Gate (Helper):** The helper function `ask_serving_enabled` is implemented in `core/epistemic_questions/serving_gate.py`. It operates in a fail-closed, default-dark manner. If the config field `ask_serving_enabled` is absent, it returns `False`. An explicit, truthy configuration is required to allow served ASK.
- **Integration gaps:** Currently, `generate/contemplation/pass_manager.py` does not emit served ASK, `chat/runtime.py` does not expose ASK, and the `Q1B_ASK_CARVE_OUT` remains active (unretired).
## 2. Non-Negotiable Boundary
Before ASK can be delivered to any user-facing surface, the following boundaries must be strictly enforced:
- **Strict Gate Guard:** No served question may be shown without an explicit, active `ask_serving_enabled` configuration check.
- **Fail-Closed Default:** The `ask_serving_enabled` helper must default to `False`. A missing or `None` config attribute must evaluate to `False`.
- **Prose Encapsulation:** The serving layer must not construct or mutate question prose directly. It may only consume pre-rendered `EpistemicQuestion` / `DeliveredQuestion` structures produced by Q1-C/Q1-D.
- **No Contentless Delivery:** An unrenderable ASK must never be promoted to `QUESTION_NEEDED`. Contentless `QUESTION_NEEDED` outcomes are strictly forbidden.
- **Carve-Out Preservation:** The `Q1B_ASK_CARVE_OUT` must remain active and unchanged until served ASK is fully verified. No proposal signal may be lost before a served `QUESTION_NEEDED` is verified live.
- **Sink Distinction:** The `question_only` (teaching/questions) sink must remain logically and physically distinct from the `proposal_only` (teaching/proposals) sink.
- **Zero Impact on Claims:** No benchmark, `CLAIMS.md`, or performance metrics may be modified by this scoping documentation.
## 3. Proposed Served Gate: ask_serving_enabled
The serving gate helper exists under `core/epistemic_questions/serving_gate.py`. This document scoping defines how future served-surface code must interact with the gate:
- **Helper Invariant:** Future code in the served-surface layer (e.g., `chat/runtime.py`) must verify `ask_serving_enabled(config)` before delivering any `QUESTION_NEEDED` response to a user.
- **Off-Serving Isolation:** The gate controls served (user-visible) output only. It must not disable or interfere with off-serving artifacts written to the `teaching/questions/` directory.
### Serving Gate Policy
| Config State | ask_serving_enabled(...) | Served ASK Allowed? |
| --- | --- | --- |
| Missing field | `False` | No |
| `None` / Default config | `False` | No |
| Explicit `False` | `False` | No |
| Explicit `True` | `True` | Only if rendering and delivery obligations pass |
## 4. pass_manager Integration Boundary
This section defines the future integration interface for `generate/contemplation/pass_manager.py`. This is scoping only; no execution is performed here.
1. **Evaluation:** The refusal/comprehension flow yields a `ComprehensionAttempt`.
2. **Assessment:** A `LimitationAssessment` is derived from the attempt.
3. **Resolution Action:** If `resolution_action == "ask_question"`:
- The pipeline invokes `deliver_ask(assessment)`.
- `deliver_ask` calls `render_question` exactly once.
- If the question is renderable, the final `DeliveryOutcome` terminal becomes `QUESTION_NEEDED`.
- If the question is unrenderable, the outcome falls back to the standing disposition (e.g., proposal or refusal).
4. **Gate Enforced downstream:** `generate/contemplation/pass_manager.py` may produce/record ASK delivery outcomes later, but user-visible exposure remains gated downstream by `ask_serving_enabled` (e.g., in `chat/runtime.py`).
- **Constraints:**
- `pass_manager` must not contain prose templates or formatting rules.
- `pass_manager` must not construct `DeliveredQuestion` manually (it must delegate to `deliver_ask`).
- `pass_manager` must never bypass `deliver_ask`.
- `pass_manager` must never emit a contentless `QUESTION_NEEDED`.
## 5. Served Behavior Matrix
| Assessment / Rendering | Gate Disabled | Gate Enabled |
| --- | --- | --- |
| Renderable `ask_question` | No served ASK; existing refusal/proposal behavior preserved | Served `QUESTION_NEEDED` is allowed |
| Unrenderable `ask_question` | Standing fallback; no `QUESTION_NEEDED` | Standing fallback; no `QUESTION_NEEDED` |
| Non-ASK limitation | Unaffected | Unaffected |
| `missing_total_count` / `missing_weighted_total` (carve-out) | Proposal signal preserved | Served ASK only after carve-out retirement proof |
| Multi-slot ASK | Unrenderable fallback | Unrenderable fallback |
*Note: Gate enabled is a necessary but not sufficient condition for serving. The renderable checks and delivery invariants must also pass.*
## 6. Q1B_ASK_CARVE_OUT Retirement Conditions
The transitional carve-out constant `Q1B_ASK_CARVE_OUT` can only be retired and removed from `core/epistemic_disclosure/limitation.py` when the following milestones are met and verified:
1. The `ask_serving_enabled` helper is tested and confirmed default-dark.
2. The `pass_manager.py` ASK integration is fully implemented behind the gate.
3. Served `QUESTION_NEEDED` terminal behavior is successfully tested using renderable `EpistemicQuestion` instances.
4. Unrenderable ASK paths are verified to fall back correctly, never emitting `QUESTION_NEEDED`.
5. Carve-out keys `missing_total_count` and `missing_weighted_total` are proven to yield safe renderable questions (or safe standing fallbacks).
6. A dedicated no-question/no-proposal dead-zone validation test is introduced and passes.
7. The registry in `core/comprehension_attempt/failure_family.py` flips `proposal_allowed=False` for the carve-out families without dropping signal.
8. The `teaching/questions/` (`question_only`) sink remains physically distinct from the `teaching/proposals/` (`proposal_only`) sink.
9. Smoke, contemplation, proposal, and disclosure test suites remain fully green.
*Until all 9 conditions are met, the registry's `proposal_allowed=True` setting for these families must remain.*
## 7. No-Dead-Zone Proof Obligation
### The Dead-Zone Hazard
If `proposal_allowed` is flipped to `False` on a failure family before ASK serving is enabled/renderable, an input falling into that family would yield no proposal signal AND no served question. This results in a silent loss of capability (a dead zone), violating the `wrong=0` refusal-first discipline.
### Required Proof Before Retirement
Before the carve-out is retired, tests must prove that for each carve-out family:
- If `proposal_allowed` is removed, either a served `QUESTION_NEEDED` is emitted safely, or the standing fallback preserves an admissible signal.
- The test suite must assert failure if an evaluation result yields neither a proposal nor a valid served question.
### Suggested Test Names
- `test_ask_serving_disabled_preserves_existing_proposal_signal`
- `test_carveout_retirement_has_no_question_no_proposal_dead_zone`
- `test_unrenderable_ask_never_emits_question_needed`
- `test_pass_manager_uses_deliver_ask_not_direct_rendering`
- `test_question_only_not_proposal_only`
## 8. Required Future Tests Before Wiring
The implementation PR must include tests asserting the following behaviors:
- **Gate Default:** `ask_serving_enabled` defaults to `False`.
- **Config Completeness:** A missing config field evaluates to `False`.
- **Opt-in Activation:** An explicit `True` configuration is required to allow served ASK.
- **Gate Override:** If the gate is disabled, no served ASK is emitted even if `deliver_ask` produces a `QUESTION_NEEDED` outcome.
- **Rendering Obligation:** If the gate is enabled, the output still requires a renderable `EpistemicQuestion`.
- **Unrenderable Fallback:** Unrenderable ASK instances are never served.
- **Prose Isolation:** `pass_manager` does not construct prose templates or strings.
- **Delegated Delivery:** `pass_manager` delegates entirely to `deliver_ask` and does not call `render_question` directly.
- **No Empty Terminals:** No contentless `QUESTION_NEEDED` outcomes are permitted.
- **Sink Safety:** The `question_only` artifact is written to the correct, distinct location.
- **Carve-out Lock:** `Q1B_ASK_CARVE_OUT` is preserved until both the gate and the dead-zone proofs are active.
- **Registry Guard:** Flipped registries are blocked until all retirement conditions are met.
## 9. Non-Claims
This scoping document establishes architectural boundaries only:
- **No Implementation:** It does not implement served ASK.
- **No Wiring:** It does not wire `generate/contemplation/pass_manager.py` or `chat/runtime.py`.
- **No Retirement:** It does not retire `Q1B_ASK_CARVE_OUT` or flip any registry `proposal_allowed` flags.
- **No Metric Changes:** It does not alter GSM8K benchmark claims or refusal behaviors.
- **No General Intelligence:** It does not make ASK generally intelligent; it simply bounds the served-surface gate.
## 10. Recommended Next Slice
To safely approach the implementation, the next sequential slices are recommended:
### Slice 1: Configuration Definition (No Wiring)
- Add a concrete configuration field `RuntimeConfig.ask_serving_enabled: bool = False` (if not already present).
- Ensure the helper `ask_serving_enabled(...)` references this configuration.
- Write configuration tests verifying the default and override values.
- Do not wire `pass_manager` or modify runtime loops.
### Slice 2: pass_manager Integration
- Wire `pass_manager` to call `deliver_ask` without constructing prose; any user-visible ASK exposure remains behind `ask_serving_enabled`; preserve `Q1B_ASK_CARVE_OUT`.

View file

@ -1,317 +0,0 @@
# Stage 2 Disclosure Bus Implementation Map — session pivot to current slices
**Date:** 2026-06-09
**Status:** implementation map / controlling checklist — docs only
This document reconciles the June 8 session-design pivot with the implementation slices that have now landed. It is not a new architecture. It is the map that prevents the work from drifting into isolated feature patches.
## 0. Why this map exists
The recent implementation work touched ASK and VERIFIED serving foundations in small slices. That was correct, but the controlling plan is larger than any one PR:
- `docs/sessions/2026-06-08-practice-attempts-and-servability-blade.md`
- `docs/sessions/2026-06-08-epistemic-question-articulation-first-skill-of-contemplation.md`
- `docs/analysis/stage2-epistemic-disclosure-bus-verified-v1-scoping-2026-06-08.md`
- `docs/analysis/q1-epistemic-question-articulation-v1-scoping-2026-06-08.md`
- `docs/analysis/ask-serving-integration-scoping-2026-06-09.md`
- `docs/analysis/verified-serving-wiring-scoping-2026-06-09.md`
The governing pivot is:
```text
wrong=0 is not a binary answer/refuse command.
wrong=0 is no false presentation of epistemic status.
practice/contemplation may explore typed, isolated candidates.
serving must disclose only what is truthfully labelable.
```
Therefore the served surface should not grow by one-off feature branches. It should grow by activating tenants on the already-scoped Epistemic Disclosure Bus.
## 1. Controlling doctrine from the session docs
### 1.1 Practice versus serving
The practice/servability session separates two lanes:
| Lane | Purpose | License | Primary danger |
| --- | --- | --- | --- |
| practice / contemplation | explore, attempt, eliminate, learn | typed and isolated candidates may exist | getting stuck / never learning |
| serving | emit to a user/downstream surface | no false presentation of epistemic status | misrepresenting uncertainty as truth |
The practical consequence is that internal attempts, ASK artifacts, proposals, and verification proofs may exist off-serving without being user-visible. Serving requires an explicit governance decision.
### 1.2 The servability blade is not a new parallel system
The session document explicitly amends the initial sketch: the servability blade must reuse the already-shipped ADR-0206 response-governance seam (`ReachPolicy`, `govern_response`, `shape_surface`) rather than invent a new parallel object.
For Stage 2 work, every served-surface slice must therefore ask:
```text
Does this activate the existing governance seam, or is it bypassing it?
```
A bypass is architectural drift.
### 1.3 ASK is typed intake, not chat clarification
The question-articulation session defines a question as a typed request for missing state. The first contemplative move is not “ask a question”; it is:
```text
What is preventing resolution — and what KIND of limitation is it?
```
Only limitations of kind `missing_information` or `ambiguous_structure` may become ASK. Capability gaps propose. Hard boundaries refuse. Contradictions report. Input-shape cases step aside.
### 1.4 QUESTION_NEEDED and PROPOSAL_EMITTED are siblings
The session doc draws a hard line:
| Terminal | Meaning |
| --- | --- |
| `QUESTION_NEEDED` | the input is under-specified but the problem is potentially knowable after one missing datum is supplied |
| `PROPOSAL_EMITTED` | the input is sufficiently specified but CORE lacks the transform/capability |
This distinction must be preserved in artifacts, paths, tests, and served surfaces.
## 2. Stage 2 bus frame
The Stage 2 scoping doc reframes the frontier:
```text
What may CORE disclose through the served surface — and under what governed disposition?
```
The bus is the consolidating view:
```text
EpistemicState + LimitationAssessment + proof/license evidence
-> ServedDisposition / disclosure decision
-> shaped surface through the governance seam
```
VERIFIED is not the whole system. It is one tenant. ASK is another. Scope-boundary explanation, contradiction reporting, proposal-only, partial progress, multiple candidates, and provisional working answers are later tenants or modes on the same governance surface.
## 3. Current implementation state
### 3.1 ASK tenant — landed foundation
The following ASK foundation is now present:
```text
Q1-B typed ASK residue / MissingSlot / LimitationAssessment ask_question
Q1-C grounded-only renderer
Q1-D off-serving DeliveredQuestion / deliver_ask / teaching/questions sink
ask_serving_enabled helper, default-dark
RuntimeConfig.ask_serving_enabled = False
pass_manager off-serving ASK integration behind exercise_ask
ContemplationResult.question_path separated from proposal_path
```
Important preserved boundaries:
```text
no chat/runtime.py serving path
no served ASK surface
no Q1B_ASK_CARVE_OUT retirement
no proposal_allowed registry flip
no CLAIMS or benchmark movement
boundary-first remains before ASK
question_only sink remains distinct from proposal_only sink
```
### 3.2 ASK tenant — not yet done
The following are still open:
```text
served ASK / QUESTION_NEEDED through the governance bus
Q2 AnswerBinding and re-run through the owner organ
no-question/no-proposal dead-zone proof
Q1B_ASK_CARVE_OUT retirement proof
registry flip for missing_total_count / missing_weighted_total
broader ASK families beyond the current typed-residue subset
```
### 3.3 VERIFIED tenant — landed foundation
The following VERIFIED foundation is now present:
```text
P1-A VERIFIED contract
P1-B off-serving gold-setup-backed R2 producer
P1-C bound_slots_digest proof hardening
verified_serving_enabled helper, default-dark
RuntimeConfig.verified_serving_enabled = False
verified serving scoping / gold-free independence doc
```
Important preserved boundaries:
```text
no served VERIFIED surface
no verify.py consumption
no eval-gold-backed proof in serving
no CLAIMS or benchmark movement
no gold-free independent reader yet
```
### 3.4 VERIFIED tenant — not yet done
The following are still open:
```text
gold-free independent reader/proof source
poison fixture harness
holdout-gated verification harness
deterministic replay digest proof for served verification traces
verify.py consumption of contract verdict
served [verified] surface behind verified_serving_enabled
```
## 4. Recent PRs in plan terms
| PR | Plan role | Status |
| --- | --- | --- |
| #664 | Q1-B typed ASK residue + carve-out | merged |
| #666 | Q1-C grounded-only renderer | merged |
| #667 | Q1-D decision record | merged |
| #668 | Q1-D off-serving delivery | merged |
| #670 | ASK default-dark gate helper | merged |
| #671 | ASK serving / carve-out retirement scoping | merged |
| #672 | VERIFIED serving / gold-free independence scoping | merged |
| #673 | VERIFIED default-dark gate helper | merged |
| #674 | explicit ASK + VERIFIED config fields | merged |
| #675 | off-serving ASK pass_manager integration | merged |
| #676 | split question_path from proposal_path | merged |
This sequence was mostly faithful to the plan, but the controlling label should be:
```text
ASK tenant implementation on the Epistemic Disclosure Bus foundation
```
not merely:
```text
question feature / pass_manager feature
```
## 5. Correct next-slice ordering
### 5.1 ASK next code slice — bus activation, not runtime bypass
The next ASK code slice should not be described as “wire chat/runtime.” It should be:
```text
Activate ASK/clarify as a served tenant through the disclosure/governance bus.
```
Minimum requirements:
- use `ask_serving_enabled(config)` as a necessary gate;
- consume `ContemplationResult.question_path` / delivered question artifact;
- do not construct question prose in serving;
- do not serve unrenderable ASK;
- preserve `Q1B_ASK_CARVE_OUT`;
- preserve proposal signal when gate is disabled;
- do not flip `proposal_allowed`;
- do not bypass `govern_response` / `shape_surface` if that seam is applicable;
- if the current governance seam cannot carry ASK yet, stop and scope the exact missing adapter first.
Expected test names or equivalents:
```text
test_ask_serving_disabled_preserves_existing_proposal_signal
test_ask_serving_enabled_surfaces_question_needed_from_artifact
test_unrenderable_ask_never_serves_question_needed
test_question_only_not_proposal_only
test_served_ask_does_not_construct_question_prose
test_served_ask_uses_governance_bus_not_parallel_runtime_path
```
### 5.2 ASK following slice — no-dead-zone proof
After served ASK exists behind the gate, prove that the carve-out families cannot fall into a no-question/no-proposal dead zone.
Required proof shape:
```text
for missing_total_count / missing_weighted_total:
gate disabled -> proposal signal preserved
gate enabled + renderable -> QUESTION_NEEDED served safely
gate enabled + unrenderable -> standing fallback, never contentless QUESTION_NEEDED
no case yields neither proposal nor valid question
```
### 5.3 ASK later slice — Q1B_ASK_CARVE_OUT retirement
Only after the no-dead-zone proof passes may a later PR consider:
```text
proposal_allowed=False for missing_total_count / missing_weighted_total
remove/retire Q1B_ASK_CARVE_OUT
```
That PR must be explicit, separate, and guarded by tests.
### 5.4 VERIFIED next code slice — gold-free independent reader
The next VERIFIED code slice is not served VERIFIED. It is:
```text
Design/prototype a gold-free independent reader/proof source.
```
Requirements:
- no eval-gold setup in serving;
- no gold answer;
- no benchmark fixture;
- distinct primary/independent reader lineages;
- convergent canonical read digests;
- strict rejection of same-reader-twice;
- strict rejection of second-solver-over-one-read;
- off-serving only.
### 5.5 VERIFIED later slices
Only after gold-free independence exists:
```text
poison fixture harness
holdout-gated verification harness
verify.py consumption scoping
served [verified] behind verified_serving_enabled
```
## 6. Explicit anti-drift rules
The following are forbidden unless a future ADR/decision record explicitly reopens them:
```text
no standalone chat/runtime ASK patch that bypasses the bus
no served ASK without ask_serving_enabled
no served ASK that constructs prose instead of consuming DeliveredQuestion
no contentless QUESTION_NEEDED
no question artifact under proposal_path
no proposal artifact under question_path
no Q1B carve-out retirement before no-dead-zone proof
no served VERIFIED from eval-gold producer
no verify.py VERIFIED consumption before gold-free independent reader + poison/holdout harness
no direct EpistemicState.VERIFIED construction outside the contract route
no CLAIMS or benchmark movement from off-serving artifacts
```
## 7. Current status checkpoint
As of the `question_path` cleanup landing, the project is here:
```text
Stage 2 bus doctrine: scoped, not fully active
ASK tenant: off-serving foundation complete enough for served-bus scoping
VERIFIED tenant: contract/gate foundation complete, serving blocked on gold-free independence
Next ASK move: bus-governed served ASK, no carve-out retirement
Next VERIFIED move: gold-free independent reader, no served VERIFIED
```
Use this document as the checklist before issuing the next implementation brief.

View file

@ -1,183 +0,0 @@
# VERIFIED serving-wiring — `verified_serving_enabled` — scoping brief
**Date:** 2026-06-08 · **Status:** scoping (NO CODE) · **HOLD for review** ·
**Branch:** `docs/serving-integration-scoping`
**What this brief is.** The scope for the served-surface decision the VERIFIED lane
deferred. The off-serving VERIFIED lane is complete and on main:
```text
P1-A the VERIFIED contract (verified_contract.py — two independent reads converge)
P1-B gold-setup-backed producer (evals/constraint_oracle/verified_producer.py — OFF-SERVING)
P1-C bound_slots_digest (a separable, load-bearing proof obligation)
```
P1-B verifies 7/13 real R2 problems with wrong=0 — but it is **gold-setup-backed**, so
it is structurally off-serving: the independent read is the INV-25 hand-authored gold
SETUP, which is not available at serving time. This brief scopes what a *serving-time*
VERIFIED would require — and why it cannot reuse P1-B. **No code here.**
> Companions: [[ask-serving-integration-scoping-2026-06-08]] (the ASK half of the same
> "where off-serving stops" line), [[VERIFIED-canonical-comparison-scoping-2026-06-06]]
> (the validate-first probe that already KILLED the naive fold-reader producer),
> [[stage2-epistemic-disclosure-bus-verified-v1-scoping-2026-06-08]] (Doc 1).
---
## 1. The seam (grounded) — and why it is still inert by design
`generate/derivation/verify.py::_canonically_verified(verified, problem_text, policy)`
is the ADR-0206 §5 VERIFIED gate — *the only thing that may license a math answer past
gold* (resolve a disagreement STRICT refuses). It **returns `None` today**, so the
widening is structurally inert: disagreement refuses regardless of `policy`, preserving
absolute `wrong == 0`. Its own docstring states the bright line this brief must honour:
> "A reliability *license* (statistical) must NEVER substitute here: math serving is
> absolute-wrong=0, not disclosed like the cognition path."
So VERIFIED serving = replacing that `return None` with a producer that returns a
derivation **only when it is canonically VERIFIED** (proven correct, not merely sound),
behind a kill switch, proven on a holdout. Everything below is the eligibility bar for
that producer.
---
## 2. The five things this decision must pin
```text
1. the gold-free independent-reader requirement — the crux
2. verified_serving_enabled — the kill switch
3. holdout gates — validate-first, no quiet widening
4. proof-producer eligibility — what may plug into _canonically_verified
5. the explicit ban on eval-gold-backed serving — why P1-B cannot serve
```
---
## 3. The gold-free independent-reader requirement (the crux)
VERIFIED means **two independent reads that converge on one canonical structure**
(P1-A): a faithful solve of a *wrong read* is caught because the independent read
disagrees. P1-B's two reads are `read_constraint_problem` (the engine reader) vs the
**gold-authored setup**. At serving time there is no gold. So serving-time VERIFIED
needs a **second, gold-free reader** whose disagreement is the safety mechanism.
The hard constraints, from the killed-probe doc and CLAUDE.md:
- **Independence must be in the READING, not the solving.** Back-substitution catches
solve-errors, never read-errors. Two solvers over one reading is *fake* independence.
The second reader must parse the problem into the same `ConstraintProblem` structure
by a **genuinely different route** (different lineage, asserted by the
`SAME_READER_LINEAGE` firewall / INV-27 reader-disjointness), and converge on the
same canonical signature.
- **No eval gold in the read** (§7). The second reader may not consult, hash, or be
derived from any gold artifact — not the answer, not the setup. If it needs gold to
read, it is P1-B, and P1-B does not serve.
- **Conservative refuse-on-doubt carries wrong=0.** The second reader, like the first,
refuses when uncertain. VERIFIED fires only on *agreement of two confident,
independent reads*; any refusal or disagreement → STRICT refuses (today's behaviour).
**Eligibility, stated as a gate:** a serving-time VERIFIED producer is eligible **only
if** it exhibits two reads with (a) distinct, firewall-asserted lineages, (b) neither
read derived from gold, (c) convergence on one canonical `ConstraintProblem` signature,
(d) back-substitution + boundary-clear + bound-slots (the P1-A/P1-C obligations), and
(e) refuse-preferring failure. Absent any one, it stays `None`.
Whether such a second R2 reader *exists yet* is the open empirical question — the killed
fold-reader probe is the cautionary precedent: complementary readers were ~98% wrong on
the refused set. **This brief does not assume one exists; it sets the bar a candidate
must clear, validate-first, before any wiring.**
---
## 4. `verified_serving_enabled` — the kill switch
Add `verified_serving_enabled: bool = False` to `core/config.py` (sibling of
`estimation_enabled`). Default **off**. When off, `_canonically_verified` returns `None`
unconditionally (today's inert state) regardless of any producer being present. The
switch is the single audited place that lights the seam, and it stays off until §5.
---
## 5. Holdout gates — validate-first, no quiet widening
VERIFIED may not widen live until proven on a **held-out** set it never trained or
tuned on (INV-25 discipline; the killed-probe doc's validate-first rule). The gate, in
order:
1. **Holdout probe (off-serving):** run the candidate producer over a held-out R2 set
with gold answers withheld; require **wrong == 0** on everything it marks VERIFIED,
and that everything it cannot independently verify it *refuses* (over-refusal is
acceptable; one wrong is disqualifying).
2. **Seal byte-identity:** the GSM8K candidate-graph serving seal (pinned SHAs) stays
byte-identical — VERIFIED widens a *different* surface (R2 constraint answers), it
must not perturb the sealed lane.
3. **Disagreement-still-refuses:** a test proving that with the producer wired and the
switch ON, a faithful solve of a deliberately wrong read still refuses (the P1-A
poison test, now on the served path).
4. Only then does `verified_serving_enabled` go on, **one surface at a time** (R2
first; R4 second), each with its own holdout pass.
---
## 6. The served surface — a distinct `[verified]` disclosure
When wired and enabled, a VERIFIED R2 answer is served under its **own** disclosure
claim/marker — the locked decision from Doc 1's review: *VERIFIED gets a distinct
served disclosure mode + `[verified]` prefix, NEVER reused from `[approximate]`*.
VERIFIED is a *license* claim (more licensed than gold-strict), not a *speculation*
claim. The route to it is the only sanctioned one: `disclosure_for_verification(result)`
`(EpistemicState.VERIFIED, DisclosureClaim.VERIFIED)``ServedDisposition.DISCLOSE`
under the P0-3 guard (which already degrades an unbacked VERIFIED claim to COMMIT).
---
## 7. The explicit ban on eval-gold-backed serving (the load-bearing non-claim)
**P1-B must never become a serving producer.** It is gold-setup-backed: its independent
read is the hand-authored gold SETUP. Wiring it into `_canonically_verified` would mean
the engine "verifies" by consulting the answer key's structure — circular, and a
wrong=0 fiction (it would serve VERIFIED on exactly the problems it was handed the setup
for). The ban, stated so a test can enforce it:
- `_canonically_verified` (and any serving producer behind it) may import **nothing**
from `evals/` — no gold loader, no `gold_to_problem`, no `r2_gold`. An AST/import test
asserts the serving path is gold-free, the mirror of the off-serving AST tests.
- P1-B stays in `evals/` precisely so this boundary is structural: `evals → core` is the
allowed direction; `core/generate serving → evals` is forbidden.
- "Verified on a holdout" (§5) is **not** the same as "verified by gold at serving" —
the holdout *measures* a gold-free producer; it never *feeds* gold into one.
This is the VERIFIED analogue of the GSM8K "candidate-graph owns the metric; no bridge
re-enables without sealed/independent wrong=0" discipline.
---
## 8. What this is NOT
- **Not** a claim that a serving-time VERIFIED producer exists — §3 sets the eligibility
bar; whether an R2 second reader clears it is an open, validate-first question.
- **Not** a reliability/statistical license at the math seam — that path is the
cognition `[approximate]` disclosure; math serving is absolute wrong=0 (§1 bright line).
- **Not** a GSM8K-seal move — VERIFIED widens R2 constraint answers, seal stays
byte-identical (§5.2).
- **Not** P1-B promoted to serving — explicitly banned (§7); P1-B stays off-serving.
---
## 9. The questions for the ruling
1. **Eligibility bar:** adopt §3 (a)(e) as the gate any serving VERIFIED producer must
clear — gold-free second reader, independence-in-the-reading, refuse-preferring? (rec: yes)
2. **Gate:** add `verified_serving_enabled = False` (sibling of `estimation_enabled`),
`_canonically_verified` stays `None` while off? (rec: yes)
3. **Holdout:** require the §5 validate-first sequence (wrong=0 on holdout + seal
byte-identity + disagreement-still-refuses) before the switch, R2-first? (rec: yes)
4. **Gold ban:** enforce the §7 import ban (serving path imports nothing from `evals/`)
with a test, keeping P1-B off-serving forever? (rec: yes)
5. **Surface:** serve VERIFIED under its distinct `[verified]` marker via
`disclosure_for_verification` only? (rec: yes — the locked Doc 1 decision)
No served-surface code until this brief is reviewed. Pairs with
[[ask-serving-integration-scoping-2026-06-08]] — together they draw the full line where
off-serving stops.

View file

@ -1,240 +0,0 @@
# VERIFIED Serving Wiring Scoping — verified_serving_enabled and Gold-Free Independence
## 1. Current State
Currently, the VERIFIED verification pipeline exists off-serving only. No served [verified] surface exists, no serving-time config or gate exists, and no `verify.py` serving wiring is implemented.
The existing off-serving spine consists of:
- **P1-A**: Defines the contract in `core/epistemic_disclosure/verified_contract.py`. This contract includes:
- `VerificationProof` (the data structure containing digests and lineages)
- `VerificationObligation` (the strict checklist of obligations)
- `evaluate_verification` (the pure-logic evaluation function enforcing the contract)
- `disclosure_for_verification` (the single sanctioned route to transition a result to the verified state)
- Defines the meaning of **VERIFIED**: two independent reads must converge on a single canonical structure. This requires:
- Independent reads (`primary_reader_lineage` must not equal `independent_reader_lineage`)
- Convergent canonical read digests (`primary_read_digest` must equal `independent_read_digest`)
- `derivation_digest` present
- `bound_slots_digest` present
- `back_substitution_digest` present
- `boundary_clear` is `True`
- `contradiction_clear` is `True`
- `limitation` is `None`
- **P1-B**: Adds an off-serving R2 verification producer in `evals/constraint_oracle/verified_producer.py`:
- Extracts the primary read from the problem text via `read_constraint_problem(text)`.
- Obtains the independent read from a hand-authored gold setup (`gold_setup`).
- The gold answer itself never enters the verification process (the gold structure setup signature is matched, not the answer).
- The producer is strictly for evaluation and runs off-serving only.
- **P1-C**: Introduces `bound_slots_digest` as a distinct, separable proof obligation to ensure the answer binds only to the stated slots (asked unknowns) and not phantom slots.
- **Serving Isolation**:
- No served VERIFIED surface exists.
- No `verified_serving_enabled` gate exists.
- No `verify.py` serving integration exists.
## 2. Hard Non-Claim
The gold-setup-backed producer implemented in `evals/constraint_oracle/verified_producer.py` **cannot** be used for serving.
What P1-B proves:
- The VERIFIED contract logic is sound and functional.
- The verification producer can successfully assemble valid `VerificationProof` objects.
- Wrong reader structures diverge and correctly fail the independent verification check.
- The actual gold answer is not required to verify the canonical structural solve.
What P1-B does **NOT** prove:
- Serving-time independence (since gold setups do not exist for runtime user requests).
- Gold-free verification.
- User-visible served `[verified]` status.
- Benchmark or `CLAIMS.md` movement.
- AGI capabilities.
Any served `[verified]` status requires a gold-free independent read source at serving time.
## 3. Proposed Served Gate: verified_serving_enabled
To control the rollout of serving-time verification, a future configuration gate must be defined:
- The configuration field `verified_serving_enabled` does not exist yet.
- This document does not add or implement this configuration gate.
- The future gate must be default-false and fail-closed: if the config field is missing or malformed, it must evaluate to `False`.
- The gate must only control the served/user-visible `[verified]` surface.
- The gate must never affect off-serving evaluation proof generation or off-serving validation runs.
- The gate must strictly prohibit eval-gold-backed verification pipelines from touching serving code.
- Enabling the gate is necessary but not sufficient: even if `verified_serving_enabled` is `True`, a served VERIFIED verdict is only allowed if all contract proof obligations successfully pass.
| Config State | verified_serving_enabled | Served VERIFIED Allowed? |
| --- | --- | --- |
| missing field | False | No |
| default config | False | No |
| explicit false | False | No |
| explicit true | True | Only if gold-free proof obligations pass |
## 4. Gold-Free Independent Read Requirement
Before any served `[verified]` surface can exist, a serving proof must use a gold-free independent read source.
A served proof must adhere to the following:
- Primary reader lineage is recorded.
- Independent reader lineage is recorded.
- The lineages must use distinct lineage identifiers.
- The primary read digest and independent read digest must converge.
- No gold setup is used.
- No gold answer is used.
- No benchmark fixture is used.
- No eval-lane imports are performed.
- Same reader twice (invoking the exact same reader lineage twice on the same text) is strictly rejected.
- Second solver over one read (running two solvers over the same read signature) is strictly rejected.
### Comparison
* **Invalid (Pseudo-Independence):**
```text
primary read ──> solver A
└───> solver B ──> same answer ──> VERIFIED (INVALID)
```
* **Valid (True Independence):**
```text
primary read lineage ───────────> canonical digest C ──┐
├──> convergent digests ──> VERIFIED (VALID)
gold-free independent lineage ──> canonical digest C ──┘
```
The valid workflow requires:
- Primary read lineage converges to canonical digest `C`.
- Gold-free independent read lineage converges to canonical digest `C`.
- Derivation is computed from stated quantities.
- Bound slots are present.
- Back-substitution succeeds.
- No boundary has fired.
- No contradiction is present.
- No unresolved limitation exists.
The primary unsolved technical milestone is designing and implementing a gold-free independent reader/verifier source.
## 5. Serving Eligibility Criteria
For any response to be eligible for served `[verified]` status, all of the following conditions must be met:
- `source_problem_digest` is present.
- `primary_reader_lineage` is present.
- `independent_reader_lineage` is present.
- `primary_reader_lineage` and `independent_reader_lineage` are distinct.
- `primary_read_digest` is present.
- `independent_read_digest` is present.
- `primary_read_digest` and `independent_read_digest` are identical (converge).
- `derivation_digest` is present.
- `bound_slots_digest` is present.
- `back_substitution_digest` is present.
- `boundary_clear` is `True`.
- `contradiction_clear` is `True`.
- `limitation` is `None`.
- `evaluate_verification(...)` evaluates to `VerificationVerdict.VERIFIED`.
- `disclosure_for_verification(...)` is the only functional pathway used to transition the result to `(EpistemicState.VERIFIED, DisclosureClaim.VERIFIED)`.
- `verified_serving_enabled` is explicitly configured to `True`.
- The proof producer is gold-free and serving-eligible.
## 6. Holdout and Kill-Switch Gates
Before any served `[verified]` behavior can be wired to production, the serving-time verifier must clear a set of holdout and kill-switch gates:
- **Sealed Holdout Lane**: A dedicated evaluation run on a sealed, off-distribution holdout dataset.
- **Wrong=0 Requirement**: Zero incorrect answers are permitted for any result designated as VERIFIED.
- **Poison Fixtures**: Comprehensive verification tests using poison test cases to prove that:
- Wrong-read structures diverge and fail.
- Same-reader-twice reads fail verification.
- Answer matches gold, but missing proof fails verification.
- Absence of refusal alone without proof fails verification.
- Missing `bound_slots_digest` fails verification.
- Triggered boundaries fail verification.
- Contradictions present fail verification.
- Unresolved limitations fail verification.
- **Deterministic Replay Digest**: The verification trace must be replayable, yielding identical digests.
- **Kill-Switch**: Default off / fail-closed behavior must be validated.
- **Gate Disabled Test**: A test must verify that disabling `verified_serving_enabled` suppresses the served `[verified]` label even when a fully valid proof is present.
- **Gate Enabled Test**: A test must verify that enabling `verified_serving_enabled` still requires all proof obligations to pass before serving.
## 7. verify.py Boundary
The transition of the verification verdict to serving must enforce strict boundaries at the `verify.py` layer:
- `verify.py` may eventually consume a verified verdict from the contract.
- `verify.py` must not define or redefine the semantic meaning of VERIFIED.
- The formal meaning of VERIFIED remains exclusively defined in `core/epistemic_disclosure/verified_contract.py`.
- `verify.py` must never attempt to repair failed proofs.
- `verify.py` must not treat raw answer correctness (matching gold) as verification.
- `verify.py` must not import or use eval-lane gold datasets.
- No hot-path repair or silent corrections may occur in `verify.py`.
## 8. Served Behavior Matrix
| Proof / Gate State | Served Behavior |
| --- | --- |
| Valid off-serving eval-gold proof | Never served |
| Valid gold-free proof + gate disabled | No served `[verified]` |
| Valid gold-free proof + gate enabled | Served `[verified]` may be allowed |
| Wrong read divergence | No served `[verified]` |
| Same reader twice | No served `[verified]` |
| Answer matches gold but proof missing | No served `[verified]` |
| No refusal but no proof | No served `[verified]` |
| Boundary fired | No served `[verified]` |
| Contradiction present | No served `[verified]` |
| Unresolved limitation | No served `[verified]` |
## 9. Required Future Tests Before Wiring
The following test suite must be implemented and pass before any serving-time code can land:
- `verified_serving_enabled` defaults to `False`.
- A missing config field for `verified_serving_enabled` resolves to `False`.
- Explicit `True` configuration is required to serve `[verified]`.
- A disabled gate suppresses served `[verified]` even when a valid proof exists.
- An enabled gate still enforces and requires all proof obligations to pass.
- Eval-gold verification producers cannot be imported or accessed by serving modules (AST / dependency check).
- Eval-gold-backed proofs are rejected from being served.
- Same reader twice fails verification.
- Running a second solver over one reading fails verification.
- Answer-gold match without verification proof does not serve `[verified]`.
- Absence of refusal alone does not serve `[verified]`.
- Direct construction of `EpistemicState.VERIFIED` is blocked outside of `disclosure_for_verification`.
- Poison wrong-read inputs do not serve `[verified]`.
- Missing `bound_slots_digest` does not serve `[verified]`.
- Triggered boundary does not serve `[verified]`.
- Contradiction present does not serve `[verified]`.
- Unresolved limitation does not serve `[verified]`.
- `verify.py` consumes the contract outcome but does not define its rules.
## 10. Non-Claims
This scoping document establishes architectural boundaries only.
- This document does not implement served VERIFIED.
- This document does not add the `verified_serving_enabled` config gate.
- This document does not wire `verify.py` to serving.
- This document does not move the evaluation producer (`verified_producer.py`) into the core serving layer.
- This document does not make gold-backed verification serving-safe.
- This document does not change benchmark metrics.
- This document does not update `CLAIMS.md`.
- This document does not solve the design of the gold-free independent reader.
- This document does not claim AGI progress.
## 11. Recommended Next Slices
The implementation of serving-time verification should proceed in isolated, review-gated slices:
### Slice 1: Configuration Gate
- Add default-dark `verified_serving_enabled` helper only.
- No runtime wiring, no `verify.py` wiring.
- Strict import checks preventing eval producer imports.
- Tests demonstrating default-dark behavior and config defaults.
### Slice 2: Gold-Free Independent Reader
- Design and prototype a gold-free independent reader/verifier source.
- Maintain off-serving isolation.
- Generate independent reader lineage and canonical digests.
- No integration with the served path.
### Slice 3: Verification Harness & Poison Fixtures
- Implement a holdout-gated verification harness.
- Integrate poison fixtures and test cases.
- Validate deterministic replay digests.
- Do not expose any served surface.
### Slice 4: verify.py Consumption
- Only after all prior slices are approved and pass tests, scope the consumption of the verified verdict within `verify.py`.
- No implementation without separate architectural review.

View file

@ -28,7 +28,7 @@ from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, TYPE_CHECKING
from typing import Any
from core.comprehension_attempt import (
ComprehensionAttempt,
@ -49,10 +49,6 @@ from generate.rate_comprehension.solver import solve_rate
from generate.contemplation.findings import Finding, Terminal
from generate.meaning_graph.reader import Refusal
if TYPE_CHECKING:
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_questions.delivery import DeliveryOutcome
#: Substantive boundaries that are *recognized-but-unsupported* capabilities (not hard errors).
_UNSUPPORTED_FAMILIES = frozenset(
{
@ -78,76 +74,16 @@ class ContemplationResult:
answer: int | None = None
family: str | None = None
proposal_path: str | None = None
question_path: str | None = None
message: str | None = None
def _delivery_outcome_for_limitation(assessment: LimitationAssessment) -> DeliveryOutcome:
"""Helper to delegate to deliver_ask, pure and testable."""
from core.epistemic_questions.delivery import deliver_ask
return deliver_ask(assessment)
def _handle_ask_delivery(
assessment: LimitationAssessment,
family_name: str,
findings: list[Finding],
attempts: tuple[ComprehensionAttempt, ...],
text: str,
proposal_root: Path | None,
question_root: Path | None,
exercise_ask: bool,
selected_organ: str | None = None,
) -> ContemplationResult:
outcome = _delivery_outcome_for_limitation(assessment)
if outcome.terminal == Terminal.QUESTION_NEEDED:
assert outcome.question is not None
import json
from core.epistemic_questions.delivery import question_path
path = question_path(outcome.question, question_root)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(outcome.question.to_json_dict(), indent=2, sort_keys=True),
encoding="utf-8",
)
findings.append(Finding("ask", f"emitted question-only {assessment.blocking_reason}"))
findings.append(Finding("terminal", Terminal.QUESTION_NEEDED.value))
return ContemplationResult(
Terminal.QUESTION_NEEDED, tuple(findings), attempts,
selected_organ=selected_organ, family=family_name,
proposal_path=None,
question_path=str(path),
)
else:
findings.append(Finding("ask", f"unrenderable ask: {outcome.fallback_reason}"))
findings.append(Finding("terminal", outcome.terminal.value))
if outcome.terminal == Terminal.PROPOSAL_EMITTED:
from core.comprehension_attempt.failure_family import family_by_name
family_obj = family_by_name(family_name)
if family_obj is not None:
path = emit_proposal(text, family_obj, attempts, root=proposal_root)
return ContemplationResult(
Terminal.PROPOSAL_EMITTED, tuple(findings), attempts,
selected_organ=selected_organ, family=family_name,
proposal_path=str(path) if path else None,
)
return ContemplationResult(
outcome.terminal, tuple(findings), attempts,
selected_organ=selected_organ, family=family_name,
)
def contemplate(
text: str,
*,
options: dict[str, Any] | None = None,
answer_key: str | None = None,
proposal_root: Path | None = None,
question_root: Path | None = None,
case_id: str | None = None,
exercise_ask: bool = False,
) -> ContemplationResult:
"""Run one bounded contemplation pass over *text*."""
findings: list[Finding] = []
@ -175,11 +111,11 @@ def contemplate(
if route.status == "routed":
assert route.selected is not None
if route.selected.organ == "r2_constraints":
return _solve_and_verify_r2(text, options, answer_key, findings, attempts, proposal_root, question_root, exercise_ask)
return _solve_and_verify_r2(text, options, answer_key, findings, attempts)
if route.selected.organ == "r3_rate":
return _solve_and_verify_r3(text, options, answer_key, findings, attempts, proposal_root, question_root, exercise_ask)
return _solve_and_verify_r3(text, options, answer_key, findings, attempts)
if route.selected.organ == "r4_combined_rate":
return _solve_and_verify_cmb(text, options, answer_key, findings, attempts, proposal_root, question_root, exercise_ask)
return _solve_and_verify_cmb(text, options, answer_key, findings, attempts)
findings.append(Finding("solve", "r1 admissible setup (numeric answer is the eval lane in v0)"))
findings.append(Finding("terminal", Terminal.SOLVED_VERIFIED.value))
return ContemplationResult(
@ -187,7 +123,7 @@ def contemplate(
)
# route.status == "all_refused"
return _classify_all_refused(text, attempts, findings, proposal_root, question_root, exercise_ask)
return _classify_all_refused(text, attempts, findings, proposal_root)
def _solve_and_verify_r2(
@ -196,26 +132,12 @@ def _solve_and_verify_r2(
answer_key: str | None,
findings: list[Finding],
attempts: tuple[ComprehensionAttempt, ...],
proposal_root: Path | None,
question_root: Path | None,
exercise_ask: bool,
) -> ContemplationResult:
problem = read_constraint_problem(text)
assert not isinstance(problem, Refusal) # routed => the reader admitted a setup
value = answer_constraint_problem(problem)
if isinstance(value, Refusal):
findings.append(Finding("solve", f"solver refused: {value.reason}"))
from core.comprehension_attempt.failure_family import family_by_name
from core.epistemic_disclosure.limitation import assess_from_family
family_obj = family_by_name(value.reason)
if family_obj is not None:
assessment = assess_from_family(family_obj)
if assessment.resolution_action == "ask_question":
if exercise_ask:
return _handle_ask_delivery(
assessment, family_obj.name, findings, attempts, text, proposal_root, question_root, exercise_ask,
selected_organ="r2_constraints"
)
findings.append(Finding("terminal", Terminal.REFUSED_KNOWN_BOUNDARY.value))
return ContemplationResult(
Terminal.REFUSED_KNOWN_BOUNDARY, tuple(findings), attempts,
@ -253,26 +175,12 @@ def _solve_and_verify_r3(
answer_key: str | None,
findings: list[Finding],
attempts: tuple[ComprehensionAttempt, ...],
proposal_root: Path | None,
question_root: Path | None,
exercise_ask: bool,
) -> ContemplationResult:
problem = read_rate_problem(text)
assert not isinstance(problem, Refusal) # routed => the reader admitted a setup
value = solve_rate(problem)
if isinstance(value, Refusal):
findings.append(Finding("solve", f"solver refused: {value.reason}"))
from core.comprehension_attempt.failure_family import family_by_name
from core.epistemic_disclosure.limitation import assess_from_family
family_obj = family_by_name(value.reason)
if family_obj is not None:
assessment = assess_from_family(family_obj)
if assessment.resolution_action == "ask_question":
if exercise_ask:
return _handle_ask_delivery(
assessment, family_obj.name, findings, attempts, text, proposal_root, question_root, exercise_ask,
selected_organ="r3_rate"
)
findings.append(Finding("terminal", Terminal.REFUSED_KNOWN_BOUNDARY.value))
return ContemplationResult(
Terminal.REFUSED_KNOWN_BOUNDARY, tuple(findings), attempts,
@ -310,9 +218,6 @@ def _solve_and_verify_cmb(
answer_key: str | None,
findings: list[Finding],
attempts: tuple[ComprehensionAttempt, ...],
proposal_root: Path | None,
question_root: Path | None,
exercise_ask: bool,
) -> ContemplationResult:
problem = read_combined_rate_problem(text)
assert not isinstance(problem, Refusal) # routed => the reader admitted a setup
@ -322,18 +227,6 @@ def _solve_and_verify_cmb(
# answerable boundary. A solver refusal is a terminal boundary, never a proposal — and the
# reason is namespaced cmb_* so it resolves to the CMB solver family, not R2/R3's.
findings.append(Finding("solve", f"solver refused: {value.reason}"))
from core.comprehension_attempt.failure_family import family_by_name
from core.epistemic_disclosure.limitation import assess_from_family
reason = cmb_reason(value.reason)
family_obj = family_by_name(reason)
if family_obj is not None:
assessment = assess_from_family(family_obj)
if assessment.resolution_action == "ask_question":
if exercise_ask:
return _handle_ask_delivery(
assessment, family_obj.name, findings, attempts, text, proposal_root, question_root, exercise_ask,
selected_organ="r4_combined_rate"
)
findings.append(Finding("terminal", Terminal.REFUSED_KNOWN_BOUNDARY.value))
return ContemplationResult(
Terminal.REFUSED_KNOWN_BOUNDARY, tuple(findings), attempts,
@ -370,8 +263,6 @@ def _classify_all_refused(
attempts: tuple[ComprehensionAttempt, ...],
findings: list[Finding],
proposal_root: Path | None,
question_root: Path | None,
exercise_ask: bool,
) -> ContemplationResult:
# CMB-over-R3 precedence (family side): when CMB substantively recognized the text, R3's broader
# partial classification is suppressed, so CMB's sharper diagnosis owns the terminal/proposal
@ -379,10 +270,9 @@ def _classify_all_refused(
considered = attempts
if cmb_is_authoritative(attempts):
considered = tuple(a for a in attempts if a.organ != "r3_rate")
families = [(a, family_for_reason(a.refusal_reason)) for a in considered]
# Boundary-first: a substantive recognized boundary blocks any proposal or ASK.
# Boundary-first: a substantive recognized boundary blocks any proposal.
for _attempt, family in families:
if family is not None and family.must_remain_refused and family.name != _NOT_MY_DOMAIN:
terminal = (
@ -393,16 +283,6 @@ def _classify_all_refused(
findings.append(Finding("terminal", f"{terminal.value} via {family.name}"))
return ContemplationResult(terminal, tuple(findings), attempts, family=family.name)
# Check for ASK delivery only after substantive boundaries are ruled out.
for attempt in considered:
from core.epistemic_disclosure.limitation import assess_from_attempt
assessment = assess_from_attempt(attempt)
if assessment is not None and assessment.resolution_action == "ask_question":
if exercise_ask:
return _handle_ask_delivery(
assessment, assessment.blocking_reason, findings, attempts, text, proposal_root, question_root, exercise_ask
)
# No substantive boundary: a genuine growth surface may emit a proposal-only artifact.
for _attempt, family in families:
if family is not None and family.proposal_allowed:

View file

@ -1,336 +0,0 @@
"""Focused tests for off-serving ASK delivery integration in the contemplation pass manager (N6).
Ensures that the pass manager seam delegates to deliver_ask, correctly handles fallback dispositions,
avoids direct rendering imports/text construction, and preserves the Q1-B carve-out.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
from core.epistemic_disclosure.limitation import (
Q1B_ASK_CARVE_OUT,
LimitationAssessment,
MissingSlot,
)
from core.epistemic_state import EpistemicState
from generate.contemplation import Terminal, contemplate
from generate.contemplation.pass_manager import (
_delivery_outcome_for_limitation,
)
from core.comprehension_attempt.failure_family import family_by_name
def _make_assessment(
*,
blocking_reason: str,
slots: tuple[MissingSlot, ...],
resolution_action: str = "ask_question",
) -> LimitationAssessment:
return LimitationAssessment(
limitation_kind="missing_information",
resolution_action=resolution_action, # type: ignore[arg-type]
epistemic_state=EpistemicState.UNDETERMINED,
owner_organ="r2_constraint",
blocking_reason=blocking_reason,
missing_slots=slots,
)
_TOTAL_COUNT_SLOT = MissingSlot(
slot_name="total_count",
expected_unit_or_type="count_int",
binding_target="collective_unit_total",
)
_WEIGHTED_SLOT = MissingSlot(
slot_name="weighted_total",
expected_unit_or_type="measured_unit_int",
binding_target="weighted_total_value",
)
def test_pass_manager_uses_deliver_ask_for_renderable_ask() -> None:
assessment = _make_assessment(
blocking_reason="missing_total_count", slots=(_TOTAL_COUNT_SLOT,)
)
outcome = _delivery_outcome_for_limitation(assessment)
assert outcome.terminal == Terminal.QUESTION_NEEDED
assert outcome.question is not None
assert outcome.fallback_reason is None
def test_pass_manager_unrenderable_ask_falls_back_without_question_needed() -> None:
# Multi-slot => unrenderable, falls back to standing disposition (PROPOSAL_EMITTED since missing_total_count is carve-out)
assessment = _make_assessment(
blocking_reason="missing_total_count",
slots=(_TOTAL_COUNT_SLOT, _WEIGHTED_SLOT),
)
outcome = _delivery_outcome_for_limitation(assessment)
assert outcome.terminal == Terminal.PROPOSAL_EMITTED
assert outcome.terminal is not Terminal.QUESTION_NEEDED
assert outcome.question is None
assert outcome.fallback_reason == "multi_slot_not_supported"
def test_pass_manager_does_not_import_or_call_render_question_directly() -> None:
path = (
Path(__file__).resolve().parents[1]
/ "generate"
/ "contemplation"
/ "pass_manager.py"
)
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
# Ensure render_question is not imported, and chat/chat.runtime is not imported
if isinstance(node, ast.ImportFrom):
assert node.module != "core.epistemic_questions.render"
assert node.module != "chat.runtime"
assert node.module != "chat"
if node.names:
for alias in node.names:
assert alias.name != "render_question"
elif isinstance(node, ast.Import):
for alias in node.names:
assert alias.name != "core.epistemic_questions.render"
assert alias.name != "chat.runtime"
assert alias.name != "chat"
# Ensure render_question is not called directly
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
assert node.func.id != "render_question"
def test_pass_manager_does_not_construct_question_text() -> None:
path = (
Path(__file__).resolve().parents[1]
/ "generate"
/ "contemplation"
/ "pass_manager.py"
)
content = path.read_text(encoding="utf-8")
forbidden_templates = ["What ", "Which ", "How many", "Please provide"]
for template in forbidden_templates:
assert template not in content, (
f"pass_manager.py must not construct prose templates like {template!r}"
)
def test_q1b_carveout_preserved_during_pass_manager_ask_integration() -> None:
assert "missing_total_count" in Q1B_ASK_CARVE_OUT
assert "missing_weighted_total" in Q1B_ASK_CARVE_OUT
for name in Q1B_ASK_CARVE_OUT:
family = family_by_name(name)
assert family is not None
assert family.proposal_allowed is True
def test_renderable_ask_path_returns_question_needed_under_exercise_ask(monkeypatch, tmp_path) -> None:
from core.comprehension_attempt import ComprehensionAttempt, RouteResult
import generate.contemplation.pass_manager as pm
from generate.binding_graph.model import SourceSpanLink
span = SourceSpanLink(source_id="src", start=0, end=8, text="chickens")
attempt = ComprehensionAttempt(
organ="r2_constraints",
outcome="setup_refused",
refusal_reason="missing_total_count",
evidence=(span,),
)
monkeypatch.setattr(pm, "route_setup", lambda text, case_id=None: RouteResult((attempt,), None, "all_refused"))
question_root = tmp_path / "teaching" / "questions"
proposal_root = tmp_path / "teaching" / "proposals"
repo_questions_dir = Path(__file__).resolve().parents[1] / "teaching" / "questions"
before_files = set(repo_questions_dir.glob("**/*")) if repo_questions_dir.exists() else set()
# 1. Assert that without exercise_ask, it falls back to PROPOSAL_EMITTED (due to carve-out)
res_normal = contemplate(
"chickens",
proposal_root=proposal_root,
question_root=question_root,
)
assert res_normal.terminal == Terminal.PROPOSAL_EMITTED
assert res_normal.proposal_path is not None
assert res_normal.question_path is None
assert proposal_root in Path(res_normal.proposal_path).parents
# 2. Assert that with exercise_ask=True, it returns QUESTION_NEEDED
calls = []
orig = pm._delivery_outcome_for_limitation
def wrapped(assessment):
calls.append(assessment)
return orig(assessment)
monkeypatch.setattr(pm, "_delivery_outcome_for_limitation", wrapped)
res_ask = contemplate(
"chickens",
proposal_root=proposal_root,
question_root=question_root,
exercise_ask=True,
)
assert res_ask.terminal == Terminal.QUESTION_NEEDED
assert len(calls) == 1 # Verify it is called exactly once! No double delivery / double render!
# Verify the question artifact path
assert res_ask.proposal_path is None
assert res_ask.question_path is not None
artifact_path = Path(res_ask.question_path)
assert artifact_path.exists()
# Assert question artifact is under question_root
assert question_root in artifact_path.parents
# Assert question artifact is not under proposal_root
assert proposal_root not in artifact_path.parents
# Assert no repo-local teaching/questions artifact is created during tests
after_files = set(repo_questions_dir.glob("**/*")) if repo_questions_dir.exists() else set()
assert before_files == after_files
def test_unrenderable_ask_falls_back_in_pass_manager(monkeypatch, tmp_path) -> None:
from core.comprehension_attempt import ComprehensionAttempt, RouteResult
import generate.contemplation.pass_manager as pm
from core.epistemic_questions.delivery import DeliveryOutcome
attempt = ComprehensionAttempt(
organ="r2_constraints",
outcome="setup_refused",
refusal_reason="missing_total_count",
)
monkeypatch.setattr(pm, "route_setup", lambda text, case_id=None: RouteResult((attempt,), None, "all_refused"))
monkeypatch.setattr(
pm,
"_delivery_outcome_for_limitation",
lambda assessment: DeliveryOutcome(Terminal.PROPOSAL_EMITTED, None, "multi_slot_not_supported")
)
question_root = tmp_path / "teaching" / "questions"
proposal_root = tmp_path / "teaching" / "proposals"
res = contemplate(
"chickens",
proposal_root=proposal_root,
question_root=question_root,
exercise_ask=True,
)
assert res.terminal == Terminal.PROPOSAL_EMITTED
assert res.terminal is not Terminal.QUESTION_NEEDED
assert res.proposal_path is not None
assert Path(res.proposal_path).exists()
assert res.question_path is None
def test_family_none_does_not_crash_ask_branch(monkeypatch, tmp_path) -> None:
from core.comprehension_attempt import ComprehensionAttempt, RouteResult
import generate.contemplation.pass_manager as pm
from core.epistemic_disclosure.limitation import LimitationAssessment
import core.epistemic_disclosure.limitation as lim_mod
fake_assessment = LimitationAssessment(
limitation_kind="missing_information",
resolution_action="ask_question",
epistemic_state=EpistemicState.UNDETERMINED,
owner_organ="r2_constraint",
blocking_reason="nonexistent_family_name",
)
attempt = ComprehensionAttempt(
organ="r2_constraints",
outcome="setup_refused",
refusal_reason="nonexistent_family_name",
)
monkeypatch.setattr(pm, "route_setup", lambda text, case_id=None: RouteResult((attempt,), None, "all_refused"))
monkeypatch.setattr(lim_mod, "assess_from_attempt", lambda att: fake_assessment)
question_root = tmp_path / "teaching" / "questions"
proposal_root = tmp_path / "teaching" / "proposals"
res = contemplate(
"chickens",
proposal_root=proposal_root,
question_root=question_root,
exercise_ask=True,
)
assert res.terminal == Terminal.NO_PROGRESS
def test_boundary_wins_over_ask_in_pass_manager(monkeypatch, tmp_path) -> None:
from core.comprehension_attempt import ComprehensionAttempt, RouteResult
import generate.contemplation.pass_manager as pm
from generate.binding_graph.model import SourceSpanLink
span = SourceSpanLink(source_id="src", start=0, end=8, text="chickens")
attempt_boundary = ComprehensionAttempt(
organ="r2_constraints",
outcome="setup_refused",
refusal_reason="too_many_categories", # maps to unsupported_system_size (must_remain_refused = True)
evidence=(span,),
)
attempt_ask = ComprehensionAttempt(
organ="r2_constraints",
outcome="setup_refused",
refusal_reason="missing_total_count", # ask carve-out
evidence=(span,),
)
monkeypatch.setattr(
pm,
"route_setup",
lambda text, case_id=None: RouteResult((attempt_boundary, attempt_ask), None, "all_refused"),
)
question_root = tmp_path / "teaching" / "questions"
proposal_root = tmp_path / "teaching" / "proposals"
res = contemplate(
"chickens",
proposal_root=proposal_root,
question_root=question_root,
exercise_ask=True,
)
# Assert terminal remains REFUSED_UNSUPPORTED_FAMILY or REFUSED_KNOWN_BOUNDARY, not QUESTION_NEEDED
assert res.terminal == Terminal.REFUSED_UNSUPPORTED_FAMILY
assert res.question_path is None
assert res.proposal_path is None
# Ensure no question is written under question_root
assert not question_root.exists() or len(list(question_root.glob("**/*"))) == 0
def test_unrenderable_ask_falls_back_to_no_progress_in_pass_manager(monkeypatch, tmp_path) -> None:
from core.comprehension_attempt import ComprehensionAttempt, RouteResult
import generate.contemplation.pass_manager as pm
from core.epistemic_questions.delivery import DeliveryOutcome
attempt = ComprehensionAttempt(
organ="r2_constraints",
outcome="setup_refused",
refusal_reason="missing_total_count",
)
monkeypatch.setattr(pm, "route_setup", lambda text, case_id=None: RouteResult((attempt,), None, "all_refused"))
monkeypatch.setattr(
pm,
"_delivery_outcome_for_limitation",
lambda assessment: DeliveryOutcome(Terminal.NO_PROGRESS, None, "some_fallback_reason")
)
question_root = tmp_path / "teaching" / "questions"
proposal_root = tmp_path / "teaching" / "proposals"
res = contemplate(
"chickens",
proposal_root=proposal_root,
question_root=question_root,
exercise_ask=True,
)
assert res.terminal == Terminal.NO_PROGRESS
assert res.proposal_path is None
assert res.question_path is None

View file

@ -1,47 +0,0 @@
"""ASK serving gate — default-dark invariant.
This is deliberately narrower than serving integration. It proves the post-scoping
kill-switch predicate is dark unless an operator/config object explicitly opts in.
No chat/runtime wiring, no pass-manager emission, no carve-out retirement.
"""
from __future__ import annotations
from dataclasses import dataclass
from core.config import DEFAULT_CONFIG, RuntimeConfig
from core.epistemic_questions.serving_gate import ask_serving_enabled
@dataclass(frozen=True, slots=True)
class _LegacyConfig:
"""A pre-field config shape: absence of the flag must mean dark."""
unrelated: bool = True
@dataclass(frozen=True, slots=True)
class _OptInConfig:
ask_serving_enabled: bool
def test_default_runtime_config_keeps_ask_serving_dark() -> None:
assert hasattr(RuntimeConfig(), "ask_serving_enabled")
assert RuntimeConfig().ask_serving_enabled is False
assert DEFAULT_CONFIG.ask_serving_enabled is False
assert ask_serving_enabled(DEFAULT_CONFIG) is False
assert ask_serving_enabled(RuntimeConfig()) is False
def test_missing_flag_is_dark_for_legacy_config_shape() -> None:
assert ask_serving_enabled(_LegacyConfig()) is False
def test_gate_only_lights_on_explicit_truthy_opt_in() -> None:
assert ask_serving_enabled(_OptInConfig(False)) is False
assert ask_serving_enabled(_OptInConfig(True)) is True
def test_none_uses_default_config_and_stays_dark() -> None:
assert ask_serving_enabled() is False

View file

@ -1,287 +0,0 @@
"""Focused tests for the Stage 2 served ASK artifact adapter.
These tests intentionally avoid ``chat.runtime``. This slice is adapter-only:
it validates Q1-D question artifacts and returns a typed decision, but does not
wire runtime acquisition of ``ContemplationResult``.
"""
from __future__ import annotations
import ast
import json
from pathlib import Path
from core.config import RuntimeConfig
from core.epistemic_disclosure import ServedAskDecision, evaluate_served_ask
from core.epistemic_disclosure.disposition import ServedDisposition, choose_served_disposition
class DummyTerminal:
def __init__(self, value: str):
self.value = value
class DummyContemplationResult:
def __init__(
self,
terminal: str,
*,
question_path: str | None = None,
proposal_path: str | None = None,
family: str | None = None,
) -> None:
self.terminal = DummyTerminal(terminal)
self.question_path = question_path
self.proposal_path = proposal_path
self.family = family
def _write_artifact(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data), encoding="utf-8")
def _valid_payload(text: str = "How many crates are left?") -> dict:
return {
"status": "question_only",
"blocking_reason": "missing_total_count",
"owner_organ": "r2_constraint",
"question": {
"text": text,
"reason": "missing_total_count",
"slot_name": "total_count",
"expected_unit_or_type": "count_int",
"binding_target": "collective_unit_total",
},
"answer_binding": None,
"requires_review": True,
"served": False,
}
def _question_result(q_path: Path, p_path: Path | None = None) -> DummyContemplationResult:
return DummyContemplationResult(
"QUESTION_NEEDED",
question_path=str(q_path),
proposal_path=str(p_path) if p_path is not None else None,
)
def test_ask_serving_disabled_preserves_existing_fallback_surface(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
_write_artifact(q_path, _valid_payload())
decision = evaluate_served_ask(
RuntimeConfig(ask_serving_enabled=False),
_question_result(q_path),
"fallback",
)
assert isinstance(decision, ServedAskDecision)
assert decision.served is False
assert decision.terminal == "QUESTION_NEEDED"
assert decision.surface == "fallback"
assert decision.disposition is ServedDisposition.REFUSE
def test_ask_serving_enabled_surfaces_question_needed_from_artifact(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
p_path = tmp_path / "proposals" / "p.json"
_write_artifact(q_path, _valid_payload("How many crates are left?"))
decision = evaluate_served_ask(
RuntimeConfig(ask_serving_enabled=True),
_question_result(q_path, p_path),
"fallback",
)
assert decision.served is True
assert decision.terminal == "QUESTION_NEEDED"
assert decision.surface == "How many crates are left?"
assert decision.disposition is ServedDisposition.ASK
def test_served_ask_uses_governance_disposition_bus(monkeypatch, tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
_write_artifact(q_path, _valid_payload())
calls = []
def traced_choose_served_disposition(*args, **kwargs):
calls.append((args, kwargs))
return choose_served_disposition(*args, **kwargs)
monkeypatch.setattr(
"core.epistemic_disclosure.ask_serving.choose_served_disposition",
traced_choose_served_disposition,
)
decision = evaluate_served_ask(
RuntimeConfig(ask_serving_enabled=True),
_question_result(q_path),
"fallback",
)
assert decision.served is True
assert decision.disposition is ServedDisposition.ASK
assert len(calls) == 1
assert calls[0][1]["limitation"].resolution_action == "ask_question"
def test_question_path_must_not_equal_proposal_path(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "same.json"
_write_artifact(q_path, _valid_payload())
decision = evaluate_served_ask(
RuntimeConfig(ask_serving_enabled=True),
_question_result(q_path, q_path),
"fallback",
)
assert decision.served is False
assert decision.surface == "fallback"
def test_non_question_needed_terminal_preserves_proposal_signal(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
_write_artifact(q_path, _valid_payload())
result = DummyContemplationResult(
"PROPOSAL_EMITTED",
question_path=str(q_path),
proposal_path=str(tmp_path / "proposals" / "p.json"),
)
decision = evaluate_served_ask(
RuntimeConfig(ask_serving_enabled=True),
result,
"fallback",
)
assert decision.served is False
assert decision.terminal == "PROPOSAL_EMITTED"
assert decision.surface == "fallback"
assert decision.disposition is ServedDisposition.PROPOSE
def test_missing_or_unreadable_question_artifact_fails_closed(tmp_path: Path) -> None:
missing = tmp_path / "questions" / "missing.json"
decision = evaluate_served_ask(
RuntimeConfig(ask_serving_enabled=True),
_question_result(missing),
"fallback",
)
assert decision.served is False
assert decision.surface == "fallback"
def test_malformed_question_artifact_fails_closed(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
q_path.parent.mkdir(parents=True, exist_ok=True)
q_path.write_text("{bad json", encoding="utf-8")
decision = evaluate_served_ask(
RuntimeConfig(ask_serving_enabled=True),
_question_result(q_path),
"fallback",
)
assert decision.served is False
assert decision.surface == "fallback"
def test_rejects_artifact_status_proposal_only(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
data = _valid_payload()
data["status"] = "proposal_only"
_write_artifact(q_path, data)
decision = evaluate_served_ask(RuntimeConfig(ask_serving_enabled=True), _question_result(q_path), "fallback")
assert decision.served is False
assert decision.surface == "fallback"
def test_rejects_artifact_served_true(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
data = _valid_payload()
data["served"] = True
_write_artifact(q_path, data)
decision = evaluate_served_ask(RuntimeConfig(ask_serving_enabled=True), _question_result(q_path), "fallback")
assert decision.served is False
def test_rejects_artifact_requires_review_false(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
data = _valid_payload()
data["requires_review"] = False
_write_artifact(q_path, data)
decision = evaluate_served_ask(RuntimeConfig(ask_serving_enabled=True), _question_result(q_path), "fallback")
assert decision.served is False
def test_rejects_artifact_non_null_answer_binding(tmp_path: Path) -> None:
q_path = tmp_path / "questions" / "q.json"
data = _valid_payload()
data["answer_binding"] = {"slot": "total_count", "value": 12}
_write_artifact(q_path, data)
decision = evaluate_served_ask(RuntimeConfig(ask_serving_enabled=True), _question_result(q_path), "fallback")
assert decision.served is False
def test_rejects_missing_or_blank_question_text(tmp_path: Path) -> None:
for value in (None, ""):
q_path = tmp_path / f"questions_{value!r}" / "q.json"
data = _valid_payload()
if value is None:
del data["question"]["text"]
else:
data["question"]["text"] = " "
_write_artifact(q_path, data)
decision = evaluate_served_ask(RuntimeConfig(ask_serving_enabled=True), _question_result(q_path), "fallback")
assert decision.served is False
assert decision.surface == "fallback"
def test_rejects_missing_or_blank_slot_name(tmp_path: Path) -> None:
for value in (None, ""):
q_path = tmp_path / f"questions_slot_{value!r}" / "q.json"
data = _valid_payload()
if value is None:
del data["question"]["slot_name"]
else:
data["question"]["slot_name"] = " "
_write_artifact(q_path, data)
decision = evaluate_served_ask(RuntimeConfig(ask_serving_enabled=True), _question_result(q_path), "fallback")
assert decision.served is False
assert decision.surface == "fallback"
def test_adapter_does_not_import_question_renderer_or_construct_prose() -> None:
path = Path(__file__).resolve().parents[1] / "core" / "epistemic_disclosure" / "ask_serving.py"
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
assert node.module != "core.epistemic_questions.render"
assert not node.module.startswith("core.epistemic_questions.render.")
if isinstance(node, ast.Import):
for alias in node.names:
assert alias.name != "core.epistemic_questions.render"
assert not alias.name.startswith("core.epistemic_questions.render.")
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
assert node.func.id != "render_question"
for forbidden_template in ("How many", "What ", "Which ", "Please provide"):
assert forbidden_template not in source

View file

@ -1,31 +0,0 @@
"""Cross-gate configuration field tests.
Verifies that RuntimeConfig has both ask_serving_enabled and
verified_serving_enabled, that both are default false, and that setting
one does not implicitly enable the other.
"""
from __future__ import annotations
import dataclasses
from core.config import RuntimeConfig
def test_cross_serving_gates_independence() -> None:
# 1. Verify both are present on a fresh config and default to False
config = RuntimeConfig()
assert hasattr(config, "ask_serving_enabled")
assert hasattr(config, "verified_serving_enabled")
assert config.ask_serving_enabled is False
assert config.verified_serving_enabled is False
# 2. Verify setting ask_serving_enabled to True does not enable verified_serving_enabled
config_ask = dataclasses.replace(config, ask_serving_enabled=True)
assert config_ask.ask_serving_enabled is True
assert config_ask.verified_serving_enabled is False
# 3. Verify setting verified_serving_enabled to True does not enable ask_serving_enabled
config_verified = dataclasses.replace(config, verified_serving_enabled=True)
assert config_verified.ask_serving_enabled is False
assert config_verified.verified_serving_enabled is True

View file

@ -1,78 +0,0 @@
"""VERIFIED serving gate — default-dark invariant.
This proves the post-scoping kill-switch predicate is dark unless an
operator/config object explicitly opts in.
"""
from __future__ import annotations
import ast
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.config import DEFAULT_CONFIG, RuntimeConfig
from core.epistemic_disclosure.serving_gate import verified_serving_enabled
@dataclass(frozen=True, slots=True)
class _LegacyConfig:
"""A pre-field config shape: absence of the flag must mean dark."""
unrelated: bool = True
@dataclass(frozen=True, slots=True)
class _OptInConfig:
"""Config with the verified_serving_enabled field."""
verified_serving_enabled: bool | None
def test_default_runtime_config_keeps_verified_serving_dark() -> None:
assert hasattr(RuntimeConfig(), "verified_serving_enabled")
assert RuntimeConfig().verified_serving_enabled is False
assert DEFAULT_CONFIG.verified_serving_enabled is False
assert verified_serving_enabled(DEFAULT_CONFIG) is False
assert verified_serving_enabled(RuntimeConfig()) is False
def test_missing_flag_is_dark_for_legacy_config_shape() -> None:
assert verified_serving_enabled(_LegacyConfig()) is False
def test_gate_only_lights_on_explicit_truthy_opt_in() -> None:
assert verified_serving_enabled(_OptInConfig(False)) is False
assert verified_serving_enabled(_OptInConfig(True)) is True
assert verified_serving_enabled(_OptInConfig(None)) is False
def test_none_uses_default_config_and_stays_dark() -> None:
assert verified_serving_enabled() is False
def test_verified_serving_gate_has_no_eval_or_runtime_imports() -> None:
path = Path(__file__).parent.parent / "core/epistemic_disclosure/serving_gate.py"
tree = ast.parse(path.read_text(encoding="utf-8"))
forbidden = {
"evals",
"evals.constraint_oracle",
"evals.constraint_oracle.verified_producer",
"verify",
"chat.runtime",
"generate.contemplation.pass_manager",
}
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
for banned in forbidden:
assert name.name != banned, f"Forbidden import: {name.name}"
assert not name.name.startswith(banned + "."), f"Forbidden import: {name.name}"
elif isinstance(node, ast.ImportFrom):
if node.module:
for banned in forbidden:
assert node.module != banned, f"Forbidden import from: {node.module}"
assert not node.module.startswith(banned + "."), f"Forbidden import from: {node.module}"