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.
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::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, oradvanced-one-shot(the:escape from Simple mode, ADR-0003), so execution can adjust output without changing identity (e.g. a Simple-modecreate tableechoing 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 inapp::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 ofMode. - The only mode information that survives past submission is a
rendering side-channel:
OutputLine.mode_at_submission: Mode(the[simple]/[advanced]echo-line tag inui.rs). It is two-way and exists purely to label the input-echo line. - Neither
Action::ExecuteDslnor 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
Commandis available (the success events already carry it). - The submission mode is not available.
self.modeis 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
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 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
Modeto three-way (Amendment 3's literal sketch). Avoids a new type but conflates transient per-submission state with persistent input state, againstmode.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::ExecuteDsland the worker request, and the submit-time resolution rule. No change to parsing, dispatch, command identity, or any executor's effect. - The persistent
Modeenum andOutputLine.mode_at_submissionare 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 theAction→ 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 addSubmissionMode. The codebase already hasEffectiveMode { Simple, AdvancedPersistent, AdvancedOneShot }(app.rs), computed byeffective_mode()and used today for the:one-shot UI feedback. It is exactly the three-way, per-submission, separate-from-Modeenum §1 argued for — so §1's "new enum" is already satisfied; the build reusesEffectiveMode(AdvancedPersistentis the ADR'sAdvanced). No new type. - The channel ships with its consumer (merged with ADR-0038). A
threaded-but-unread
EffectiveModeon 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 theAction→worker threading is built together with ADR-0038 rather than as a standalone commit — the submit-side resolution (whichActioncarries whichEffectiveMode) 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_submissionis 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.errordirectly, 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.