# 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.