Files
rdbms-playground/docs/adr/0027-input-validity-indicator.md
T
claude@clouddev1 032a050f7b docs: add ADR-0027 — input-field validity indicator
A debounced `[ERR]` / `[WRN]` marker at the right edge of
the input row, summarising — before submit — whether the
current command would run. Backed by a small
diagnostics-severity model: the walker emits severity-
tagged diagnostics (parse outcome, schema-existence of
table / column names) that the indicator summarises and
the existing highlighting / hint layers detail. Advisory
only — submission is never blocked.

- docs/adr/0027-input-validity-indicator.md — the ADR.
- docs/adr/README.md — index entry.
- docs/requirements.md — new S6 (TUI shell).
2026-05-18 20:46:06 +00:00

242 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0027: Input-field validity indicator
## Status
Accepted
## Context
While the user types, the app already flags problems in
two immediate ways (ADR-0022): per-token syntax
highlighting, and an ambient hint panel describing the slot
under the cursor. Both are *local* — they speak about the
spot being edited.
What is missing is a *global* signal: a single, glanceable
answer to "if I press Enter now, will this command run?" A
learner can type something whose error is highlighted ten
columns back, move the cursor to the end, and submit it
without noticing.
This ADR adds a **validity indicator** at the right edge of
the input row, and — because a meaningful indicator needs a
consistent notion of "what is wrong" — a small
**diagnostics-severity model** behind it.
The indicator is **advisory**. It never blocks submission:
that posture is settled (ADR-0022 — the app indicates, it
does not refuse; ADR-0026 §7 — even a flagged WHERE still
runs if submitted).
## Decision
### 1. Two severities
- **ERROR** — the input is *known* to fail. Either it does
not parse (incomplete, or a mismatched / invalid token),
or it parses but names something that does not exist (an
unknown table or column).
- **WARNING** — the input is valid and *will* run, but is
very likely not what a knowledgeable user wants: a
type-mismatched comparison, or `= NULL` (both from
ADR-0026 §7).
The split is *certainty of failure* versus *likely
misleading*. The indicator shows the highest severity
present; clean input shows nothing at all.
### 2. The diagnostics model
The walk produces a set of **diagnostics** alongside its
`WalkOutcome`. A diagnostic is, roughly,
`{ severity, span, message_key }` — the severity drives the
indicator, the span drives highlighting, the message key
drives the hint text (ADR-0019 catalog).
Diagnostics come from three sources, all reachable within
the single schema-aware walk:
- **The parse outcome.** `Incomplete`, `Mismatch`, and
`ValidationFailed` each yield an ERROR diagnostic; a
plain `Match` yields none.
- **Schema existence.** A matched `IdentSource::Tables` or
`IdentSource::Columns` token whose name is absent from
the schema cache yields an ERROR diagnostic. *This is new
behaviour.* Today `walk_ident` returns `Matched` for any
identifier-shaped token and consults the schema only to
populate context (`current_table`, `current_column`); an
unknown table parses cleanly and fails only at execution.
The check is cheap — the schema cache is already in
`WalkContext` — but it is genuinely new code, and it must
run for every Tables / Columns ident, not only the
`writes_*` ones.
- **Expression flagging (ADR-0026).** A type-mismatched
comparison and `= NULL` yield WARNING diagnostics.
The walker is the one schema-aware pass, so diagnostics are
emitted there, not in a separate re-resolution pass.
`WalkResult` gains a `diagnostics` field. The indicator
reads the highest severity across the outcome and the
diagnostics; highlighting and the hint panel read the
individual diagnostics for *where* and *why*. The indicator
is the summary; the existing layers remain the detail.
### 3. "Would Enter succeed now?" — and the debounce
The indicator answers exactly one question: *if you pressed
Enter right now, what would happen?* — runs clean (nothing
shown), runs but flagged (`[WRN]`), or will not run
(`[ERR]`).
An incomplete, still-being-typed command is in the `[ERR]`
bucket: pressing Enter on it fails. The indicator does not
special-case "still typing".
Flicker is prevented not by suppressing states but by a
**debounce**:
- On every keystroke the indicator is hidden immediately.
- After roughly one second with no typing, it reappears,
showing the current verdict.
So: typing → blank; a pause → the verdict. The user gets a
settled, glance-before-submit signal without a marker that
thrashes on every key. Highlighting and hints stay
immediate and unchanged — only the *indicator's display* is
debounced; diagnostics themselves are still computed every
keystroke to feed those immediate layers.
The indicator's display is therefore time-gated. Per the
`update()`-is-pure invariant, the debounce timer lives in
the runtime / event loop; `App` holds the indicator's
visible state, which the runtime sets when the quiet
interval elapses or a keystroke arrives. The interval is a
single tunable constant (~1 s).
### 4. Rendering
The indicator is a fixed-width five-column label —
`[ERR]` or `[WRN]` — or nothing when the input is clean. A
positive "all good" state is deliberately omitted: absence
*is* the all-clear.
It sits in the input row, at the right end. The rightmost
strip — five columns for the label plus one column of gap,
six in all — is **reserved unconditionally**: the text area
is always `width 6`, whether or not the indicator is
currently visible. The label appears and disappears within
its already-reserved strip, so the text / horizontal-scroll
boundary never moves and the label never collides with the
typed command.
A *conditionally* reserved strip was rejected: it would
make a long, horizontally-scrolled command jump up to six
columns sideways on every typing-pause transition.
Unconditional reservation costs ~6 columns of typing width
— negligible — for a stable layout.
Border-mounting the indicator (as chrome on the field's
frame, like the mode label) was also considered and not
chosen: a right-edge border decoration reads differently
from a top-border label, and the in-row position is the
intended look.
`[ERR]` uses the existing `theme.error`. `[WRN]` needs a
new **`warning`** theme colour — an orange / amber —
defined for both the light and dark themes (NFR-5: colour
conveys information; NFR-7: legible on either background).
### 5. The indicator never blocks submission
Enter always submits, whatever the indicator shows. A user
who wants to run a flagged command — or even a doomed one,
to see what the engine does — may; the error lands in the
output as usual. The indicator's job is to make "this will
not do what you think" visible *before* submission, not to
prevent it. This is the same advisory posture as ADR-0022
and ADR-0026 §7.
### 6. The existing-cases sweep, and discoverability
Implementation includes a **sweep** of the current command
surface for failures that are detectable before submission
but not yet surfaced as diagnostics — chiefly unknown table
and column references, across every command that takes an
identifier. Each becomes an ERROR diagnostic via §2. (The
WARNING set is thin until ADR-0026 lands; type mismatch and
`= NULL` are its first members.)
The diagnostics model is documented as *the* route for any
future "we can tell, before it runs, that this is wrong or
dubious" case. This ADR is cross-referenced from the
diagnostic-producing code and from related ADRs, so new
cases plug into the model rather than becoming one-off
checks.
### 7. Out of scope
- **Blocking submission** — never; the indicator is
advisory (§5).
- **A positive `[OK]` state** — clean input shows nothing.
- **Raw advanced-mode SQL** — there is no SQL parser yet
(`Q1`). The indicator covers simple-mode DSL and the
app-level commands the walker parses; when SQL parsing
lands, SQL diagnostics route through this same model.
- **Per-diagnostic display in the indicator itself** — the
indicator is a one-glyph summary; *where* and *why* stay
with highlighting and the hint panel.
## Consequences
- `WalkResult` gains a `diagnostics` field; the walker
emits diagnostics (parse-outcome and schema-existence) as
it walks.
- A new schema-existence check on `IdentSource::Tables` /
`Columns` matches. Small, but genuinely new — today an
unknown identifier parses cleanly and fails only at
execution.
- The event loop gains a debounce timer for the indicator's
display. It lives in the runtime, so `update()` stays
pure.
- A new `warning` theme colour; the input field reserves a
fixed six-column right strip.
- The indicator is advisory; submission is never gated.
- A reusable diagnostics-severity model that future
pre-submit checks — and, eventually, advanced-mode SQL —
extend.
- The WARNING severity has no triggers until ADR-0026 is
implemented. The indicator may ship ERROR-only first and
gain WARNING with C5a, or ship after C5a; the model
carries both severities regardless.
## Implementation notes
A sensible order, each step test-guarded:
1. The `Diagnostic` type and the `diagnostics` field on
`WalkResult`; map the parse outcome to diagnostics.
2. The schema-existence check in `walk_ident` for
`Tables` / `Columns` idents.
3. The `warning` theme colour; the fixed six-column input
strip; the `[ERR]` / `[WRN]` rendering.
4. The debounce in the runtime, and the indicator's
visible state on `App`.
5. The sweep — confirm every identifier-taking command
surfaces unknown-name diagnostics; cross-reference this
ADR from the diagnostic sites.
ADR-0026's expression diagnostics (type mismatch, `= NULL`)
land with that ADR's implementation and feed the WARNING
severity.
## See also
- ADR-0003 — input modes; the input field and its mode
label.
- ADR-0019 — the friendly-message catalog the diagnostic
message keys resolve through.
- ADR-0022 — ambient typing assistance: the immediate
highlighting and hint layers this indicator summarises.
- ADR-0026 — complex WHERE expressions; the type-mismatch
and `= NULL` WARNING diagnostics.