feat(ask): activate gated served ASK through disclosure bus

This commit is contained in:
Shay 2026-06-09 11:49:42 -07:00
parent 426adbd197
commit e4094d81e9
8 changed files with 586 additions and 69 deletions

View file

@ -525,6 +525,7 @@ class ChatResponse:
recalled_words: tuple[str, ...] = ()
# ADR-0024 Phase 2 — stable refusal reason value
refusal_reason: str = ""
disposition: str = ""
dispatch_trace: DispatchTrace | None = None
@ -1992,6 +1993,7 @@ class ChatRuntime:
discovery_intent_tag: Any = None,
discovery_intent_subject: str | None = None,
dispatch_trace: DispatchTrace | None = None,
contemplation_result: Any | None = None,
) -> ChatResponse:
zero = np.zeros(field_state.F.shape, dtype=np.float32)
prop = Proposition(
@ -2062,58 +2064,85 @@ class ChatRuntime:
grounding_source = grounded_source_tag
else:
grounding_source = "none"
# ADR-0075 (C1) — realizer slot-type guard. Runs BEFORE
# register decoration so a register cannot accidentally heal
# an illegal articulation by wrapping it, and BEFORE anchor-
# lens annotation extraction so the lens annotation never
# rides on a guard-rejected surface. On rejection, route to
# the bounded disclosure string and force grounding_source to
# ``"none"`` (an illegal surface is ungrounded by construction).
# The pre-guard candidate is preserved on walk_surface_stub
# for telemetry — the stub path normally leaves walk_surface as
# _UNKNOWN_DOMAIN_SURFACE, so this swap strictly increases
# observability under rejection.
guard_verdict_stub = _check_realizer_surface(
response_surface,
pos_lookup=self._pos_by_surface.get,
)
realizer_guard_status_stub = guard_verdict_stub.status
realizer_guard_rule_stub = guard_verdict_stub.rule_id
walk_surface_stub = _UNKNOWN_DOMAIN_SURFACE
if guard_verdict_stub.status == "rejected":
walk_surface_stub = response_surface
response_surface = _GUARD_DISCLOSURE_SURFACE
grounding_source = "none"
# ADR-0077 (R6) — register layering separation.
# ``register_canonical_surface`` is the composer / guard output
# BEFORE any register transformation; the pipeline hashes this
# field for ``trace_hash`` so substantive register transforms
# cannot move the truth-path identity. Substantive transforms
# are skipped on ``grounding_source == "none"`` so the bounded
# disclosure stays sacrosanct under terse_v1's drop_articles.
register_canonical_surface_stub = response_surface
if grounding_source == "none":
substantive_surface_stub = response_surface
# Determine the fallback disposition
if refusal_emitted:
fallback_disposition = "refuse"
elif grounding_source in ("vault", "pack", "teaching"):
fallback_disposition = "commit"
else:
substantive_surface_stub = apply_substantive_register(
fallback_disposition = "refuse"
from core.epistemic_disclosure.ask_serving import evaluate_served_ask
decision = evaluate_served_ask(
self.config,
contemplation_result,
response_surface,
)
if decision.served and not refusal_emitted:
response_surface = decision.surface
stub_disposition = decision.disposition.value
stub_epistemic_state = "undetermined"
pre_decoration_surface_stub = response_surface
register_canonical_surface_stub = response_surface
class _DummyDecoration:
surface = decision.surface
variant_id = ""
decoration_stub = _DummyDecoration()
realizer_guard_status_stub = "ok"
realizer_guard_rule_stub = ""
walk_surface_stub = _UNKNOWN_DOMAIN_SURFACE
else:
if contemplation_result is not None:
stub_disposition = decision.disposition.value
else:
stub_disposition = fallback_disposition
stub_epistemic_state = epistemic_state_for_grounding_source(grounding_source).value
# ADR-0075 (C1) — realizer slot-type guard. Runs BEFORE
# register decoration so a register cannot accidentally heal
# an illegal articulation by wrapping it, and BEFORE anchor-
# lens annotation extraction so the lens annotation never
# rides on a guard-rejected surface. On rejection, route to
# the bounded disclosure string and force grounding_source to
# ``"none"`` (an illegal surface is ungrounded by construction).
# The pre-guard candidate is preserved on walk_surface_stub
# for telemetry — the stub path normally leaves walk_surface as
# _UNKNOWN_DOMAIN_SURFACE, so this swap strictly increases
# observability under rejection.
guard_verdict_stub = _check_realizer_surface(
response_surface,
pos_lookup=self._pos_by_surface.get,
)
realizer_guard_status_stub = guard_verdict_stub.status
realizer_guard_rule_stub = guard_verdict_stub.rule_id
walk_surface_stub = _UNKNOWN_DOMAIN_SURFACE
if guard_verdict_stub.status == "rejected":
walk_surface_stub = response_surface
response_surface = _GUARD_DISCLOSURE_SURFACE
grounding_source = "none"
# ADR-0077 (R6) — register layering separation.
register_canonical_surface_stub = response_surface
if grounding_source == "none":
substantive_surface_stub = response_surface
else:
substantive_surface_stub = apply_substantive_register(
response_surface,
self.register_pack,
semantic_domains=pack_semantic_domains,
)
response_surface = substantive_surface_stub
# ADR-0071 (R4) — apply seeded discourse-marker decoration to
# the realized surface AFTER substantive register transforms.
pre_decoration_surface_stub = response_surface
decoration_stub = decorate_surface(
response_surface,
self.register_pack,
semantic_domains=pack_semantic_domains,
turn_idx=len(self.turn_log),
)
response_surface = substantive_surface_stub
# ADR-0071 (R4) — apply seeded discourse-marker decoration to
# the realized surface AFTER substantive register transforms.
# Empty marker buckets ⇒ no-op (UNREGISTERED / neutral / terse).
# Preserve the pre-decoration string so the pipeline can hash
# the truth-path surface and trace_hash stays invariant under
# register (ADR-0069 invariant C, strengthened by ADR-0077).
pre_decoration_surface_stub = response_surface
decoration_stub = decorate_surface(
response_surface,
self.register_pack,
turn_idx=len(self.turn_log),
)
response_surface = decoration_stub.surface
response_surface = decoration_stub.surface
register_id_stub = (
"" if self.register_pack.is_unregistered()
else self.register_pack.register_id
@ -2143,7 +2172,6 @@ class ChatRuntime:
refusal_emitted=refusal_emitted,
hedge_injected=False,
)
stub_epistemic_state = epistemic_state_for_grounding_source(grounding_source).value
stub_normative_clearance = clearance_from_verdicts(verdicts_bundle).value
stub_normative_detail = normative_detail_from_verdicts(verdicts_bundle)
if tokens:
@ -2178,6 +2206,7 @@ class ChatRuntime:
epistemic_state=stub_epistemic_state,
normative_clearance=stub_normative_clearance,
normative_detail=stub_normative_detail,
disposition=stub_disposition,
)
self.turn_log.append(stub_event)
self._emit_turn_event(stub_event)
@ -2237,10 +2266,16 @@ class ChatRuntime:
normative_clearance=stub_normative_clearance,
normative_detail=stub_normative_detail,
refusal_reason=refusal_surface if refusal_emitted else "",
disposition=stub_disposition,
dispatch_trace=dispatch_trace,
)
def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse:
def chat(
self,
text: str,
max_tokens: int | None = None,
contemplation_result: Any | None = None,
) -> ChatResponse:
self._last_input_text = text # W-013: store for explain_last_turn()
tokens = self._tokenize(text)
filtered = self._apply_oov_policy(tokens)
@ -2359,6 +2394,7 @@ class ChatRuntime:
discovery_intent_tag=discovery_intent_tag,
discovery_intent_subject=discovery_intent_subject,
dispatch_trace=dispatch_trace,
contemplation_result=contemplation_result,
)
)
@ -2451,6 +2487,7 @@ class ChatRuntime:
stub = self._stub_response(
field_state,
tokens=tuple(filtered),
contemplation_result=contemplation_result,
)
return self._checkpointed_response(
replace(stub, refusal_reason=_exhaustion_exc.reason.value)
@ -2748,6 +2785,14 @@ class ChatRuntime:
committed_surface=response_surface,
decode_state=main_epistemic,
)
from core.epistemic_disclosure.disposition import choose_served_disposition
from core.epistemic_disclosure.disclosure_claim import DisclosureClaim
main_disposition = choose_served_disposition(
epistemic_state=main_epistemic,
limitation=None,
disclosure_claim=DisclosureClaim.NONE,
).value
main_normative_clearance = clearance_from_verdicts(verdicts_bundle).value
main_normative_detail = normative_detail_from_verdicts(verdicts_bundle)
turn_event = TurnEvent(
@ -2782,6 +2827,7 @@ class ChatRuntime:
normative_clearance=main_normative_clearance,
normative_detail=main_normative_detail,
reach_level=main_reach_level,
disposition=main_disposition,
)
self.turn_log.append(turn_event)
self._emit_turn_event(turn_event)
@ -2834,6 +2880,7 @@ class ChatRuntime:
normative_detail=main_normative_detail,
reach_level=main_reach_level,
refusal_reason=refusal_surface if refusal_emitted else "",
disposition=main_disposition,
dispatch_trace=dispatch_trace,
)
)

