feat: replace the [ok] summary line with a ✓/✗ echo marker

An audit of the command surface found the `[ok] <verb> <subject>`
summary line duplicated the echo line above it everywhere; its only
unique signal was success-vs-error. Retire it: a command's echo line
now resolves from `running: <input>` to `<input> ✓` / `<input> ✗`
on completion, and the symmetric `"<verb> <subject>" failed:` prefix
is dropped (only the reason remains). Content lines (row counts,
structure, plan tree, teaching echo) are unchanged.

Echo lines carry an EchoStatus; executed commands push Pending and
resolve the oldest-pending echo on their result event (FIFO worker —
correct under interleaving). Parse-time and pre-flight rejections are
not executed and keep their running: + caret rendering. App-command
[ok] lines (rebuild/export/replay) are payload-bearing and untouched.
ADR-0040.
This commit is contained in:
claude@clouddev1
2026-05-30 21:38:48 +00:00
parent f62cccec55
commit 8311de44a8
9 changed files with 546 additions and 124 deletions
@@ -0,0 +1,184 @@
# ADR-0040: A per-command completion marker (✓/✗) replaces the `[ok]` summary line
## Status
**Accepted** — 2026-05-30 (issue #9). Amends the output conventions of
ADR-0014 (data operations), ADR-0028 (query plans / `explain`), and
ADR-0019 (failure rendering); builds on ADR-0037's mode-tagged echo
line.
## Context
Every DSL / data / SQL command renders its outcome in three parts:
```
[<mode>] running: <full input> ← the echo line (OutputKind::Echo, pushed at submit)
[ok] <verb> <subject> ← the summary line (note_ok_summary)
<result content> ← structure / data table / plan tree / row counts
```
Issue #9 started narrow — `[ok] explain Customers` is uninformatively
terse, and (after ADR-0039 routed SQL through the same path)
`explain select …` even renders `[ok] explain` with an **empty**
subject. Pulling the thread, the question generalised: *what does the
`[ok]` line add at all?*
An audit of the whole command surface (the two things the line
carries, against the rest of the output):
| What `[ok]` carries | Already present? |
|---|---|
| verb + subject | **Yes** — verbatim, and more fully, in the echo line directly above |
| success signal (vs `[error]`) | **No** — its only unique contribution |
By command shape:
- **Content-bearing** (create table, add/drop/change column,
insert/update/delete, show data/table, explain): the content below
*is* the proof of success; the verb+subject duplicates the echo.
- **Content-less** (drop table/index/relationship, add index,
add/drop constraint): `[ok]` is the only line printed — but what
does the work there is the **success signal**, not the verb+subject.
The failure path is symmetric: `dsl.failed` =
`'"{verb} {subject}" failed: {rendered}'` — the same redundant
verb+subject prefix, plus the genuinely useful rendered reason.
**Conclusion:** the summary line's text is redundant everywhere; its
sole irreplaceable job is the success-vs-error distinction, needed on
every command. So it can be removed entirely once that one signal
moves elsewhere.
## Decision
Replace the `[ok]` / `"…" failed:` **summary lines** with an inline
**completion marker** on the echo line:
- **Success:** the echo line gains a trailing green **✓**; the
`[ok] <verb> <subject>` line is no longer emitted.
- **Failure:** the echo line gains a trailing red **✗**; the
redundant `"<verb> <subject>" failed:` prefix is dropped and only
the rendered reason is shown (still `[error]`-tagged).
- **The "running:" prefix is a pending state.** The echo reads
`running: <input>` while the worker is in flight and becomes
`<input> ✓` / `<input> ✗` on completion — the line carries the raw
input plus an outcome status the renderer decorates.
- **Marker placement: inline**, immediately after the echoed input
(`[advanced] drop table Orders ✓`). Robust to long inputs that wrap
and needs no render-time width arithmetic, unlike a right-aligned
column.
- **Content is unchanged.** Row-count footers, structure renders, data
tables, plan trees, cascade summaries, auto-fill notes, and the
ADR-0038 teaching echo all still render — they carry the real
information.
### Scope
**Executed commands only.** The marker covers commands that reach the
worker: success (✓), runtime failure (✗), a skipped no-op (✓), and
`replay` (✓/✗ on completion). **Parse-time and pre-flight rejections**
(a parse error, the form-B positional pre-flight, the wrong-count DSL
insert) never reach the worker and do *not* use the `[ok]`/`failed:`
summary — they keep their existing rendering unchanged
(`running: <input>` + a `^` caret + a `parse error:`/teaching
message). They are "didn't run," not "ran and failed," and their caret
alignment is pinned to the `running: ` prefix, so they stay as-is.
This applies to the **DSL / data / SQL command family** — the
commands that push an `OutputKind::Echo` line and route success
through `note_ok_summary`. App-command successes
(`[ok] rebuild — …`, `[ok] export — wrote …`, `[ok] now editing: …`)
are **out of scope and unchanged**: they have no echo line to mark,
do not go through `note_ok_summary`, and their `[ok]` text carries a
real payload (path, summary, project name) rather than a redundant
verb+subject. A small residual inconsistency (app commands keep an
`[ok]` line; DSL commands use a marker) is accepted as the lesser
evil — converting payload-bearing lines to a bare marker would *lose*
information, the opposite of this change's intent.
## Example
Before:
```
[advanced] running: explain show data Customers
[ok] explain Customers
SELECT "id", "Name", "Age" FROM "Customers"
└─ SCAN Customers
```
After:
```
[advanced] explain show data Customers ✓
SELECT "id", "Name", "Age" FROM "Customers"
└─ SCAN Customers
```
Failure, after:
```
[advanced] insert into Customers values (1, 'x') ✗
error: NOT NULL constraint failed: Customers.Age
```
## Consequences
- **Saves one row per command** on a space-constrained TUI, and the
surviving signal (✓/✗) is sharper than a prose `[ok]` line.
- **The echo line gains a lifecycle** (pending → ok/err). The status
is set on completion by locating the line — the existing
`rfind(|l| l.kind == OutputKind::Echo)` already does this lookup.
- **Snapshot churn:** every test that captures command output is
re-baselined. The marker is part of the captured frame, so the
diffs are mechanical but broad.
- **i18n:** `ok.summary` is retired; `dsl.failed` is reduced to the
rendered reason. The marker glyphs are not translatable strings
(they are symbols), consistent with the existing tree connectors.
## Implementation notes (from the design `/runda`)
- **Echo line carries an outcome status** — `Pending` / `Ok` / `Err`
on the `OutputKind::Echo` line. The renderer decorates: `Pending`
`running: <input>`; `Ok``<input> ✓` (green); `Err``<input> ✗`
(red). The raw input is stored; "running:" and the marker are
render-time decoration keyed on status.
- **Attribution: mark the *oldest* still-`Pending` echo** when a
result event arrives — not merely the last echo line. `spawn_dsl_
dispatch` is fire-and-forget and input is not gated, so two commands
*can* be in flight; results arrive in submission order (the db
worker is FIFO), so the oldest-pending echo is the correct target
and a finished command can never leave an earlier one stuck on
`running:`. In practice execution is sub-millisecond and a human
cannot interleave submissions, so there is effectively one command
in flight; the oldest-pending rule is the cheap, order-correct
formalisation of that. (A fully order-independent echo-id round-trip
was considered and rejected as disproportionate plumbing.)
- **Synchronous error paths set `Err` at push time.** Parse errors,
the form-B positional pre-flight, and the wrong-count DSL-insert
pre-flight (`dispatch_dsl`) push their echo already-`Err` (the
outcome is known immediately, no worker round-trip), then render the
reason — so they never dangle on `Pending`.
- **Skipped no-ops set `Ok`.** `CREATE/DROP IF [NOT] EXISTS` that
no-op (`DslCreateSkipped` / `DslDropSkipped` /
`DslDropIndexSkipped` / `DslCreateIndexSkipped`) mark their echo ✓
(the command succeeded as a no-op) and keep their explanatory skip
note beneath.
- **`note_ok_summary` stops emitting the `[ok]` line** but stays as
the success hook: it sets the echo status to `Ok` and still consumes
the ADR-0038 `pending_echo` teaching lines.
- **i18n:** `ok.summary` is retired; `dsl.failed` becomes just the
rendered reason (no `"{verb} {subject}" failed:` prefix). The
`replay.completed`, `rebuild_ok`, `export_ok`, `switched_ok` `[ok]`
templates are payload-bearing and untouched.
## Out of scope
- App-command `[ok]` lines (see Scope).
- The `[WRN]` validity indicator (typing-time; ADR-0027) and the
`[error]`/`[system]` **tag colours** (issue #10) — orthogonal.
## See also
- ADR-0014 — data operations, auto-show, the summary/row-count footers.
- ADR-0028 — `explain` output (SQL echo + plan tree) this sits above.
- ADR-0037 — the mode-tagged echo line the marker attaches to.
- ADR-0019 — the friendly-error rendering the failure reason flows through.
- Issue #9 — the report and the audit that generalised it.
+1
View File
@@ -45,3 +45,4 @@ This directory contains the project's ADRs, recorded per
- [ADR-0037 — Execution-time mode side-channel (the three-way submission mode)](0037-execution-time-mode-side-channel.md) — **Accepted** (design agreed 2026-05-27; channel **implemented + verified end-to-end** by its motivating consumer — ADR-0038's fully-shipped DSL → SQL teaching echo — across handoff-46 `04c8e42` (channel + first echo slice), handoff-47 `90479cb` (full Bucket A), `275c726` (Bucket B resolved-name + multi-statement renderers), `e6ad1ae` (the category-3 `--dont-convert` caveat — gated on this channel too), and `2aab457` (the §4 styled-runs rendering polish)), **redeems the follow-up deferred by ADR-0033 Amendment 3** (which named this ADR and its motivating consumer). Establishes the channel that lets a command know, **at execution time**, the effective mode it ran under — so execution can adjust **output** without touching **identity** (the motivating case: a DSL-form `create table` echoing the equivalent SQL when run in advanced mode, silent in simple — ADR-0030 §10, realised by ADR-0038). Introduces a **new per-submission enum `SubmissionMode` { Simple, Advanced, AdvancedOneShot }***refining* Amendment 3's "widen `Mode`" sketch: the persistent input `Mode` stays **two-way** (`mode.rs` keeps the one-shot `:` out of persistent state), and the three-way distinction lives on the per-submission channel where the transient `:` belongs. Resolved at submit time (Simple+`:``AdvancedOneShot`; Advanced `:` is a no-op), threaded through `Action::ExecuteDsl` → worker, **output-only** (no executor branches its *effect* on it — Amendment 3 forbids behavioural mode dependence). The worker builds the teaching echo (+ category-3 expansion data — ADR-0038) for DSL-form commands in advanced/one-shot mode and returns it; the App renders it beneath `[ok]`. Co-located with execution because the echo's harder forms (resolved auto-names, generated `shortid`s, conversion counts) are worker-computed facts, and gating on mode means the work happens only when shown. Alternatives weighed + rejected: widening `Mode` (conflates transient/persistent state); App-side gating with the worker always emitting echo data (computes unconditionally, doesn't generalise, re-opens the render-side framing ruled against). Scope: channel + resolution rule only — the renderer/catalogue/`Value → SQL-literal` are ADR-0038, the `ALTER COLUMN` gap-fill is the ADR-0035 amendment
- [ADR-0038 — The DSL → SQL teaching echo](0038-dsl-to-sql-teaching-echo.md) — **Accepted** (design agreed 2026-05-27; **fully implemented + verified** — every catalogue row in §7 Buckets A + B and the §6 category-3 prose round-trips per line through the advanced walker per §1, and the §4 de-emphasised styled-runs polish is wired: handoff-46 `04c8e42` shipped the channel + create-table slice, handoff-47 `90479cb` the full Bucket A expansion + a skeleton contract-gap fix (dropped per-column `DEFAULT`/`CHECK`), `275c726` the Bucket B resolved-name + multi-statement renderers (auto- and user-named `add index`, positional `drop index`, `add`/`drop relationship` in both selector forms, `drop column --cascade`, `add relationship --create-fk`), `e6ad1ae` the last category-3 line — the `change column --dont-convert` *caveat* (shortid + transform notes were already surfaced via pre-existing `client_side.*` keys), and `2aab457` the §4 styled-runs polish: a new `OutputKind::TeachingEcho` custom rendering branch (dimmed `Executing SQL:` prefix + the SQL re-lexed in advanced mode for token-class colouring, same as the input echo) plus a new `OutputStyleClass::Hint` for every cat-3 prose line — caveat *and* the existing illuminating notes, user-confirmed broader scope), **realises ADR-0030 §10** (the teaching bridge) — the Phase-5 echo **ADR-0035 §12 forward-referenced** — building on **ADR-0037** (the `SubmissionMode` gate) and **ADR-0035 Amendment 2** (standard-first dialect + `ALTER COLUMN` gap-fill). When a **DSL-form** command runs in advanced/one-shot mode, the worker emits the equivalent SQL beneath `[ok]` as a de-emphasised styled `OutputLine` (ADR-0028); the App renders it. **Defining invariant — the copy-paste contract:** every echoed line is *runnable advanced-mode SQL* (round-trip-tested: parse the echo → same-effect command; a planned "copy the echo" affordance depends on it). **Type vocabulary = the playground's own keywords** (`serial`/`shortid`/…, accepted by `from_sql_name`, decision (a)); **statement shape = the standard-first dialect** (Am2). **DML uses substituted literals, not `?`** (per-type `Value → SQL-literal`, round-trip-safe; `blob` moot — no literal syntax exists; auto-gen columns omitted to match `do_insert` + X4). **Firing reality — a DDL + `show data` feature:** in advanced mode `insert`/`update`/`delete … where` are SQL-first (`Sql*` = already SQL = nothing to echo per §10); only DSL-*only* spellings echo (DDL + `show data` + the `delete`/`update … --all-rows` fall-throughs — the latter via **ADR-0033 Amendment 4**, a bug-fix folded in here that reverses Amendment 3's `update … --all-rows` misparse). **Three-category framework** for "what happens beyond the literal SQL": **(1) engine-implementation-hiding** (the rebuild, rowid PK, non-PK `serial` MAX+1) — *never surfaced*; **(2) decomposable into advanced SQL** (`drop column --cascade`, `--create-fk` relationship) — *shown as the runnable multi-line sequence, one statement per line*; **(3) playground type-behaviour with no SQL-expressible form** (`shortid` generation — no `shortid()`; type-conversion transforms — no `USING`) — *de-emphasised prose expansion from the worker's `client_side.*` notes*. Carries the **full catalogue** (Buckets A single-statement / B resolved-name + multi-line / C no-echo) mapping every DSL-form command to its echo. OOS: reverse SQL→DSL echo (§13 OOS-5), app commands / `show table` / `explain` / `replay`, a `blob` literal, the column-level UNIQUE/CHECK drop residual (Bucket C until Am2's gap closes), and surfacing any category-1 engine internal
- [ADR-0039 — EXPLAIN over advanced-mode SQL queries](0039-explain-over-advanced-sql.md) — **Accepted** (2026-05-27), **implemented 2026-05-30 (issue #7)**, **supersedes ADR-0030 §13 OOS-2**. Lets `explain` wrap the advanced SQL commands (`Select`/`SqlInsert`/`SqlUpdate`/`SqlDelete`, plus `with`/CTE which builds a `Select`) in addition to the DSL `ShowData`/`Update`/`Delete` it already covers (ADR-0028), running `EXPLAIN QUERY PLAN` over the validated SQL text through the existing ADR-0028 span-styled plan tree (advanced mode only; DSL `explain` unchanged in both modes). Implemented via a second `Advanced` `explain` CommandNode (`EXPLAIN_SQL`) registered under the shared `explain` entry word — reusing the established `insert`/`update`/`delete` shared-word dispatch (`decide`: SQL-first / DSL-fallback), so `explain show data …` and DSL-only `--all-rows` still reach the DSL node; rejected a `DynamicSubgrammar` mode-gate (its resolution cache key omits `mode`). `build_explain_sql` slices the inner SQL off the source (excludes `explain`) and reuses the existing SQL builders; `do_explain_plan` runs the carried text verbatim, no params. Advanced `explain update`/`delete` now route through SQL (identical plan, full SQL syntax); DSL-explain tests pinned to simple mode. Reframed OOS-2 as a *deferred* exclusion (per ADR-0000's out-of-scope discipline), not a rejection. OOS (deferred): EXPLAIN of DDL (no query plan exists)
- [ADR-0040 — A per-command completion marker (✓/✗) replaces the `[ok]` summary line](0040-completion-marker-replaces-ok-summary.md) — **Accepted 2026-05-30 (issue #9)**, amends ADR-0014 / ADR-0028 / ADR-0019 output conventions, builds on ADR-0037's mode-tagged echo. An audit of the whole command surface found the `[ok] <verb> <subject>` summary line duplicates the echo line above it (verb+subject) everywhere; its only unique contribution is the success-vs-error signal (and `explain select` even rendered `[ok] explain` with an empty subject post-ADR-0039). Decision: drop the `[ok]` line and the symmetric `"…" failed:` prefix; the echo line gains a trailing inline **✓** (green, success) / **✗** (red, failure) — `running:` becomes a pending state that resolves to `<input> ✓/✗` on completion (status set via the existing `rfind(Echo)` lookup). Content (row counts, structure, data, plan tree, teaching echo) unchanged. Scoped to the DSL/data/SQL family that has the redundant echo+`[ok]` pair; app-command `[ok]` lines (`rebuild`/`export`/`now editing`) are payload-bearing, have no echo to mark, and stay as-is. `ok.summary` retired; `dsl.failed` reduced to the rendered reason. Broad but mechanical snapshot churn. OOS: app-command `[ok]` lines, the `[WRN]` validity indicator, and the tag colours (issue #10)