ADR-008: Mocks are opt-in, never silent fallback¶
Status: Accepted Source: prd.md §19
Context¶
ADR-002 introduced MockLlm so the demo could run on a laptop with no AWS credentials. ADR-005 and ADR-007 inherited that pattern through get_llm() plus try: bedrock; except LlmError: MockLlm() in the Clarifier nodes. Across the metric-aware Clarifier work this fallback silently masked six distinct real failures back-to-back (tool-use schema rejection, deeply-nested-field truncation by Haiku 4.5, ~/.aws mounted read-only by Docker, wrong AWS profile, env-var drift across docker compose up invocations, marketplace access not granted for the configured BEDROCK_MODEL_ID). In every case the persisted spec carried the deterministic display_name: GeneratedMockCustomCard and assumptions: ["Offline MockLlm — deterministic placeholder…"], but the operator (and sometimes the agent) saw only "the widget shipped" and moved on. The fallback turned every Bedrock infrastructure failure into a Potemkin success — exactly the failure mode the docs/lessons-learned.md doc had warned about, repeating six times in 48 hours.
Decision¶
BUILDER_MODEis the single switch.backend/app/settings.pygainsbuilder_mode: Literal["live", "offline"] = "live"and aresolved_builder_mode()helper that ORs with the legacyUSE_BEDROCKenv var.liveis the default; missing / broken AWS surfaces as an error rather than a silent fallback.get_llm()returns Mock only in offline mode.backend/app/widgets/llm.py:if mode == "offline": return MockLlm(). Inlivemode, Bedrock init failures raise a newBuilderModeError(subclass ofLlmError) — neverMockLlm(). Per-nodeexcept LlmError: MockLlm().generate_json(...)stanzas inintent_extractor.pyandspec_synthesizer.pyare deleted.- The runner classifies and forwards exceptions.
backend/app/widgets/runner.py_classify_exctranslatesBuilderModeError → kind: "builder_unavailable",LlmError → kind: "llm_error",else → "unknown", attaches the resolvedbuilder_mode, and emits the SSEevent: errorpayload. The result snapshot also carriesbuilder_modeso the frontend can branch UX. - The frontend renders a clear failure UI.
frontend/src/widgets/useWidgetClarifier.tstrackserrorKindandbuilderModealongsideerror.frontend/src/widgets/WidgetBuilderModal.tsxrenders a rose error banner ("LLM unavailable (Bedrock)" + actionable copy +make up-offlineinstruction) whenerrorKind === "builder_unavailable", instead of silently rendering a placeholder spec. - The header always shows the resolved mode.
frontend/src/components/Header.tsxrenders an amber OFFLINE MODE pill whendashboard_state.builder_mode === "offline"so the operator can never confuse the two paths.dashboard_stateexposesbuilder_modeviabackend/app/dashboard_state.py. makeexposes both paths explicitly.make upruns live (defaultBUILDER_MODE=live,USE_BEDROCK=true);make up-offlineruns offline (BUILDER_MODE=offline USE_BEDROCK=false). Demo reviewers picking the offline path get the badge and the deterministic mock; everyone else gets real Bedrock or a hard error.
Consequences¶
- AWS misconfiguration is now visible in seconds instead of after a debugging session. The end-to-end test of breaking AWS_PROFILE on purpose now produces a clean rose banner saying "boto3 init failed: The config profile (does-not-exist) could not be found" — exactly the message you need to fix the problem.
- ADR-002's offline-demo guarantee is preserved (
make up-offlinestill produces a working preview/persist flow againstMockLlm), but the silent-substitution failure mode is gone. - Six lessons-learned entries about Bedrock infra (model access, SSO cache, env-var drift, etc.) move from "footgun you have to remember" to "footgun the system tells you about". They're still in the doc as historical context but the new entry Mocks must be opt-in, never silent fallback governs.
- One small backwards-incompatibility for local dev:
USE_BEDROCK=falsealone now flips the resolved mode tooffline(it always did, but now the UI signals it). Anyone who hadUSE_BEDROCK=falsein a local env file and was relying on the previous behavior of "Bedrock disabled but no offline pill" should explicitly setBUILDER_MODE=offlineand live with the badge. This is the intended outcome. - This ADR's fail-loud discipline extends to all new fail-loud gates: ADR-PROTO-002 (free-text SQL rejection), the Databricks health endpoint (RFC 7807 503), and the
backend/app/sql_gen/routing.pyboot validator (PRD v2.1 §C.5.3) all mirror this shape.
Cross-references¶
- ADR-002 — supersedes its silent-fallback semantics for the Clarifier.
- ADR-005, ADR-007 — Clarifier nodes that previously silently substituted
MockLlm. - ADR-PROTO-002 — extends the same fail-loud discipline to SQL generation.
- Lessons-learned: Mocks must be opt-in, never silent fallback.
- Implementation: backend/app/widgets/llm.py, backend/app/widgets/runner.py.