diff --git a/core/epistemic_questions/serving_gate.py b/core/epistemic_questions/serving_gate.py new file mode 100644 index 00000000..f1818b2d --- /dev/null +++ b/core/epistemic_questions/serving_gate.py @@ -0,0 +1,32 @@ +"""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"] diff --git a/tests/test_ask_serving_gate.py b/tests/test_ask_serving_gate.py new file mode 100644 index 00000000..5b7f76f5 --- /dev/null +++ b/tests/test_ask_serving_gate.py @@ -0,0 +1,43 @@ +"""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 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