Files
rdbms-playground/docs/adr/0037-execution-time-mode-side-channel.md
T
claude@clouddev1 ae57c6fc82 feat: colour output tags by status, not mode — readable error bodies (#10)
The output tag was tinted by submission mode for every line kind, so a
[system] line and an [error] line rendered with an identical leftmost
tag — distinguishable only by body colour. And flooding the whole error
body in red made long messages hard to read.

Colour the tag by message status instead (its OutputKind): [system] →
green, [error] → red; the echo tag keeps the mode tint (ADR-0037's
actual purpose — per-command success rides the ✓/✗ marker). Bodies go
neutral; the error body stays bold for weight (rustc-style: severity-
coloured label, readable bold message). Yields a status traffic-light
matching the ✓/✗ palette.

Narrows ADR-0037's mode side-channel to the echo line it was always for.
ADR-0037 Amendment 1; closes the tag-colour gap ADR-0040 flagged as OOS.
2026-05-31 22:02:12 +00:00

14 KiB

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::ExecuteDslruntime::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

/// 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 AdvancedAdvanced (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 shortids, 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.