# RDBMS Playground — Requirements (Phase 1)

This document is the **consolidated Phase 1 requirements
checklist** for RDBMS Playground. It captures everything the
project has committed to so far, derived from the design
conversation and the ADRs in `docs/adr/`.

**Purpose.** Phase 5 verification at every milestone measures
delivered work against this checklist. An item not on this list
was not promised; an item silently dropped without confirmation
is a process failure.

**Scope.** The list is intentionally coarse — each item is a
unit of "satisfied / not satisfied" judgement. When an item is
taken up for implementation, it is decomposed further in a
backlog (initially in this repo, now tracked as Gitea issues).

## Status legend

- `[ ]` — open, not yet started
- `[/]` — partial / in progress: some of it is built and tested,
  but named gaps remain. The entry states what works and what is
  still missing. (Distinct from `[ ]`, which is genuinely
  untouched, and from `[~]`, which is *deliberately* deferred
  pending an ADR rather than half-built.)
- `[x]` — satisfied (implemented + tested)
- `[~]` — deferred, awaiting an ADR or further design before any
  implementation
- `[-]` — explicitly out of scope (rationale at the bottom)

> **Reconciliation note (2026-06-07).** A full audit of every
> `[ ]` item against the source found ~46 % of them mis-marked —
> overwhelmingly *under*-claimed (e.g. tab completion `I3` and
> syntax highlighting `I4` were shipped but marked "not yet
> implemented"). The binary legend was the root cause: a
> shipped feature, a half-built one, and an untouched one all
> wore the same `[ ]`. The `[/]` marker above was added to fix
> this, and the audited items were re-marked. When you implement
> against an item, move it `[ ]` → `[/]` → `[x]` rather than
> jumping straight to `[x]`, and keep the gap note current.

## Test baseline

After ADR-0022 Amendment 6 (the curated SQL function-name list —
issues #15 tab-completion + #16 typing-time typo hint):
**1538 lib unit tests passing, 0 failing, 1 ignored**
(`cargo test --lib`; the full `cargo test` across every binary is
2107 passing, 0 failing, 1 ignored — the one ignored is a
long-standing `` ```ignore `` doc-test in `src/friendly/mod.rs`).
Clippy clean with the nursery lint group enabled. (Earlier
reference points — lib counts: 1131 after the ADR-0027 highlight /
hint follow-up + the optional-trailing-flag / `--resume`
manual-testing fixes; 1100 after ADR-0027's initial ship; 1079
after ADR-0026 (complex WHERE expressions); 1039 after ADR-0025
(indexes); 1006 after ADR-0024 + the handoff-14 cleanup; 449
after B2/C2. Note the intervening issue fixes #8/#13/#12/#7/#9
landed tests without a baseline bump; this is the first refresh
since ADR-0027.)

---

## Distribution and install

- [ ] **D1** Cross-platform binaries: Linux, macOS, Windows on
  x86_64 and aarch64.
- [ ] **D2** Single static binary, no runtime dependencies.
- [ ] **D3** Released via prebuilt binaries plus Homebrew, Scoop,
  `winget`, and `cargo binstall`.

## TUI shell

- [x] **S1** Three-region layout: items list (left), output
  panel (right), input field (bottom).
  *(Verified 2026-06-07: `ui.rs:26-58` lays out a horizontal
  split — items panel left, right column subdivided into output
  panel / input field / hint panel; rendered every frame.
  **ADR-0046 evolves this:** the left items region becomes
  width-optional — hidden by default at ≤ 90 columns, peek-revealed
  via `Ctrl-O` navigation mode — so the three-region layout is the
  wide-terminal default, not an invariant.)*
- [x] **S2** Items list shows tables and per-table indexes;
  designed to extend to additional element kinds (relations,
  views, etc.) without restructuring.
  *(ADR-0025: the items panel renders a nested list — each
  table with its index names indented beneath it. The nested
  model is the extension point for future element kinds.
  **ADR-0046 overrides the nesting approach for relationships:**
  because relationships are cross-table rather than per-table, they
  get their own sibling panel stacked below the tables list, not
  nested items within it — user-confirmed 2026-06-10.)*
- [/] **S3** Output panel renders a visualization of the
  currently selected item and supports multiple tabs.
  *(Partial, verified 2026-06-07: single-element structure
  visualisation renders (`output_render.rs:82-180`); **multiple
  tabs are not implemented** — the output is one line buffer, no
  tab abstraction. Same multi-tab gap as V2.)*
- [x] **S4** Hint area below the input field, showing hints about
  the current input or last error.
  *(Verified 2026-06-07: `ui.rs:1088-1110` `render_hint_panel` /
  `resolve_hint_lines` — a dynamic 1–`MAX_HINT_ROWS` panel below
  the input showing ambient hints, candidates, or the last error.
  **Correction (2026-06-10, ADR-0046):** the original wording said
  the area was "keyboard-toggleable"; that was never implemented and
  is deliberately dropped — the panel became indispensable once
  completion moved into it (ADR-0022), so it is always on. ADR-0046
  replaces its content-driven height with a geometry-driven one to
  stop the resize jump (#20); no toggle is added.)*
- [x] **S5** Mode label and distinct border style on the input
  field communicate the current input mode at all times.
  *(Verified 2026-06-07: `ui.rs:896-934` `render_input_panel` —
  a coloured, bold mode label plus a mode-distinct border colour
  (`border` vs `border_advanced`), tracking the three-way
  `EffectiveMode` incl. the one-shot `:` state.)*
- [x] **S6** 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 walker diagnostics-severity model
  (ERROR / WARNING). Advisory only — never blocks submission.
  *(ADR-0027: `Severity` / `Diagnostic` on `WalkResult`;
  `input_verdict` combines the parse outcome, schema-existence
  ERRORs — unknown table / column — and the ADR-0026 §7
  expression WARNINGs — type mismatch, `= NULL`. The runtime
  debounces the indicator's display ~1 s; the rightmost six
  columns of the input row are reserved unconditionally. New
  `warning` theme colour. A follow-up pass completed §2's
  highlight + hint wiring — diagnostics overlaid on the input
  field and surfaced in the hint panel, with precise
  per-literal WARNING spans — and Amendment 1 adds a
  `LIKE`-on-numeric-column WARNING.)*

## Input field

- [ ] **I1** Multi-line entry that auto-expands; Ctrl-Enter (or
  equivalent) submits, plain Enter inserts a newline.
- [x] **I1a** In-line cursor editing in the input field: Left /
  Right arrows move the cursor by character (UTF-8 boundaries
  honoured), Home / End jump to the extremes, Delete removes the
  character at the cursor, Backspace removes the character
  before. Insertion happens at the cursor position.
  *(Verified 2026-06-07: `app.rs:973-1005` `cursor_left` /
  `cursor_right` (with `is_char_boundary` checks), Home/End,
  Delete, Backspace, `insert_at_cursor`. This is single-line
  cursor editing and is complete on its own terms; the separate
  **multi-line** entry goal is tracked under I1, which is
  genuinely not started.)*
- [ ] **I1b** Readline-style cursor shortcuts: Ctrl-A / Ctrl-E
  as aliases for Home / End for users on keyboards without those
  keys (and for ergonomics in command-driven workflows). Likely
  followed by Ctrl-W (delete previous word), Ctrl-K (delete to
  end), Ctrl-U (delete to start). Pending.
- [x] **I2** Persistent navigable input history (project-scoped).
  *(Implemented across Iterations 2 + 6: per-command append to
  `history.log` (Iter 2); on project open, the in-memory
  navigable history is hydrated from the tail of
  `history.log` up to the same in-memory cap (Iter 6). Global
  rolling history is out of scope per OOS-6 / N4.)*
- [x] **I3** Tab completion for app commands, DSL keywords, table
  names, column names, and SQL keywords.
  *(Verified 2026-06-07 — this was mis-marked `[ ]` despite being
  shipped: `src/completion.rs` is 2852 lines with ~100 tests;
  `app.rs:898` binds Tab → `completion_tab_forward` (and
  BackTab → `_backward`) with forward/backward cycling through a
  candidate memo and a colour-coded candidate line in the hint
  panel (`ui.rs:1125` `render_candidate_line`). All five
  candidate categories work — app commands (via REGISTRY), DSL
  keywords (walker `Expectation`), table names + column names
  (`SchemaCache`), and SQL keywords/functions in advanced mode.
  Refinement 2026-05-30, issue #15: SQL expression slots
  (`sql_expr_ident`) also offer a curated set of SQL function
  names — `KNOWN_SQL_FUNCTIONS` in `src/dsl/sql_functions.rs`,
  surfaced as `CandidateKind::Function` (ADR-0022 Amendment 6).
  The original "broad tab-completion" goal is met; any further
  polish is incremental, not a missing core.)*
- [x] **I4** Syntax highlighting for both the DSL and SQL.
  *(Verified 2026-06-07 — mis-marked `[ ]` despite being shipped:
  `input_render.rs:64-113` lexes the input to styled byte-range
  runs (`lex_to_runs_in_mode`) and renders them per-mode (DSL in
  simple, SQL in advanced), with nine token classes in `theme.rs`
  (`tok_keyword`, `tok_identifier`, `tok_string`, `tok_punct`,
  `tok_flag`, `tok_error`, `tok_function`, `tok_type`) and
  diagnostics overlaid (error/warning spans). Both surfaces are
  highlighted; the core goal is met.
  Refinement 2026-05-29, issue #8: column data types now carry a
  dedicated `HighlightClass::Type` / `tok_type` colour, distinct from
  identifiers and clause keywords — ADR-0022 Amendment 4; a further
  refinement 2026-05-30, issue #15: SQL function-name candidates carry
  a dedicated `tok_function` colour (the ninth `Theme` token colour,
  ADR-0022 Amendment 6). The broad highlighting goal stays open.)*
- [ ] **I5** In-flight query/command cancellation (Ctrl-C in the
  output area or input field).

## Input modes (per ADR-0003)

- [x] **M1** Simple mode is the default. It accepts DSL data
  commands and the canonical app-level commands; raw SQL is
  rejected with a friendly hint.
- [x] **M2** Advanced mode accepts SQL plus the canonical
  app-level commands without any sigil.
- [x] **M3** Prefixing a single line with `:` in simple mode is a
  one-shot advanced escape (with the prompt label updated). The
  `mode simple` / `mode advanced` command switches modes
  persistently.
- [x] **M4** Execution-time mode side-channel — **implemented** via
  ADR-0037 (the channel) and its motivating consumer ADR-0038 (the
  DSL → SQL teaching echo). Every command knows, at execution time,
  which of three modes it ran under — the three-way
  `EffectiveMode` { `Simple`, `AdvancedPersistent`,
  `AdvancedOneShot` } resolves at submit time and threads through
  `Action::ExecuteDsl` → runtime; the persistent `Mode` enum stays
  two-way (the transient one-shot `:` lives on the channel where it
  belongs, not in persistent state). The runtime gates the
  ADR-0030 §10 teaching echo on it: a DSL-form command run in
  advanced/one-shot mode renders the equivalent advanced-mode SQL
  beneath the command's echo line (the `[ok]` summary it once sat
  under was retired by ADR-0040, issue #9 — the echo line now carries
  a ✓/✗ completion marker instead); simple-mode and SQL-entered
  submissions stay silent. Echo coverage: **ADR-0038 is feature-complete** — every
  catalogue row in §7 round-trips per line through the advanced
  walker (the §1 copy-paste contract; §6 category 2 holds it per
  line), every §6 category-3 line surfaces, and the §4
  de-emphasised styled-runs rendering polish (ADR-0028) is wired.
  Shipped across four feature commits: **Phase 1** Bucket A —
  single-statement DDL + `show data` + `--all-rows` fall-throughs
  (`90479cb`); **Phase 2** Bucket B — resolved-name + multi-line
  echoes (`add index` auto- and user-named, positional
  `drop index`, `add`/`drop relationship` in both selector forms,
  `drop column --cascade`, `add relationship --create-fk`)
  (`275c726`); **Phase 3** category-3 prose — `shortid` generation
  and type-conversion transforms via the pre-existing
  `client_side.auto_fill_*` / `client_side.transformed*` notes, plus
  the new `change column --dont-convert` caveat (`e6ad1ae`); **Phase
  4 styled-runs polish** — `OutputKind::TeachingEcho` custom
  rendering branch (dimmed `Executing SQL:` prefix + the SQL re-lexed
  via `input_render::lex_to_runs_in_mode(Advanced)` for token
  highlighting, same as the input echo), `OutputStyleClass::Hint`
  for every cat-3 prose line (caveat + the existing illuminating
  notes — broader scope, visually consistent) (`2aab457`).

## App-level commands (per ADR-0003)

- [/] **A1** All canonical app-level commands implemented and
  available in both modes: `save`, `save as`, `load`, `new`,
  `rebuild`, `export`, `import`, `seed`, `replay`, `undo`,
  `redo`, `mode`, `help`, `hint`, `quit`.
  *(Partial, verified 2026-06-07: 13 of 15 implemented and
  available in both modes — `quit`/`q`, `mode simple|advanced`,
  `help`, `save`, `save as`, `load`, `new`, `rebuild`, `export`,
  `import`, `replay`, `undo`, `redo` (REGISTRY in
  `grammar/app.rs:249-333`). **Missing: `seed`** (tracked as SD1)
  **and `hint`** (tracked as H2) — neither is registered. A1
  closes when SD1 + H2 land.)*

## DSL data commands

- [x] **C1** Table operations: create / drop / rename.
  *(Verified 2026-06-07: create + drop done; **rename done on the
  advanced surface** — `ALTER TABLE … RENAME TO`, ADR-0035 §6 / 4h
  (`do_rename_table`, `db.rs:4821`). A simple-mode rename-table
  verb is **deliberately** not provided — table rename is
  advanced-mode only — so the requirement is satisfied as
  designed, not partial.)*
- [x] **C2** Column operations: add / drop / rename / change
  type. `drop column` and `rename column` use SQLite native
  ALTER TABLE (3.35+ / 3.25+); `change column` routes through
  the rebuild-table primitive since ALTER doesn't support
  type changes. PK and relationship-involved columns are
  refused with friendly messages (drop the relationship
  first); SQLite STRICT enforces type compatibility on the
  data copy during a type change.
- [x] **C3** Schema constraints: primary key (single and
  compound), foreign key with `ON DELETE` / `ON UPDATE` referential
  actions, indexes, `NOT NULL`, `UNIQUE`, `CHECK`, `DEFAULT`.
  *(PK including compound done at create-table time;
  FK with `ON DELETE` / `ON UPDATE` actions done (ADR-0013) —
  declared via `add 1:n relationship`; symmetric outbound +
  inbound view in the structure renderer; type compatibility
  validated at declaration via `Type::fk_target_type()`.
  Indexes done (ADR-0025) — `add index` / `drop index`,
  rebuild-preserving, persisted in `project.yaml`; UNIQUE indexes
  added on the advanced-mode SQL surface (`CREATE UNIQUE INDEX`,
  ADR-0035 §4d / ADR-0025 Amendment 1; simple-mode `add unique index`
  deferred).
  `NOT NULL` / `UNIQUE` / `CHECK` / `DEFAULT` done (ADR-0029) —
  a constraint suffix on `create table` / `add column`, plus
  `add constraint` / `drop constraint` on existing columns;
  populated-column additions are guarded by a pre-flight
  dry-run that refuses with a table of offending rows.)*
- [~] **C3a** Modify relationship: `modify relationship <name>
  [on delete <action>] [on update <action>]`. Users can achieve
  the same via drop + add today; one-step modify is a small
  follow-up using the existing rebuild-table machinery. ADR
  pending.
- [x] **C4** Convenience: `create m:n relationship from <T1> to
  <T2>` produces an auto-named junction table the user can rename;
  pulls primary keys and FK definitions automatically.
  *(Done 2026-06-10 via **ADR-0045**. `create m:n relationship from
  <T1> to <T2> [as <name>]` builds a junction table with one FK column
  per parent PK column (`{table}_{pkcol}`, typed via `fk_target_type`),
  a **compound PK** over them, and two **`CASCADE`** 1:n relationships
  — all in one `do_create_table` call = one undo step. Auto-named
  `{T1}_{T2}` (optional `as`), available in both modes, compound-parent
  PKs supported (ADR-0043). Self-referential m:n refused; PK-less parent
  refused. Wired across every surface — completion (`m:n` composite),
  hints, highlighting, `help`/usage, and the advanced-mode DSL→SQL
  teaching echo (the generated `CREATE TABLE … FOREIGN KEY …`). 9
  integration + 7 typing-surface + echo/parse unit tests. The build
  surfaced — and fixed — two latent simple-mode dispatch/completion
  assumptions ("≤1 DSL form per entry word"), now generalized
  behaviour-preservingly.)*
- [x] **C5** Data operations: insert / update / delete via DSL.
  *(ADR-0014. INSERT short and long forms, UPDATE/DELETE with
  required WHERE plus `--all-rows` opt-in, `show data <T>`,
  per-column-type value-literal validation, FK enforcement
  with metadata-driven error enrichment, auto-show after
  writes. Bulk insert, complex WHERE expressions, and SELECT
  in advanced mode are explicitly tracked separately — see
  C5a below.)*
- [x] **C5a** Complex WHERE expressions (AND/OR, comparison
  operators, LIKE, IS NULL, IN, BETWEEN) for UPDATE/DELETE/
  show-data filtering; `show data` also gains `where` and
  `limit`.
  *(ADR-0026 steps 1–4: the stratified expression grammar
  reached through a new `Subgrammar` node, the recursive
  `Expr` AST + `build_expr`, wiring into update / delete /
  show data, and `Expr` → parameterised SQL with an implicit
  primary-key `ORDER BY` for `limit`. Type-mismatched WHERE
  comparisons are permissive — they run rather than being
  rejected (§7). The §7 advisory **flagging** of type
  mismatches / `= NULL` is the seam with ADR-0027's
  diagnostics-severity model and is tracked there — see
  ADR-0026 "As-built notes".)*

## SQL handling

- [x] **Q1** SQL parsed via `sqlparser-rs`; supported subset is
  defined (specifics deferred to a future ADR).
  *(Progress: the advanced-mode SQL surface is authored as grammar
  within the unified grammar tree (ADR-0030 / ADR-0024) and parsed by
  the existing walker — **not** a separate batch parser — so SQL gets
  the same completion / highlighting / hints as the DSL (ADR-0001's
  `sqlparser-rs` reservation is superseded). Implemented so far: full
  `SELECT` (ADR-0032), `INSERT` / `UPDATE` / `DELETE` (ADR-0033), and
  `CREATE TABLE` (ADR-0035, 2026-05-25 — executed structurally: columns
  + types + `NOT NULL`/`UNIQUE`/`PRIMARY KEY` + `IF NOT EXISTS` (4a),
  then per-column `DEFAULT`/`CHECK` (raw `sql_expr` text) and composite
  `UNIQUE(a,b)` (4a.2), then table-level/multi-column `CHECK` (4a.3 —
  round-trips via the new `__rdbms_playground_table_checks` metadata
  table, since the engine reports no CHECK constraints), then foreign
  keys (4b — inline `REFERENCES` + table-level `FOREIGN KEY` → ADR-0013
  named relationships in the create transaction; self-references and
  bare `REFERENCES <parent>` supported), then `DROP TABLE [IF EXISTS]`
  (4c — reuses `do_drop_table`; `IF EXISTS` is a no-op-with-note), then
  `CREATE [UNIQUE] INDEX` / `DROP INDEX [IF EXISTS]` (4d — reuse
  `do_add_index`/`do_drop_index`; `CREATE UNIQUE INDEX` admitted in
  advanced mode via the `IndexSchema.unique` flag, ADR-0025 Amendment 1),
  then `ALTER TABLE` add/drop/rename column (4e — `alter` is advanced-only,
  runtime-decomposed to the existing column executors; ADD COLUMN reaches
  CREATE-TABLE constraint parity; drop/rename refuse a table-CHECK-
  referenced column), then `ALTER TABLE … ALTER COLUMN TYPE` (4f —
  runtime-decomposed to `change_column_type` with `ForceConversion`, the
  §7 advanced policy: lossy converts with a note, incompatible + static
  refusals (`↔ blob`, non-`int → serial`) refuse, `int → serial` allowed;
  the internal-`__rdbms_*` guard folded into `do_change_column_type`),
  then `ALTER TABLE` add/drop constraint + add FK (4g — `ADD [CONSTRAINT
  <name>] (CHECK | UNIQUE | FOREIGN KEY)` + `DROP CONSTRAINT <name>`;
  ADD = CHECK + composite UNIQUE + FK (PRIMARY KEY + named UNIQUE
  refused); table-CHECK/UNIQUE rebuild with a dry-run guard, FK reuses
  `add_relationship`; named table-CHECKs round-trip via a rebuild-only
  `name` column on `__rdbms_playground_table_checks` + a `project.yaml`
  `check_constraints` `{expr, name}` extension; the internal-table guard
  completed across `do_add_constraint`/`do_add_relationship`)).
  then `ALTER TABLE … RENAME TO` (4h — the one genuinely new low-level
  op, `do_rename_table`: native rename + one-transaction reconciliation of
  the CSV file and every metadata row naming the table, incl. **rewriting
  CHECK text that qualifies a column with the old table name** so a fresh
  rebuild round-trips; refuses same-name / existing-target / `__rdbms_*` /
  non-existent; auto-named indexes + relationships kept stale per §6
  scope; one undo step), then the **4i verification sweep** (shared-entry-
  word completion merge + simple/advanced completion colour; `describe` of
  table-level constraints; self-ref FK pre-submit indicator; CREATE-TABLE
  help/usage refresh). **ADR-0035 Phase 4 (4a–4i) is complete.**)*
- [x] **Q2** Non-standard syntax rejected with a clear message
  pointing at the supported subset.
  *(Design done — ADR-0030 §8: out-of-subset statements are
  refused with an engine-neutral message naming the construct.
  Implementation pending.)*
- [x] **Q3** User-facing simplified types map transparently to
  SQLite STRICT types in generated DDL. *(All ten types implemented
  and tested.)*
- [x] **Q4** Supported SQL subset specification — **ADR-0030**.
  Advanced mode is a standard-SQL surface, engine-neutral; the
  supported surface — `SELECT` (full query surface),
  `INSERT` / `UPDATE` / `DELETE`, `CREATE` / `DROP` / `ALTER
  TABLE`, `CREATE` / `DROP INDEX` — is authored as grammar in
  the unified tree. DDL routes through the typed `Command`
  executor (metadata + the playground type vocabulary
  preserved); DML and `SELECT` execute as validated SQL. Q1's
  implementation is now unblocked.

## Database backend (per ADR-0002)

- [x] **B1** SQLite via `rusqlite`; all tables created `STRICT`;
  `PRAGMA foreign_keys = ON` per connection. *(Database accessed
  through a dedicated worker thread per ADR-0010.)*
- [x] **B2** Schema evolution uses the rebuild-table technique
  internally where SQLite `ALTER TABLE` cannot — currently
  the change-column-type code path. Add-column, drop-column,
  and rename-column take the simpler ALTER TABLE route since
  modern SQLite supports them natively; metadata sync into
  `__rdbms_playground_columns` and
  `__rdbms_playground_relationships` happens in the same
  transaction either way.
- [ ] **B3** Query timeout and cancellation supported (no
  cartesian-join-of-doom can hang the app).
  *(Progress: the worker-thread architecture is in place; the
  cancellation/timeout protocol on top of it is pending.)*

## Type system (per ADR-0005)

- [x] **T1** All ten user-facing types implemented: `text`,
  `int`, `real`, `decimal`, `bool`, `date`, `datetime`, `blob`,
  `serial`, `shortid`. *(Mapping to SQLite STRICT covered by
  ADR-0005; FK target type rule by ADR-0011.)*
- [x] **T2** `shortid` generation: base58, 10–12 characters,
  omits ambiguous characters; generated client-side at insert.
  *(Implemented per ADR-0014; auto-fills omitted shortid
  columns and validates user-supplied values against the same
  alphabet and length range.)*
- [x] **T3** Compound primary keys handled end-to-end (DSL,
  storage, display, FK reference).
  *(Done 2026-06-09 via **ADR-0043**: the FK-reference leg now
  works on both surfaces — DSL `add 1:n relationship from
  P.(a, b) to C.(x, y)` and SQL `FOREIGN KEY (a, b) REFERENCES
  P(x, y)` (bare `REFERENCES P` auto-expands to the full PK).
  References the parent's full compound PK matched positionally,
  per-pair type-compat (ADR-0011); the FK is engine-enforced,
  persisted (`columns: [a, b]` in `project.yaml`, comma-joined in
  metadata), shown symmetrically by `describe`, and `--create-fk`
  creates one child column per parent PK column. The relationship
  model went list-based through all six layers, single-column
  behaviour preserved (commit `b14f019`); 12 integration tests in
  `tests/it/compound_fk.rs` plus the existing single-column suite
  as the regression net. The earlier-noted (2026-06-07) breakdown:*
  *compound-PK **declaration** (`with pk a(int),b(int)`),
  **storage** (`primary_key: Vec<String>`), and **display** were
  already present and tested. The FK-reference leg — once an
  ADR-first ~15–20-site change across the relationship model — is
  what ADR-0043 delivered (back-compat dropped by user decision, so
  no migration was needed). Subset / non-PK (UNIQUE-target) FK
  references stay out of scope.)*

## Visualizations

- [x] **V1** Single-element views render in the output pane: a
  selected table as its structure (columns, types, keys,
  constraints); a selected relationship as two tables joined by
  a line.
  *(Done 2026-06-10 — **ADR-0044**. The table-structure half shipped
  earlier; the **relationship-as-line-art** half (ADR-0016 OOS-1) now
  renders as **Style-A two-table connector diagrams** wherever a
  relationship is the subject: `show relationship <name>` (full
  structure boxes), `show table <T>` and `add`/`drop relationship`
  echoes (focal box + compact stacked diagrams). Child-left /
  parent-right, `n…1` cardinality, referential actions, bold title
  rows; rendered App-side and **width-aware** (side-by-side ↔ vertical
  fallback). **Compound** FKs route a shared bus + an explicit
  `(a, b) ▶ P.(x, y)` pairing line; **self-referential** FKs draw two
  same-named boxes. Incidental DDL echoes keep the prose form (the
  "relationship-relevant" reach). The §3 last-resort helper line was
  considered and rejected. Two `/runda` passes (design + implementation).
  Selection-nav and the broader journal direction remain in V4.)*
- [/] **V2** SQL query results render as a dynamic table view in
  the output pane, with multiple result tabs supported.
  *(Partial, verified 2026-06-07: the **table view** is done —
  `output_render.rs:38-72` `render_data_table` renders a
  box-drawing frame with aligned columns (numeric right, text
  left) and NULL/control-char sanitisation, for `show data` and
  after every write (ADR-0014). **Missing: multiple result tabs**
  — the output is a single `VecDeque<OutputLine>` with no tab
  abstraction (same gap as S3). Multi-tab sits in V4 territory.)*
- [~] **V3** Full ER-diagram export (whole-database graph, viewed
  outside the TUI) — low priority; design and ADR pending.
- [~] **V4** Output panel as a *scrollable per-session log* with
  inline rich rendering. Direction agreed in conversation: the
  output area is a chronological journal of operations and
  selections (e.g. a "selected table X" entry with the rendered
  structure underneath); structure renderings choose between a
  compact ASCII-table form and a vertical line-per-column form
  based on dimensions; the log is exportable to Markdown so
  learners can keep a record of their session. Design and ADR
  pending before any implementation.
  *(Partial: PageUp / PageDown scrolling of the existing line
  buffer is in, with new output snapping the view to the most
  recent. The full V4 scope — smart structure rendering, log
  styling, Markdown export, scroll indicator — remains pending.)*
- [x] **V5** `show <kind> [<name>]` family of commands for
  redisplaying schema info on demand.
  *(Done 2026-06-07: `show table <name>` + `show data <Table>`
  (single-item) plus the list-all family `show tables` /
  `show relationships` / `show indexes` — the latter three landed
  as `Command::ShowList { kind }` (one variant, `grammar/data.rs`),
  a read-only worker `show_list` formatting count-headed lists from
  the same helpers the items panel uses, with help + parse-usage
  entries and 10 integration tests (`tests/it/show_list.rs`). The
  one remaining member of the `[<name>]` clause — singular
  per-item detail for relationships/indexes — is split out as
  **V5a** below so it has a tracked home rather than living as a
  footnote here.)*
- [x] **V5a** Singular per-item detail views `show relationship
  <name>` / `show index <name>` — the `[<name>]` half of V5 for
  the relationship and index kinds (the table kind already has
  `show table <name>`).
  *(Done 2026-06-07: folded into `Command::ShowList { kind, name:
  Option<String> }` — `name: Some(_)` is the singular form. Two
  grammar branches (`relationship <name>` / `index <name>`,
  reusing the `Relationships`/`Indexes` completion sources for the
  name slot), a worker `do_show_one` rendering a labelled detail
  block (endpoints + ON DELETE/UPDATE for a relationship; table,
  columns, uniqueness for an index) or a friendly "No relationship/
  index named `X`." line, reusing the V5 `ShowList` render path.
  Help + parse-usage entries + two ADR-0042 near-miss matrix rows;
  5 added integration tests. V5's `[<name>]` clause is now
  complete across all three kinds.)*
- [x] **V6** Copy the output panel to the system clipboard
  (issue #11, ADR-0041). `copy` / `copy all` copy the whole
  panel; `copy last` copies the most recent command's output.
  Delivery is OSC 52 (SSH-friendly, no native dep) *plus* a
  best-effort native write (`arboard`), always both; the payload
  is the panel's plain text exactly as rendered. Removes the
  terminal-select-and-fight-wrapping friction of filing bug
  reports. (Complements V4's planned Markdown export — a
  different "get the session out" path.)

## Project lifecycle (per ADR-0004)

- [x] **P1** Auto-named temp project on startup under
  `<data-root>/projects/`. OS-standard data root via
  `directories` crate; `--data-dir` overrides (Iteration 1).
- [x] **P2** `save` / `save as` elevate / copy + switch
  (Iteration 4b). `save` on a named project reports
  "already auto-saved".
- [x] **P3** Auto-save: per-command write-through to YAML +
  CSV + `history.log` inside the SQLite tx with
  commit-db-last ordering (Iteration 2). No dirty state.
- [x] **P4** `load` opens an in-TUI picker, sorted newest
  first, with `[TEMP]` markers and a `b`-to-browse path-entry
  sub-mode (Iteration 4b).
- [x] **P5** Existence-only load + explicit `rebuild`
  command with confirmation modal (Iterations 3 + 4a).
- [x] **P-NAME-1** Temp project directory naming pattern:
  `<YYYYMMDD>-[temp]-<word>-<word>-<word>` from a 161-word
  built-in list (Iterations 1 + 4b). Bracketed `[temp]`
  marker is unambiguous against user-named projects because
  `validate_user_name` rejects brackets.
- [x] **P-NAME-2** Display-name prettifier strips
  `YYYYMMDD-` AND `[temp]-`; splits kebab / snake / camel;
  title-cases each word.
- [x] **P-NAME-3** Status bar shows
  `Project: [TEMP] <name>` for temp projects,
  `Project: <name>` for named.
- [x] **P-CLEAN-1** Unmodified empty temp projects are
  auto-deleted on switch and quit, gated by
  `safely_delete_temp_project`'s stacked guards
  (containment, symlink rejection, `[temp]` marker, contents
  allowlist).

## Project file format (per ADR-0004)

- [x] **F1** `project.yaml` with `version: 1` field carries
  schema (ordered tables + columns), relationships, and
  `created_at`. `data/<table>.csv` carries table data (UTF-8,
  header row, RFC 4180; NULL distinct from empty string)
  (Iteration 2). Empty tables produce no CSV.
- [x] **F2** `.gitignore` template (`/playground.db`,
  `/.rdbms-playground.lock`, `/project.yaml.v*.bak`) created
  in each new project (Iteration 1). Per ADR-0007 amendment
  1, `history.log` is NOT in the template — user decides
  whether to commit it.
- [x] **F3** Migration framework scaffold (Iteration 6).
  `MigratorRegistry` + `migrate_to_latest` +
  `ensure_project_yaml_migrated` are wired into every project
  open; no migrators registered in v1 (the production
  registry is empty). The framework is exercised by tests
  that inject a fake v1→v2 migrator: registry plumbing,
  `.v<N>.bak` backup, version-bump sanity check, and
  newer-than-supported / malformed-version errors are all
  covered. The first real migrator (when v2 ships) is a
  one-file change.

## Undo and replay (per ADR-0006)

- [x] **U1** Auto-snapshot before **every** data/schema mutation
  (DSL + SQL) into a persisted ring buffer (size N=50, tunable),
  per ADR-0006 Amendment 1 (single-step undo, superseding the
  original destructive-only model). Snapshot is a hybrid
  whole-project copy (database via online backup API + `project.yaml`
  / `data/*.csv` as files); staged before the mutation's transaction,
  finalised after the db commit (preserves ADR-0015 §6). A batch
  command (`replay` / future batch ops) records one boundary
  snapshot; `import` takes none. A `--no-undo` CLI flag disables
  snapshotting. *(Implemented 2026-05-24, plan
  `docs/plans/20260524-adr-0006-undo-snapshots.md`: `src/undo.rs`
  ring + worker dispatch hook in `src/db.rs`; snapshots gated on a
  user command `source` so internal ops like open-time rebuild are
  not recorded; snapshot-bookkeeping failures are non-fatal, restore
  failures surface.)*
- [x] **U2** `undo` restores the most recent snapshot (database +
  text, directly); `redo` re-applies (redo stack discarded on new
  work); both prompt for confirmation naming the command being
  undone / re-applied (`Y` confirms). *(Implemented 2026-05-24:
  `undo` / `redo` app commands, `Modal::UndoConfirm`, runtime
  prepare→confirm→restore→refresh; `--no-undo` reports undo is off,
  empty stacks report "nothing to undo/redo". UX polish 2026-05-29,
  issue #13: the confirm dialog grows to fit its summary on one row,
  capitalises `Snapshot` / `Yes` / `No`, and renders the snapshot
  timestamp in local time, human-formatted (`24 May 2026, 11:00`) via
  the new `chrono` dependency.)*
- [x] **U3** `history.log` records every submitted command in
  append-only form, tagged with its outcome (Iteration 2;
  broadened by ADR-0034). Format: `<ISO-8601 Z>|<status>|<source>`
  per ADR-0015 §5 / ADR-0034 §1 — `status` is `ok` for a
  successful command and `err` for one that failed to parse or
  execute. Hydration (cross-session recall) reads all records;
  replay reads `ok` only.
- [x] **U4** `replay` runs commands from a `history.log` or
  `.commands` file. *(Implemented via ADR-0024 Phase E:
  `runtime::run_replay` parses each non-blank, non-`#`-comment
  line in advanced mode and dispatches it through the normal
  pipeline; stops at the first genuine error, no rollback.
  ADR-0034 §3: replay reads journal records (`<ts>|<status>|
  <source>`), running `ok` records and skipping non-`ok`, while
  still accepting bare-command scripts. ADR-0034 Amendment 1:
  replay re-applies only schema/data write commands and **skips**
  every app-lifecycle command + nested `replay` — all skips
  continue (a nested `replay` is now skipped, not refused), with a
  `[skip]` warning on `import` / nested-`replay`. Covered by
  `tests/replay_command.rs`.)*

## Sharing and export (per ADR-0007)

- [x] **E1** `export` produces a zip excluding `playground.db`
  AND `history.log` (per ADR-0007 amendment 1); default
  filename `YYYYMMDD-<projectname>-export-NN.zip` with a
  non-clobbering two-digit sequence under the active data root
  (Iteration 5). The zip preserves the project's directory
  name as a single top-level folder. `import <zip> [as <t>]`
  is the inverse: derive target name from the zip's top
  folder, auto-suffix `-NN` on collision (ADR-0015 §11
  amendment), rebuild from text on open.
- [ ] **E2** User documentation includes sharing recipes for
  git, email, and direct file transfer.

## Sample data / seeding

- [ ] **SD1** `seed <table> [count]` generates plausible fake
  data; junction tables are seeded with valid foreign-key
  references drawn from existing parent rows.
- [~] **SD2** Detailed seeding rules (per-type generators,
  locale, determinism, override hooks) — design and ADR pending.

## Query analysis

- [x] **QA1** `EXPLAIN QUERY PLAN` is run on demand for queries;
  output is rendered as an annotated tree highlighting full
  scans, index use, and join order.
  *(Implemented per ADR-0028: the `explain` prefix over
  `show data` / `update` / `delete`, with a span-styled plan
  tree. `EXPLAIN QUERY PLAN` never executes, so explaining a
  destructive `update` / `delete` is safe. Extended 2026-05-30,
  issue #7 / ADR-0039: `explain` now also wraps advanced-mode
  SQL — `select` / `with` / `insert` / `update` / `delete` — via
  a second `Advanced` `explain` CommandNode on the shared entry
  word, reusing the same plan-tree renderer.)*
- [x] **QA2** Plan rendering specifics — tree layout, annotation
  taxonomy, colour scheme. Implemented per ADR-0028 (§3–§6):
  a box-drawing tree, the substring-pattern taxonomy, and the
  `OutputLine` styled-runs mechanism.

## Hints, help, errors

- [x] **H1** Friendly error-rewriting layer translates engine
  error messages into learner-friendly equivalents (ADR-0019).
  *(Done: the `friendly::translate_error` chokepoint is wired on
  the live failure path (runtime + app + `DbError::friendly_message`)
  and covers all five ADR-0019 §3 categories — UNIQUE, FOREIGN KEY
  (parent- and child-side), NOT NULL, CHECK, and type-mismatch —
  with operation×kind×verbosity catalog wording, the
  `messages short|verbose` verbosity command, and §6 row-pinpointing
  via runtime-resolved facts rendered through the bordered
  diagnostic table. Covered by 44 `friendly` unit tests + 12
  full-stack `friendly_enrichment` integration tests. Remaining
  ADR-0019 scope is deferred and separately tracked: the §9 i18n
  migration sweep of all other user-facing strings, advanced-mode
  SQL-error sanitization (§OOS-2), and `messages` persistence
  (§OOS-3, awaits the settings ADR).)*
- [x] **H1a** Strong syntax-help in parse errors. When the user
  types something near-correct (e.g. `insert into T ('Oli')` —
  forgotten `values`; or `update T set x=1` — missing WHERE),
  the error should *name the missing keyword or clause* rather
  than just point at the unexpected character. This is a
  separate effort from H1 (which targets database errors); it
  targets parser errors. *(Done via the **ADR-0042** systematic
  pass, 2026-06-06.)* Built piecemeal first (e.g. `values` becoming optional in
  INSERT removes one such case; ADR-0024's typed value slots
  give per-column-type rejection wording; `insert into T (col)`
  with no `values` clause now flags "looks like Form A — add
  `values (...)`"; **issue #1 / ADR-0033 Amendment 5, 2026-05-28**
  added two pedagogical lines that teach the INSERT Form B
  positional-`VALUES` contract — a simple-mode submit-time
  teaching note covering under-supply, over-supply, and extra
  values (`insert.form_b_extra_values_note`) and an
  advanced-mode dispatch-time pre-flight note for the same
  value-count class (`insert.form_b_positional_count_mismatch_note`),
  plus a walker-level `diagnostic.insert_arity_mismatch_form_b`
  ERROR that lights the `[ERR]` validity indicator at typing
  time; **issue #2 / ADR-0022 Amendment 3, 2026-05-29** made the
  ambient-hint fallback rung schema-aware so the "Next:" prose
  names the schema-correct next token (`,` between values, `)`
  after the last) instead of the type-blind close-paren, and so a
  wrong-arity closed tuple surfaces the real parse error rather
  than a misleading "submit with Enter"; **issue #17 / ADR-0036
  Amendment 2, 2026-05-29** then brought the §8.1 arity diagnostic
  to **simple mode** at parity with advanced — a wrong-count DSL
  insert (Form A/B/C) now fires the friendly *"N value(s) for
  `col`…"* message at typing time, counted against the user-fillable
  columns, with `serial`/`shortid` auto-fill named; new keys
  `diagnostic.insert_arity_mismatch_form_b_simple` /
  `diagnostic.insert_arity_mismatch_all_auto`). The **ADR-0042
  systematic pass** then closed it: a per-command near-miss matrix
  (`tests/it/parse_error_pedagogy.rs`) locks every entry word's
  bare / missing-clause / wrong-token cases plus the committed
  multi-forms, in both modes; friendlier labels landed (`add` →
  `1:n relationship`; bare `select` → "a projection: …"); the
  usage block became mode-aware (advanced shows the SQL forms
  plus the still-valid DSL fallback forms, SQL-first); `with`
  got its own CTE template; and `cross join … on` now teaches
  that a CROSS JOIN takes no ON clause. The advanced-SQL
  diagnostics that the survey thought missing (INSERT…SELECT
  count, RETURNING column scope) were verified already present.
  One low-priority residual is deferred by decision: at *submit*
  time a non-projection expression position (bare `where `,
  `returning `) still shows the raw expression first-set —
  typing-time completion already offers the right candidates
  there, so the payoff is small.
- [ ] **H2** `hint` provides contextual help for the current
  input or the most recent error.
- [x] **H3** `help` provides general reference and per-command
  help.
  *(Done 2026-06-07: the **general reference** is `help` (no arg) —
  intro + the full command list (REGISTRY × `help_id`, so new
  commands appear automatically) + the type reference + a footer
  pointing at the focused form. **Per-command help** is `help
  <command>` (H3's new piece): the HELP node took an optional
  single-word topic (`BarePath`), `AppCommand::Help { topic }`,
  and `note_help_topic` renders the block(s) of every command
  sharing that entry word — so `help create` covers both create
  forms — plus `help types` for the type reference and a friendly
  "no help for `X`" pointer for an unknown topic. Help/usage
  strings catalogued + key-registered; 9 integration tests
  (`tests/it/help_command.rs`). A richer *narrative* overview
  (modes, the `:` escape, syntax conventions) is reference-docs
  scope, tracked under **DOC1** — not part of H3.)*

## CLI

- [x] **L1** Load a project via a positional CLI argument
  (Iteration 1). Plus `--data-dir` to override the data root
  and `--help` / `-h` for the usage banner.
- [x] **L1a** `--resume` CLI flag opens the most recently used
  project (path tracked in `<data-root>/last_project`).
  Iteration 6: errors cleanly with a stderr banner above the
  shell prompt if no previous project is recorded or the
  recorded path is gone — no silent fallback; mutually
  exclusive with a positional path argument (ADR-0015 §7).
  `last_project` is rewritten on every successful project
  open (startup, load, new, save as, import).
- [x] **L1b** Per-project input-mode restore (issue #14,
  ADR-0015 Amendment 1). The input mode is stored in
  `project.yaml` (`project.mode:`), restored on every open, and
  persisted on a `mode` change and on unload (quit / project
  switch) — so the mode you leave a project in is what reopens.
  A teacher can ship a project that opens in advanced mode; a
  learner's last-used mode is restored per project. New
  `--mode simple|advanced` CLI flag (precedence `--mode` >
  stored > `simple`; combines with `--resume`). Independent of
  the `history.log` input-history hydration piece of Iteration 6.
- [~] **L2** Submit a command alongside project load — deferred,
  not v1.

## Tutorials and lessons

- [~] **TU1** Tutorial / lesson system — design and ADR pending
  before any implementation. Out of v1 unless an ADR is written.

## Documentation

- [/] **DOC1** User- and student-facing reference
  documentation under `docs/`: the DSL command surface,
  the type system, and the boundaries of simple mode.
  `docs/simple-mode-limitations.md` is the first piece —
  it doubles as student explanation and as detailed
  reference. Distinct from in-app `help` (`H3`), the
  interactive tutorial system (`TU1`), and the sharing
  recipes under `E2`.
  *(Partial, verified 2026-06-07: `docs/simple-mode-limitations.md`
  exists (~55 lines, covers the WHERE-expression and
  table-creation boundaries). **Missing:** a DSL
  command-surface reference and a standalone type-system
  reference under `docs/`.)*

## Testing (per ADR-0008)

- [x] **TT1** Tier 1: `cargo test` + `proptest` covering
  pure-logic modules (parser, dispatcher, type mapping, project
  I/O, snapshot ring buffer, replay log).
- [x] **TT2** Tier 2: Ratatui `TestBackend` + `insta` snapshots
  for representative views.
- [x] **TT3** Tier 3: synthetic event-loop integration tests
  covering the user-facing flows in this checklist.
- [~] **TT4** Tier 4: PTY-based end-to-end for the four critical
  flows named in ADR-0008 (cold launch → DDL → quit; save →
  reopen; export → import → rebuild; undo after DROP).
  *(Verified 2026-06-07: **nothing is wired** — no
  `portable-pty` / `expectrl` / `vt100` dependencies, no PTY test
  files; ADR-0008 §Tier-4 is a specification only. The Tier-3
  `tests/it/*_e2e.rs` files are synthetic event-loop tests, not
  PTY. Correcting a stale `CLAUDE.md` line that read "Tier 4 is
  wired only for the listed critical flows" — it was not wired at
  all. Genuinely deferred.)*
- [ ] **TT5** CI runs all tiers on Linux, macOS, and Windows on
  stable Rust.

## Cross-cutting

- [x] **X1** Comprehensive logging via the project's logging
  infrastructure per `CLAUDE.md` (decision points, parameter
  values, fallback paths).
  *(Done 2026-06-10 via a full-sweep instrumentation pass. The
  prior state (verified 2026-06-07) was a wired harness
  (`src/logging.rs`) but sparse instrumentation — failure-path
  heavy, nothing in `db.rs`/parser/executors. The sweep brought
  every layer to the "log liberally" bar under a documented level
  discipline (see the `logging.rs` module doc): **`db.rs`** gained
  entry-level `debug!` on all 34 `do_*` executors plus decision-point
  logs (rebuild-table primitive, insert auto-fill, delete cascade,
  FK resolution) — so the route through delegating executors is
  visible in the log sequence; **persistence** logs every
  yaml/CSV/history write (the silent-failure paths); **runtime**
  logs `execute_command_typed` dispatch; **`app.rs`** logs
  submit / app-command dispatch / render-mode choice; the **parser**
  logs parse begin/outcome at `trace` (it is a per-keystroke hot
  path). Levels: `debug` for per-command detail (off by default,
  `RDBMS_PLAYGROUND_LOG=debug`), `info` for lifecycle, `warn` for
  fallbacks, `trace` for hot paths. Emission verified end-to-end
  through the real worker thread + `logging::init`. ~75 → ~135 sites.)*
- [~] **X2** Language: English-only for v1; multi-language is an
  open question to revisit later.
- [~] **X3** Accessibility: TUI screen-reader support is
  best-effort and not a v1 commitment; revisit if user need
  emerges.
- [x] **X4** Auto-fill semantics differ between simple and advanced
  mode — **resolved 2026-05-27** (raised 2026-05-26). Was: simple-mode
  `do_insert` auto-fills an omitted **non-PK `serial`** column with
  `MAX(col)+1`, but the advanced-mode SQL insert auto-filled **only
  `shortid`**, leaving an omitted non-PK serial **silently NULL** —
  violating ADR-0018 §1's "auto-generated on every path" contract (the
  column is `INTEGER UNIQUE`, not `NOT NULL`, so SQLite permits the
  NULL). Confirmed by characterization, escalated, and fixed (decision:
  advanced mode matches simple mode): the advanced-mode auto-fill
  reconstruction (`db.rs`, renamed `plan_shortid_autofill` →
  **`plan_autogen_autofill`**) now also fills an omitted non-PK serial
  with `MAX+1` per row (single- and multi-row), mirroring `do_insert`
  and the existing shortid fill. PK serial is excluded (rowid alias);
  Form B (no column list) still supplies every column. Covered by
  `tests/sql_insert.rs::sql_insert_autofills_omitted_nonpk_serial`.
  Honours ADR-0018 §1/§5; no ADR amendment needed (the contract already
  said "every path"). ADR-0036 was correct that it did not touch this.
- [~] **X5** Framework cohesion / restructuring — **strategic,
  revisit later** (raised 2026-05-26). The grammar/execution
  framework (lexer → walker `Node`s → unified grammar tree → typed
  `Command`s → executors; ADR-0023/0024 and everything layered on
  since) grew organically from the original grammar outline and now
  reuses + mixes elements across many levels without a *cohesive
  written specification* of what the framework comprises, which
  elements are meant to be reusable, and where the boundaries sit for
  the recurring "reuse vs create new" decision. A concrete symptom:
  commands are coupled tightly enough to their execution that reusing
  an *execution behaviour* tempts reusing the whole *command* (see the
  ADR-0036 discussion — the temptation to emit `Command::Insert` from
  the advanced path just to reuse `do_insert`). Desired end state
  (user-stated): **unique commands for every unique case, with a clean,
  documented structure for reusable mechanics** (so execution helpers
  are shared as library functions, not by collapsing command
  identity). Consider a dedicated specification + restructuring run
  (its own ADR) to map the framework and set the reuse-boundary rules,
  easing future maintenance and extension. Not scheduled.

---

## Non-functional requirements

NFRs are quality bars rather than discrete features. Where a
target is measurable, it is stated numerically; where it is
necessarily qualitative, the criterion is named and the bar is
"reviewer judgement against the criterion."

- [ ] **NFR-1 Performance — startup.** Cold launch to first
  rendered frame under 500ms on commodity hardware (developer
  laptop, mid-range desktop). Measured in CI on the Linux runner
  as a regression gate.
- [ ] **NFR-2 Performance — input latency.** Keystroke-to-render
  latency under 16ms during normal editing; long-running queries
  must execute off the UI thread so the interface remains
  responsive (typing, scrolling, mode switching) while a query is
  running.
- [ ] **NFR-3 Performance — resource footprint.** Idle memory
  under 50MB on the smallest target platform; no busy-loops; CPU
  near zero when waiting for input.
- [ ] **NFR-4 Visual quality — distinctive design.** Colour
  palette and typography are deliberate and consistent across
  views; layout uses Unicode box-drawing and symbols where they
  add clarity; rendering avoids the generic flat-default look
  that ships with most TUI frameworks. Criterion: a reviewer can
  identify the app from a screenshot of any view.
- [ ] **NFR-5 Visual quality — colour use.** Colour conveys
  information rather than decoration: mode indication, query
  result types (numeric vs text vs null), error severity,
  syntax highlighting categories. Foreground/background
  combinations meet WCAG-AA contrast (4.5:1 for normal text)
  even though we have not committed to broader accessibility.
- [ ] **NFR-6 Cross-platform parity.** Behaviour and visual
  quality are equivalent across Linux, macOS, and Windows on
  crossterm-supported terminals. Platform-specific divergence
  (e.g. font fallbacks) is documented, not silently tolerated.
- [ ] **NFR-7 Light and dark background support.** The colour
  scheme remains legible and visually coherent on both light and
  dark terminal backgrounds. The mechanism (auto-detect via
  terminal query, explicit user setting, or both) is an
  implementation choice, but the outcome is non-negotiable: no
  dark-on-dark or light-on-light readability failures on either
  background.

---

## Explicitly out of scope

- [-] **N1** Hosted publishing platform — per ADR-0007. Sharing
  is local-artifact based.
- [-] **N2** Real UUID column type — per ADR-0005. The `shortid`
  type covers the pedagogical need at TUI-friendly width.
- [-] **N3** Cross-emulator visual regression coverage — per
  ADR-0008. Crossterm abstracts terminals adequately; we revisit
  only if a real regression surfaces.
- [~] **N4** Global rolling input history (cross-session,
  cross-project). Mentioned in I2's wording; deferred per
  ADR-0015 §12 — project-scoped history (via `history.log`) is
  the v1 surface. Revisit if real demand emerges.

---

## Maintenance

This document is updated whenever:

- A new requirement is committed to (added as a new item with the
  next free ID in its section).
- A deferred item is taken up (status moves from `[~]` to `[ ]`).
- An item is satisfied (status moves to `[x]`, with a reference
  to the commit, PR, or test that demonstrates it).
- An item moves out of scope (status moves to `[-]` with a
  rationale and a link to the decision).

IDs are stable: once assigned, they are not reused. Removing a
requirement leaves a "withdrawn" entry referencing the decision.
