# ADR-0037: Execution-time mode side-channel (the three-way submission mode) ## Status Accepted. Design agreed with the user (2026-05-27); the channel is **implemented and validated end-to-end** by its motivating consumer (ADR-0038's DSL → SQL teaching echo). The three-way `EffectiveMode` { `Simple`, `AdvancedPersistent`, `AdvancedOneShot` } resolves at submit time in `App::submit` and threads through `Action::ExecuteDsl` → `runtime::spawn_dsl_dispatch`, which gates `echo::echo_for` on it (handoff-46 commit `04c8e42` shipped the channel + first echo slice; handoff-47 commit `90479cb` proved it carries the full Bucket A catalogue). Redeems the follow-up **deferred by ADR-0033 Amendment 3**, which named this very ADR and its motivating consumer. ## Context ADR-0033 Amendment 3 ("Execution-time mode is side-channel — deferred to its own ADR") recorded a requirement and deferred it: > every command should — at **execution time** — know which of three > modes it ran under: `simple`, `advanced`, or `advanced-one-shot` > (the `:` escape from Simple mode, ADR-0003), so execution can adjust > **output** without changing **identity** (e.g. a Simple-mode > `create table` echoing the generated SQL when run in Advanced mode, > while staying silent in Simple mode). That echo is the **DSL → SQL teaching bridge** (ADR-0030 §10), specified as its own Phase-5 ADR (ADR-0038). ADR-0038 is the motivating — and, at time of writing, only — consumer of this side-channel. This ADR establishes the channel; ADR-0038 consumes it. ### What exists today - The persistent input mode `Mode` (`src/mode.rs`) is **two-way**: `Simple` / `Advanced`. Its doc comment is explicit that the one-shot `:` escape "is handled at submission time in `app::App::submit`, **not as additional state here**." - The one-shot `:` is therefore a **transient, per-submission** property, collapsed to an effective mode at submit time. It is not persistent state and was deliberately kept out of `Mode`. - The only mode information that survives past submission is a **rendering** side-channel: `OutputLine.mode_at_submission: Mode` (the `[simple]`/`[advanced]` echo-line tag in `ui.rs`). It is two-way and exists purely to label the input-echo line. - Neither `Action::ExecuteDsl` nor the database worker (ADR-0010) carries any mode. Execution is mode-agnostic. ### Why the gap blocks the echo The teaching echo (ADR-0038) renders the equivalent SQL **beneath the `[ok]` summary**. That summary is built in the App's outcome handler (`App::update`, the `Dsl*Succeeded` arms) from the **worker's result event** — not at dispatch time. At that point: - The typed `Command` is available (the success events already carry it). - The submission mode is **not** available. `self.mode` is the *current* persistent mode, which is unreliable for this purpose: it can change between submission and outcome, and for a `:` one-shot it never reflected the effective mode of that single line at all. So the echo needs the **effective submission mode delivered to execution/outcome**. And several echo forms (ADR-0038 — auto-resolved index / relationship names, generated `shortid` values, lossy-conversion counts) are only knowable **after the worker has run**, so the echo's data is fundamentally worker-produced. The natural place to gate and build mode-dependent output is therefore the execution path, with the mode threaded to it — exactly the side-channel Amendment 3 anticipated. ## Decision Introduce a **three-way effective submission mode**, computed at submit time and threaded through the `Action` → worker interface, available at **execution time** to adjust **output only**. Command identity, dispatch, and execution semantics are **unchanged** (ADR-0033 Amendment 3: identity is intrinsic to the mode-rooted grammar path, not a flag). ### 1. A new per-submission enum, distinct from persistent `Mode` ```rust /// The effective mode of a single submitted line, resolved at submit /// time. Distinct from the persistent input `Mode` (which stays /// two-way) because the one-shot `:` escape is a transient per-line /// property, never persistent state (ADR-0003; `mode.rs`). pub enum SubmissionMode { Simple, Advanced, AdvancedOneShot, } ``` **This refines Amendment 3's framing.** Amendment 3 sketched "widening `Mode` to the three-way distinction." On implementation review, a **separate enum** is cleaner: `Mode` models *persistent input state*, and `mode.rs` deliberately keeps the one-shot out of it. Folding a transient `AdvancedOneShot` into the persistent `Mode` would contradict that design note and let a non-persistable value leak into persistent- mode call sites. `SubmissionMode` carries the three-way distinction on the per-submission channel where it belongs; `Mode` stays two-way and untouched. The *requirement* Amendment 3 recorded is met; only the **shape** is refined. (Flagged for `/runda`/user challenge — it is an internal architecture choice with no user-facing effect.) The effective mode is resolved at submit time from the persistent `Mode` plus whether the `:` sigil was used: - persistent `Simple`, no `:` → `Simple` - persistent `Simple`, `:` prefix → `AdvancedOneShot` - persistent `Advanced` → `Advanced` (the `:` is a no-op there) ### 2. Threaded through `Action::ExecuteDsl` to the worker `Action::ExecuteDsl` gains a `submission_mode: SubmissionMode` field; the worker request mirrors it. Execution thus knows, per command, the effective mode it ran under. The value is **output-only**: no executor branches its *effect* on it (that would be a behavioural mode dependency, which ADR-0033 Amendment 3 forbids — identity and effect are intrinsic). ### 3. The runtime's execution dispatcher produces the echo; the App renders it For the first consumer (ADR-0038): when the command is a **DSL-form** command (`Command::CreateTable`/`Insert`/… — *not* the `Sql*` variants) and `submission_mode` is `Advanced` or `AdvancedOneShot`, the teaching echo (equivalent SQL + any category-3 expansion data — ADR-0038) is built from the `Command` **plus the worker's execution result**, and the App renders it as de-emphasised `OutputLine`(s) beneath `[ok]`. In `Simple` mode, or for a command typed as SQL, no echo is produced. **Where it is built (build correction — see Implementation notes).** Not in the db.rs worker: the worker receives *decomposed* calls, not the `Command`, so it cannot render `Command → SQL`. The echo is built at the **runtime's `ExecuteDsl` handler**, the one place where the `Command`, the threaded `EffectiveMode`, and the worker's result (resolved auto-names, generated `shortid`s, conversion counts) all converge. This is still **execution-time aware** — it consumes the execution *results* — it just lives at the dispatch layer, not inside the storage worker. **Non-interactive re-execution does not echo.** `replay` (ADR-0034) re-runs recorded commands through the dispatch pipeline in advanced mode (ADR-0033 Amendment 3) — but a per-line teaching echo there would bury the replay summary in noise. So replayed lines (and any future programmatic re-execution) dispatch with a `SubmissionMode` that **suppresses** mode-dependent output: command *identity* still parses in advanced mode (Amendment 3, unchanged), but no echo fires. The mechanism — a fourth non-echoing context, or an `interactive: bool` alongside the mode — is a build choice; the contract fixed here is *replay is silent*. ### 4. Scope of this ADR This ADR establishes the channel and the resolution rule **only**. The echo renderer, its catalogue, and the `Value → SQL-literal` machinery are ADR-0038. The advanced-mode `ALTER COLUMN` gap-fill the echo relies on is the ADR-0035 amendment. No echo behaviour is specified here beyond the gating contract in §3. ## Alternatives considered - **Widen `Mode` to three-way (Amendment 3's literal sketch).** Avoids a new type but conflates transient per-submission state with persistent input state, against `mode.rs`'s explicit design note. Rejected for §1's separate enum. - **App-side gating, worker always returns echo data.** Keep the worker mode-agnostic; have it return echo data on *every* result, and let the App decide whether to render from `mode_at_submission` + command shape. Rejected: it computes the echo unconditionally (including in Simple mode, where it is never shown), does not generalise to other mode- dependent output, and re-opens exactly the "the echo is purely render- side" framing the user has twice ruled against — the settled direction is execution-time mode awareness, per Amendment 3. ## Consequences - Additive and small: a new enum, one field on `Action::ExecuteDsl` and the worker request, and the submit-time resolution rule. No change to parsing, dispatch, command identity, or any executor's effect. - The persistent `Mode` enum and `OutputLine.mode_at_submission` are unchanged. (ADR-0038 may enrich the render tag separately; not required here.) - Future mode-dependent **output** has a home. Anything touching command *identity* or *effect* does not belong here (Amendment 3). - Tests: submit-time resolution for all three cases (incl. `:` one-shot and the Advanced-mode `:`-is-a-no-op case); the field survives the `Action` → worker round-trip; a Simple-mode DSL command yields no echo request while an Advanced / one-shot one does (the gating contract). ## Implementation notes (2026-05-27, during build) Two refinements found when building, recorded so the ADR matches reality: - **Reuse the existing `EffectiveMode`, do not add `SubmissionMode`.** The codebase already has `EffectiveMode { Simple, AdvancedPersistent, AdvancedOneShot }` (`app.rs`), computed by `effective_mode()` and used today for the `:` one-shot UI feedback. It is exactly the three-way, per-submission, *separate-from-`Mode`* enum §1 argued for — so §1's "new enum" is already satisfied; the build reuses `EffectiveMode` (`AdvancedPersistent` is the ADR's `Advanced`). No new type. - **The channel ships with its consumer (merged with ADR-0038).** A threaded-but-unread `EffectiveMode` on the worker request is dead code, which this project's `-D warnings` (nursery) rejects. The side-channel has no consumer other than the echo, so the `Action`→worker threading is built **together with ADR-0038** rather than as a standalone commit — the submit-side resolution (which `Action` carries which `EffectiveMode`) is Tier-1 testable, and the worker-side threading becomes live + end-to-end testable the moment the echo reads it. ## Amendment 1 — Output tag is colour-coded by status, not mode (2026-05-31, issue #10) The original side-channel (§ above) exists "purely to label the **input-echo line**" — the `[simple]`/`[advanced]` tag whose colour tells the learner which mode their command ran under. In implementation, however, the tag-colour rule in `render_output_line` (`src/ui.rs`) was applied to **every** output kind, keyed on `mode_at_submission` regardless of whether the line was an echo. That over-applied the channel: a `[system]` line and an `[error]` line — neither of which is an input echo — both picked up the same mode tint (blue in simple, orange in advanced). The only thing distinguishing a routine `[system]` message from an `[error]` was the **body** colour (green vs red), while the tag — the leftmost glyph the eye lands on — was identical (issue #10). That is backwards for the line a learner most needs to spot fastest. The mode has limited value on an error line; "this is an error" has high value. And flooding the whole error **body** in red makes a long message *harder* to read, not easier. **Change — the status-coloured-tag model.** The output tag is colour-coded by the message's **status** (its `OutputKind`), and the **body** is neutral so the message text stays readable: | Kind | Tag colour | Body | | --- | --- | --- | | `Echo` | **mode tint** (`mode_simple`/`mode_advanced`) — *the sole exception* | `theme.fg` / lexed (unchanged); per-command success rides the trailing ✓/✗ (ADR-0040) | | `System` | `theme.system` (green) | `theme.fg` (was green) | | `TeachingEcho` | `theme.system` (green — it is a `[system]`-tagged line) | dim prefix + lexed SQL (unchanged) | | `Error` | `theme.error` (red) | `theme.fg` **+ BOLD** (was red) | This **narrows** the side-channel to its stated purpose rather than contradicting it: the mode tint now lives **only** on the echo tag, where ADR-0037 always said it belonged. Everything else reads as a status traffic-light — **green tag = ok/info, red tag = error** — which is the same palette as the ✓/✗ echo markers (ADR-0040), so the whole output surface speaks one colour vocabulary. **Why bold-neutral for the error body** (not plain, not red). This is the established diagnostic-rendering convention — `rustc`, `clang`, `tsc`, and most linters colour the **severity label** and render the **message** in the default foreground (bold), not a wall of severity colour. The red moves to the tag (the scan target); the body keeps weight via BOLD without the readability cost of coloured prose. **Scope / non-changes.** - `OutputLine.mode_at_submission` is **unchanged** — still carries the mode for the echo tag. Only *which kinds consult it for colour* changed. - The ✓/✗ completion markers (ADR-0040) are untouched — they already use `theme.system`/`theme.error` directly, and now visually rhyme with the new tag colours. - This supersedes the three options sketched in issue #10 (red tag / amber attention tag / glyph) with a cleaner fourth model that also fixes body readability and the `[system]` tag in one rule. ADR-0040 had flagged the `[error]`/`[system]` tag colours as orthogonal and out of its scope (issue #10) — this amendment closes that gap. **Coverage** (`src/ui.rs` tests): `system_line_renders_green_tag_and_neutral_body`, `error_line_renders_red_tag_and_bold_neutral_body`, `echo_tag_keeps_the_mode_tint_not_a_status_colour` (locks the sole exception across both modes), `teaching_echo_tag_is_green_like_other_system_lines`. ## See also - ADR-0040 — the ✓/✗ completion markers whose green/red palette the status tag now matches; it deferred these tag colours as orthogonal (issue #10), closed by Amendment 1. - ADR-0033 Amendment 3 — deferred this side-channel; defines the intrinsic command-identity model this ADR must not disturb. - ADR-0030 §10 — the DSL → SQL teaching bridge (the motivating consumer). - ADR-0038 — the teaching echo; the consumer built on this channel. - ADR-0003 — input modes and the one-shot `:` escape. - ADR-0010 — the database worker thread this mode is threaded to.