View file

@ -75,6 +75,7 @@ def serialize_turn_event(
"normative_detail": str(
getattr(event, "normative_detail", "") or ""
),
"disposition": str(getattr(event, "disposition", "") or ""),
"refusal_emitted": bool(getattr(verdicts, "refusal_emitted", False)),
"hedge_injected": bool(getattr(verdicts, "hedge_injected", False)),
"versor_condition": float(getattr(event, "versor_condition", 0.0)),

View file

@ -54,6 +54,10 @@ from core.epistemic_disclosure.verified_contract import (
disclosure_for_verification,
evaluate_verification,
)
from core.epistemic_disclosure.ask_serving import (
ServedAskDecision,
evaluate_served_ask,
)
__all__ = [
"DEFAULT_DISCLOSURE_CLAIM",
@ -64,6 +68,7 @@ __all__ = [
"LimitationKind",
"MissingSlot",
"ResolutionAction",
"ServedAskDecision",
"ServedDisposition",
"VerificationObligation",
"VerificationProof",
@ -74,5 +79,6 @@ __all__ = [
"choose_served_disposition",
"disclosure_for_verification",
"evaluate_verification",
"evaluate_served_ask",
"terminal_for_action",
]

View file

@ -0,0 +1,135 @@
"""Stage 2 ASK serving/disclosure bus adapter.
Determines whether a pre-rendered question artifact can be served to the user
based on runtime configuration, contemplation result, and artifact validity.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.epistemic_questions.serving_gate import ask_serving_enabled
from core.epistemic_disclosure.disposition import choose_served_disposition, ServedDisposition
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_state import EpistemicState
@dataclass(frozen=True, slots=True)
class ServedAskDecision:
"""The decision made by the served ASK disclosure bus adapter."""
served: bool
terminal: str
surface: str
disposition: ServedDisposition
def evaluate_served_ask(
config: Any,
contemplation_result: Any,
fallback_surface: str,
) -> ServedAskDecision:
"""Evaluate whether to serve a pre-rendered question instead of the fallback surface.
ASK returns an intake request (pre-rendered question content), which is a request
for missing state rather than a committed answer or an approximation. Therefore,
it is governed as an intake request (ServedDisposition.ASK) through
choose_served_disposition rather than formatted as a statistical/approximate answer
surface in shape_surface. The standard ADR-0206 governance seam (ReachPolicy,
govern_response, shape_surface) remains fully preserved for the fallback
answer/refusal paths.
"""
# 1. Fail closed when config or helper is disabled/absent
if not ask_serving_enabled(config):
return _make_fallback_decision(contemplation_result, fallback_surface)
# 2. Check if the contemplation terminal is QUESTION_NEEDED
terminal_val = getattr(contemplation_result, "terminal", None)
if terminal_val is None:
return _make_fallback_decision(contemplation_result, fallback_surface)
terminal_str = getattr(terminal_val, "value", str(terminal_val))
if terminal_str != "QUESTION_NEEDED":
return _make_fallback_decision(contemplation_result, fallback_surface)
# 3. Retrieve question and proposal paths
q_path_attr = getattr(contemplation_result, "question_path", None)
p_path_attr = getattr(contemplation_result, "proposal_path", None)
# Refuse to serve when question_path is missing or equals proposal_path
if not q_path_attr or q_path_attr == p_path_attr:
return _make_fallback_decision(contemplation_result, fallback_surface)
# 4. Attempt to read and parse the DeliveredQuestion artifact
q_path = Path(q_path_attr)
if not q_path.is_file():
return _make_fallback_decision(contemplation_result, fallback_surface)
try:
data = json.loads(q_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return _make_fallback_decision(contemplation_result, fallback_surface)
# Refuse to serve if the artifact is malformed or missing key fields
if not isinstance(data, dict):
return _make_fallback_decision(contemplation_result, fallback_surface)
question_data = data.get("question")
if not isinstance(question_data, dict):
return _make_fallback_decision(contemplation_result, fallback_surface)
question_text = question_data.get("text")
if not question_text or not isinstance(question_text, str) or not question_text.strip():
return _make_fallback_decision(contemplation_result, fallback_surface)
# 5. Use choose_served_disposition to map the ASK resolution
blocking_reason = data.get("blocking_reason", "")
owner_organ = data.get("owner_organ", "r2_constraint")
limitation = LimitationAssessment(
limitation_kind="missing_information",
resolution_action="ask_question",
epistemic_state=EpistemicState.UNDETERMINED,
owner_organ=owner_organ,
blocking_reason=blocking_reason,
)
disposition = choose_served_disposition(
epistemic_state=EpistemicState.UNDETERMINED,
limitation=limitation,
)
return ServedAskDecision(
served=True,
terminal="QUESTION_NEEDED",
surface=question_text,
disposition=disposition,
)
def _make_fallback_decision(
contemplation_result: Any,
fallback_surface: str,
) -> ServedAskDecision:
"""Create a fallback decision preserving the existing proposal/refusal signals."""
terminal_val = getattr(contemplation_result, "terminal", None)
terminal_str = getattr(terminal_val, "value", str(terminal_val)) if terminal_val is not None else "NO_PROGRESS"
# Map the terminal to the correct ServedDisposition for the fallback
if terminal_str == "PROPOSAL_EMITTED":
disposition = ServedDisposition.PROPOSE
elif terminal_str in ("REFUSED_KNOWN_BOUNDARY", "REFUSED_UNSUPPORTED_FAMILY", "AMBIGUOUS_ORGAN", "NO_PROGRESS"):
disposition = ServedDisposition.REFUSE
elif terminal_str == "SOLVED_VERIFIED":
disposition = ServedDisposition.COMMIT
else:
disposition = ServedDisposition.REFUSE
return ServedAskDecision(
served=False,
terminal=terminal_str,
surface=fallback_surface,
disposition=disposition,
)

View file

@ -298,6 +298,7 @@ class TurnEvent:
epistemic_state: str = "undetermined"
normative_clearance: str = "unassessable"
normative_detail: str = ""
disposition: str = ""
# ADR-0206 — Response Governance Bridge reach level for this turn.
# The reach policy that governed the response surface, as a
# lower_snake_case string mirroring core.response_governance.ReachLevel

View file

@ -84,19 +84,17 @@ def test_pass_manager_does_not_import_or_call_render_question_directly() -> None
)
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):
# Ensure render_question is not imported, and chat/chat.runtime/chat.* is not imported
if isinstance(node, ast.ImportFrom) and node.module:
assert node.module != "core.epistemic_questions.render"
assert node.module != "chat.runtime"
assert node.module != "chat"
assert node.module != "chat" and not node.module.startswith("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"
assert alias.name != "chat" and not alias.name.startswith("chat.")
# Ensure render_question is not called directly
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):

View file

@ -0,0 +1,324 @@
"""Focused integration tests for the Stage 2 served ASK slice.
Covers the 6 required test cases ensuring gated served ASK, fallback preservation,
unrenderable ask safety, distinct sinks/paths, no prose construction, and governance bus usage.
"""
from __future__ import annotations
import ast
import json
from pathlib import Path
from typing import Any
import pytest
from chat.runtime import ChatRuntime, ChatResponse
from core.config import RuntimeConfig
from core.epistemic_disclosure.ask_serving import evaluate_served_ask, ServedAskDecision
from core.epistemic_disclosure.disposition import choose_served_disposition, ServedDisposition
from generate.contemplation.pass_manager import ContemplationResult
from generate.contemplation.findings import Terminal
from field.state import FieldState
import numpy as np
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,
):
self.terminal = DummyTerminal(terminal) if isinstance(terminal, str) else terminal
self.question_path = question_path
self.proposal_path = proposal_path
self.family = family
def _write_valid_artifact(path: Path, text: str = "How many chickens are there in total?") -> None:
data = {
"status": "question_only",
"blocking_reason": "missing_total_count",
"owner_organ": "r2_constraint",
"question": {
"text": text,
"reason": "missing_total_count",
"slot_name": "total_count",
},
"requires_review": True,
"served": False,
}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data), encoding="utf-8")
def _get_dummy_field_state() -> FieldState:
from field.state import FieldState
# Create a dummy array of appropriate size for FieldState
return FieldState(np.zeros((32,), dtype=np.float32))
def _get_mocked_runtime(config: RuntimeConfig, monkeypatch: pytest.MonkeyPatch) -> ChatRuntime:
runtime = ChatRuntime(config=config, no_load_state=True)
# Mock safety check to prevent dummy field state from failing versor condition checks
monkeypatch.setattr(
runtime.safety_check,
"check",
lambda *args, **kwargs: type(
"Verdict",
(),
{"upheld": True, "violated_boundaries": (), "runtime_checkable_count": 0},
)(),
)
monkeypatch.setattr(
runtime.ethics_check,
"check",
lambda *args, **kwargs: type(
"Verdict",
(),
{"upheld": True, "violated_commitments": (), "runtime_checkable_count": 0},
)(),
)
return runtime
# 1. test_ask_serving_disabled_preserves_existing_proposal_signal
def test_ask_serving_disabled_preserves_existing_proposal_signal(monkeypatch, tmp_path) -> None:
config = RuntimeConfig(ask_serving_enabled=False)
q_path = tmp_path / "questions" / "q1.json"
p_path = tmp_path / "proposals" / "p1.json"
_write_valid_artifact(q_path)
# Case A: Terminal is QUESTION_NEEDED but serving is disabled
res = DummyContemplationResult(
terminal="QUESTION_NEEDED",
question_path=str(q_path),
proposal_path=str(p_path),
)
decision = evaluate_served_ask(config, res, "fallback_surface")
assert not decision.served
assert decision.disposition == ServedDisposition.REFUSE
# Case B: Check that stub response maps correctly and preserves signals
runtime = _get_mocked_runtime(config, monkeypatch)
stub = runtime._stub_response(
field_state=_get_dummy_field_state(),
contemplation_result=res,
pack_grounded_surface="fallback_surface",
)
assert stub.surface == "fallback_surface"
assert stub.disposition == "refuse"
# Case C: Terminal is PROPOSAL_EMITTED
res_proposal = DummyContemplationResult(
terminal="PROPOSAL_EMITTED",
question_path=str(q_path),
proposal_path=str(p_path),
)
decision_p = evaluate_served_ask(config, res_proposal, "fallback_surface")
assert not decision_p.served
assert decision_p.disposition == ServedDisposition.PROPOSE
stub_p = runtime._stub_response(
field_state=_get_dummy_field_state(),
contemplation_result=res_proposal,
pack_grounded_surface="fallback_surface",
)
assert stub_p.surface == "fallback_surface"
assert stub_p.disposition == "propose"
# 2. test_ask_serving_enabled_surfaces_question_needed_from_artifact
def test_ask_serving_enabled_surfaces_question_needed_from_artifact(monkeypatch, tmp_path) -> None:
config = RuntimeConfig(ask_serving_enabled=True)
q_path = tmp_path / "questions" / "q1.json"
p_path = tmp_path / "proposals" / "p1.json"
question_text = "How many chickens are there in total?"
_write_valid_artifact(q_path, text=question_text)
res = DummyContemplationResult(
terminal="QUESTION_NEEDED",
question_path=str(q_path),
proposal_path=str(p_path),
)
decision = evaluate_served_ask(config, res, "fallback_surface")
assert decision.served
assert decision.surface == question_text
assert decision.disposition == ServedDisposition.ASK
# Check runtime stub response integration
runtime = _get_mocked_runtime(config, monkeypatch)
stub = runtime._stub_response(
field_state=_get_dummy_field_state(),
contemplation_result=res,
pack_grounded_surface="fallback_surface",
)
assert stub.surface == question_text
assert stub.disposition == "ask"
assert stub.epistemic_state == "undetermined"
# 3. test_unrenderable_ask_never_serves_question_needed
def test_unrenderable_ask_never_serves_question_needed(monkeypatch, tmp_path) -> None:
config = RuntimeConfig(ask_serving_enabled=True)
q_path = tmp_path / "questions" / "q1.json"
p_path = tmp_path / "proposals" / "p1.json"
runtime = _get_mocked_runtime(config, monkeypatch)
# Sub-case A: Artifact file does not exist
res_missing = DummyContemplationResult(
terminal="QUESTION_NEEDED",
question_path=str(tmp_path / "nonexistent.json"),
proposal_path=str(p_path),
)
decision = evaluate_served_ask(config, res_missing, "fallback_surface")
assert not decision.served
stub = runtime._stub_response(
field_state=_get_dummy_field_state(),
contemplation_result=res_missing,
pack_grounded_surface="fallback_surface",
)
assert stub.surface == "fallback_surface"
assert stub.disposition == "refuse"
# Sub-case B: Malformed JSON
q_path.parent.mkdir(parents=True, exist_ok=True)
q_path.write_text("{malformed json", encoding="utf-8")
res_malformed = DummyContemplationResult(
terminal="QUESTION_NEEDED",
question_path=str(q_path),
proposal_path=str(p_path),
)
decision = evaluate_served_ask(config, res_malformed, "fallback_surface")
assert not decision.served
stub = runtime._stub_response(
field_state=_get_dummy_field_state(),
contemplation_result=res_malformed,
pack_grounded_surface="fallback_surface",
)
assert stub.surface == "fallback_surface"
# Sub-case C: Question text empty/missing
data = {
"status": "question_only",
"question": {"text": " "} # whitespace only
}
q_path.write_text(json.dumps(data), encoding="utf-8")
res_empty = DummyContemplationResult(
terminal="QUESTION_NEEDED",
question_path=str(q_path),
proposal_path=str(p_path),
)
decision = evaluate_served_ask(config, res_empty, "fallback_surface")
assert not decision.served
stub = runtime._stub_response(
field_state=_get_dummy_field_state(),
contemplation_result=res_empty,
pack_grounded_surface="fallback_surface",
)
assert stub.surface == "fallback_surface"
# 4. test_question_only_not_proposal_only
def test_question_only_not_proposal_only(tmp_path) -> None:
config = RuntimeConfig(ask_serving_enabled=True)
q_path = tmp_path / "questions" / "q1.json"
p_path = tmp_path / "proposals" / "p1.json"
_write_valid_artifact(q_path)
# Sub-case A: question_path is missing
res_no_q = DummyContemplationResult(
terminal="QUESTION_NEEDED",
proposal_path=str(p_path),
)
decision = evaluate_served_ask(config, res_no_q, "fallback_surface")
assert not decision.served
# Sub-case B: question_path equals proposal_path
res_same = DummyContemplationResult(
terminal="QUESTION_NEEDED",
question_path=str(q_path),
proposal_path=str(q_path),
)
decision = evaluate_served_ask(config, res_same, "fallback_surface")
assert not decision.served
# Sub-case C: physically distinct paths
assert q_path.parent != p_path.parent
# 5. test_served_ask_does_not_construct_question_prose
def test_served_ask_does_not_construct_question_prose() -> None:
# AST Guard: check that ask_serving.py does not import render or render_question, and contains no question templates
adapter_path = Path(__file__).resolve().parents[1] / "core" / "epistemic_disclosure" / "ask_serving.py"
runtime_path = Path(__file__).resolve().parents[1] / "chat" / "runtime.py"
forbidden_modules = ("core.epistemic_questions.render",)
forbidden_templates = ("What ", "Which ", "How many", "Please provide")
for path in (adapter_path, runtime_path):
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
assert not any(
node.module == m or node.module.startswith(m + ".") for m in forbidden_modules
), f"{path.name} imports forbidden render module {node.module}"
elif isinstance(node, ast.Import):
for alias in node.names:
assert not any(
alias.name == m or alias.name.startswith(m + ".") for m in forbidden_modules
), f"{path.name} imports forbidden render module {alias.name}"
# Ensure render_question is not called directly
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
assert node.func.id != "render_question", f"{path.name} calls render_question directly"
# Ensure no ad-hoc question prose template is defined in ask_serving.py
content = adapter_path.read_text(encoding="utf-8")
for template in forbidden_templates:
assert template not in content, f"ask_serving.py contains forbidden prose template: {template!r}"
# 6. test_served_ask_uses_governance_bus_not_parallel_runtime_path
def test_served_ask_uses_governance_bus_not_parallel_runtime_path(monkeypatch, tmp_path) -> None:
config = RuntimeConfig(ask_serving_enabled=True)
q_path = tmp_path / "questions" / "q1.json"
p_path = tmp_path / "proposals" / "p1.json"
_write_valid_artifact(q_path)
res = DummyContemplationResult(
terminal="QUESTION_NEEDED",
question_path=str(q_path),
proposal_path=str(p_path),
)
# Monkeypatch choose_served_disposition to trace calls and assert it is called on the governance bus path
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,
)
runtime = _get_mocked_runtime(config, monkeypatch)
stub = runtime._stub_response(
field_state=_get_dummy_field_state(),
contemplation_result=res,
pack_grounded_surface="fallback_surface",
)
assert stub.surface == "How many chickens are there in total?"
assert len(calls) == 1
assert calls[0][1]["epistemic_state"] == "undetermined" # maps to EpistemicState.UNDETERMINED
assert calls[0][1]["limitation"].resolution_action == "ask_question"

View file

@ -217,20 +217,25 @@ def test_question_path_is_content_addressed(tmp_path: Path) -> None:
def test_delivery_is_off_serving() -> None:
"""The delivery module must not import the sealed GSM8K serving substrate."""
path = Path(__file__).resolve().parents[1] / "core" / "epistemic_questions" / "delivery.py"
forbidden = ("generate.derivation", "core.reliability_gate")
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
assert not any(
node.module == f or node.module.startswith(f + ".") for f in forbidden
), f"delivery.py imports forbidden serving module {node.module}"
elif isinstance(node, ast.Import):
for alias in node.names:
"""The delivery and ask_serving modules must not import the sealed GSM8K serving substrate or chat."""
repo_root = Path(__file__).resolve().parents[1]
paths = [
repo_root / "core" / "epistemic_questions" / "delivery.py",
repo_root / "core" / "epistemic_disclosure" / "ask_serving.py",
]
forbidden = ("generate.derivation", "core.reliability_gate", "chat")
for path in paths:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
assert not any(
alias.name == f or alias.name.startswith(f + ".") for f in forbidden
), f"delivery.py imports forbidden serving module {alias.name}"
node.module == f or node.module.startswith(f + ".") for f in forbidden
), f"{path.name} imports forbidden serving module {node.module}"
elif isinstance(node, ast.Import):
for alias in node.names:
assert not any(
alias.name == f or alias.name.startswith(f + ".") for f in forbidden
), f"{path.name} imports forbidden serving module {alias.name}"
def test_question_needed_is_a_distinct_terminal() -> None: