Files
rdbms-playground/docs/adr/README.md
T
claude@clouddev1 f0afec3812 docs: session handoff 64 + ADR-0047 implemented (#22/#24)
Flip ADR-0047 Status -> implemented (commits f879d54..2d0f4b2, phased
A->B->C + flat-rectangle restyle); update the README index entry to
match (implemented, flat black-on-yellow rectangles, final 2290-green
tally, website-cast follow-up noted). Add session handoff 64 covering
#24 (vi load-picker nav) and #22 (ADR-0047 demo overlay layer).
2026-06-11 09:59:51 +00:00

72 KiB
Raw Blame History

Architecture Decision Records

This directory contains the project's ADRs, recorded per ADR-0000.

Index

  • ADR-0000 — Record architecture decisions
  • ADR-0001 — Language and TUI frameworkAmendment 1 (2026-06-09): after the GitHub→Gitea migration (git.lazyeval.net), the prebuilt-binary distribution channel named in the Decision ("GitHub releases") is reopened as an undecided choice, to be settled by a future distribution ADR; package-manager channels unaffected
  • ADR-0002 — Database engine
  • ADR-0003 — Input modes and command dispatch — the persistent Simple/Advanced mode and the : one-shot escape. The startup mode is no longer always simple: it is restored from the project's stored mode and overridable with --mode (see ADR-0015 Amendment 1, issue #14). The app-command registry gains copy (ADR-0041, issue #11)
  • ADR-0004 — Project file format
  • ADR-0005 — Column type vocabulary
  • ADR-0006 — Undo snapshots and replay logAccepted. The replay/journal half (U3/U4) shipped via ADR-0034; the undo/snapshot half (U1/U2) is settled by Amendment 1 (2026-05-24) and implemented 2026-05-24 (plan: docs/plans/20260524-adr-0006-undo-snapshots.md; ring in src/undo.rs, worker hook in src/db.rs). Amendment 1 supersedes the original "snapshots only before destructive operations" model: a snapshot is taken before every data/schema mutation (DSL + SQL) for familiar single-step (Ctrl-Z) undo — so the confirmation collapses to naming the one command being undone (no db-diff). Snapshot is a hybrid whole-project copy — database via the online backup API plus project.yaml/data/*.csv as files — reconciling this ADR with ADR-0015's "text is authoritative, db is derived"; undo restores all three directly. Staged before the mutation's transaction, finalised after the db commit (preserves ADR-0015 §6 commit-db-last); rolled-back ops leave no snapshot. Persisted ring under .snapshots/, N = 50 (raised from 10), git-ignored + export-excluded + temp-cleanup-aware. redo supported, redo stack discarded on new work. Batch ops record one undo step (replay + future batch via a Begin/EndBatch worker primitive); import is outside undo (it switches projects per ADR-0015 §11, leaving the current project untouched). A --no-undo CLI flag disables snapshotting (hardware escape hatch). Adds the backup feature to rusqlite
  • ADR-0007 — Sharing and export
  • ADR-0008 — Testing approach
  • ADR-0009 — DSL command syntax conventions
  • ADR-0010 — Database access via a dedicated worker thread
  • ADR-0011 — Foreign-key column type compatibility
  • ADR-0012 — Internal metadata for user-facing column types
  • ADR-0013 — Relationships, naming, and the rebuild-table strategy
  • ADR-0014 — Data operations, value literals, and the auto-show pattern
  • ADR-0015 — Project storage runtimeAmendment 1 (2026-05-31, issue #14): the input mode is per-project state in project.yaml (a new optional mode: key under project:, alongside created_at), restored on every open and persisted as it changes — so a teacher can ship a project that opens in advanced mode, and a learner's last-used mode is restored per project. Mode is live UI state, not schema (rebuild ignores it) and not stored in the database: the persistence handle carries the current mode and the worker stamps it into project.yaml on every write, so a later command rewrites the live value rather than clobbering it (no preserve-the-old-value dance needed). Backward-compatible optional field (pre-#14 files default to simple, no migration). New CLI flag --mode simple|advanced, precedence --mode > stored > simple; combines with --resume. Mid-session mode changes persist via Action::PersistModeDatabase::set_mode (immediate, crash-safe), and the mode is persisted on unload (quit + project switch) so the mode you leave a project in is always restored — deterministic, not selectively dependent on whether you ran a DDL (rejected as confusing) nor rewriting project.yaml on every read command (rejected for load-picker mtime churn). Switches save the outgoing project's mode, then restore the incoming project's stored mode via the ProjectSwitched event
  • ADR-0016 — Pretty table rendering for data and structure views
  • ADR-0017 — Column type-change compatibility
  • ADR-0018 — Auto-fill contracts for serial and shortid columns
  • ADR-0019 — Friendly error layer (H1) and i18n message catalog
  • ADR-0020 — Tokenization layer for the DSL parserSuperseded by ADR-0024 (never implemented). Specified a chumsky-over-tokens architecture (separate lexer, define_keywords!, &[Token] grammar). ADR-0024 adopted a scannerless hand-rolled walker and removed chumsky entirely; the lexer/keyword/token model here does not exist. Kept as institutional memory of the path not taken.
  • ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)Mechanism superseded by ADR-0024; H1a scope continued in ADR-0042. The intent (show the command's grammar at the point of error) shipped — usage_ids on each CommandNode, the parse.usage.* templates, and the available_commands fallback all exist — but via grammar nodes, not the chumsky UsageEntry registry / parse.token.* keys described here (which were never built).
  • ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)Amendment 1 supersedes §12's simple-mode-only carve-out: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. ambient_hint_in_mode + hint_resolution_at_input_in_mode + expected_for_hint_snapshot thread Mode; render_hint_panel calls ambient for all modes (no more advanced-mode None); the one-shot : sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; Amendment 2 reverses the handoff-14 keywords-first candidate ordering — schema identifiers (table/column/relationship names) now sort before keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the tok_identifier/tok_keyword colour split marks the boundary); shipped with a walk_repeated fix that surfaces a list item's trailing optionals at a clean boundary (order by Name asc/desc, select Name as, create table … Code(text) not/unique/default/check; the , separator deliberately not surfaced); records a deferred two-line hint box for growing lists; Amendment 3 makes the ambient-hint fallback rung schema-aware — Amendment 1's bottom-rung parse_command_in_mode was schemaless while every earlier rung was not, so between-values insert hints pointed at ) (type-blind close) instead of , and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses parse_command_with_schema_in_mode, no extra walk, with the friendly arity diagnostic still winning at its higher rung; Amendment 4 gives column types a dedicated highlight class — both Node::Ident.highlight_override and the Word.highlight_override field were dead (driver destructured the former to _, walk_word hardcoded Keyword); now both wired through, with a new HighlightClass::Type + eighth Theme field tok_type (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three IdentSource::Types slots opt in via Some(Type) (advanced-mode single-word SQL aliases — float, varchar, … per ADR-0035 §3 — ride along for free), and the two-word double precision alias opts in via the new Word::type_keyword constructor so it matches its synonyms; Amendment 5 lets the hint panel grow for long prose hints — a fixed one-row panel clipped long field-value/usage hints past the first line (issue #12); resolve_hint_lines now pre-wraps prose and render_right_column sizes the panel to the line count (1 row default, up to MAX_HINT_ROWS=3, reclaimed when short) with a clamp_wrapped ellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-char parse.usage.sql_create_table synopsis to a terse one-liner (full grammar remains in help.ddl.sql_create_table); Amendment 6 adds a curated SQL function-name list (src/dsl/sql_functions.rs, KNOWN_SQL_FUNCTIONS — aggregates + common + broader scalars; cast deliberately excluded as its CAST(x AS type) syntax isn't a plain-call shape) as the single source of truth shared by two consumers at the sql_expr_ident slot (ADR-0031 §1): issue #15 offers the functions as Tab candidates under a new CandidateKind::Function + ninth Theme colour tok_function (a blue distinct from keyword/identifier/type, parallel to Amendment 4's tok_type) so a learner discovers sum/upper/…; issue #16 restores the typing-time column-typo flag the issue-#6 fix had dropped wholesale at this slot — invalid_ident_at_cursor now bails only when the partial prefix-matches a known function, else falls through to the schema-column check, so select Agx warns again at typing time while select sum does not (the issue-#6 lockdown tests + the submit-time unknown_column diagnostic path are untouched, and the no-validation-allowlist posture stands); see ADR-0031's status note for the grammar-side anchor
  • ADR-0023 — Unified declarative grammar tree — direction (superseded for execution detail by ADR-0024)
  • ADR-0024 — Unified grammar tree: execution planAccepted, the executable spec — implemented (Phases AF; Phase F shipped "minimal", parser.rs retained as the router — see the ADR's Phase F implementation note)
  • ADR-0025 — IndexesAccepted (Amendment 1, 2026-05-25: UNIQUE indexes admitted on the advanced-mode surface via CREATE UNIQUE INDEX — ADR-0035 §4d; the IndexSchema.unique flag round-trips through project.yaml with no new metadata table since the engine reports uniqueness natively; simple-mode add unique index stays deferred), add index / drop index, persistence, rebuild-table preservation, and items-list display (C3 index portion + S2)
  • ADR-0026 — Complex WHERE expressionsAccepted, stratified recursive expression grammar (AND/OR/NOT, comparisons, LIKE, IS NULL, IN, BETWEEN) for update / delete / show data filters; show data gains where + limit; adds the Subgrammar node and a recursive Expr AST (C5a)
  • ADR-0027 — Input-field validity indicatorAccepted, a debounced [ERR] / [WRN] marker at the input row's right edge, backed by a walker diagnostics-severity model (parse-outcome + schema-existence); advisory, never blocks submission (S6); Amendment 1 adds a LIKE-on-numeric-column WARNING
  • ADR-0028 — Query plans (EXPLAIN QUERY PLAN)Accepted, an explain prefix command over show data / update / delete; an annotated, span-styled plan tree; introduces the OutputLine styled-runs mechanism (ADR-0016's deferred per-span styling) (QA1 / QA2)
  • ADR-0029 — Column constraints (NOT NULL / UNIQUE / CHECK / DEFAULT)Accepted, the four column-level constraints declared in the column-spec suffix (create table / add column) and modified on existing columns via add constraint … / drop constraint …; a pre-flight dry-run guards populated columns; CHECK reuses the ADR-0026 expression grammar via Subgrammar (C3)
  • ADR-0030 — Advanced mode: the standard-SQL surfaceAccepted, SQL added as grammar within the unified grammar tree (ADR-0024), not a separate batch parser — so SQL gets the same completion / highlighting / hints / parse-errors as the DSL; mode gates the SQL forms; DDL routes through the typed Command executor (metadata + type vocabulary preserved), DML and SELECT execute as validated SQL; engine-neutral posture, the DSL→SQL teaching echo; supersedes ADR-0001's sqlparser-rs reservation; phased plan (Q1 / Q2 / Q4); §13 OOS-2 (EXPLAIN of advanced SQL) superseded by ADR-0039
  • ADR-0031 — The SQL expression grammarAccepted, the stratified SQL expression grammar fragment commissioned by ADR-0030 §3: a single precedence ladder (OR/AND/NOT, the comparison/LIKE/IN/BETWEEN/IS NULL predicate set, arithmetic incl. ||, function calls, CASE) — the superset of ADR-0026's DSL WHERE grammar, authored as a parallel fragment so simple mode is untouched; pure validation, builds no AST (consumers run/store SQL as text per ADR-0030 §4/§6); reuses ADR-0026's Subgrammar recursion + depth cap unchanged; subquery expressions and qualified column refs deferred to ADR-0030 Phase 2; status note (2026-05-30) records that ADR-0022 Amendment 6 layers a curated known-function list on the sql_expr_ident slot (§1) for completion + the typing-time typo hint (issues #15/#16) — the grammar itself is unchanged, and the no-validation-allowlist posture stands
  • ADR-0032 — The full SQL SELECT grammarAccepted, the Phase-2 grammar commissioned by ADR-0030 §3: full SELECT with INNER/LEFT/RIGHT/FULL OUTER/CROSS joins, GROUP BY/HAVING, all four set ops (UNION/UNION ALL/INTERSECT/EXCEPT), WITH and WITH RECURSIVE CTEs, LIMIT … OFFSET, DISTINCT, t.*, and bare-alias projection (lifting Phase-1 §4.2); additive extensions to ADR-0031's sql_expr for scalar subqueries, IN (SELECT …), [NOT] EXISTS, and qualified column refs (redeeming ADR-0031 §7 OOS-1/OOS-2); grammar-recursion via Subgrammar(&SQL_SELECT_COMPOUND) reuses ADR-0026's MAX_SUBGRAMMAR_DEPTH = 64 cap unchanged; softens ADR-0030 §8's "ambient assistance comes for free" claim: completion scope needs new WalkContext accumulators (a from_scope_stack of ScopeFrames holding from_scope / cte_bindings / projection_aliases), a new walker node variant Node::ScopedSubgrammar(&Node) as the push/pop trigger (existing Node::Subgrammar unchanged so DSL Expr and sql_expr recursion are unaffected), qualified-prefix completion narrowing, body-projection-derived CTE column resolution (so SELECT * and explicit-projection CTE bodies both yield real column completion past cte_alias.|), and a post-walk fixup pass that re-resolves projection-list identifier highlighting/validity once FROM is parsed (the projection-before-FROM problem); classifies every Phase-2 validation case against ADR-0027's ERROR/WARNING guideline (§11): five new diagnostic.* keys for parse-time-detectable cases (unknown qualifier, ambiguous column, projection-alias misplaced, CTE/compound arity mismatch) plus eight engine.* translation keys; a MatchedPath-walking predicate-warnings variant that closes the Phase-1 gap where SQL WHERE expressions emitted no LIKE-on-numeric / = NULL / type-mismatch warnings (ADR-0027 Amendment 1 finally extends to the SQL surface); adds a worker-side post-prepare type-resolution pass via engine column-origin metadata so bare column refs recover their playground type (partially lifting Phase-1 §4.5, the bool→0/1 case) — Cargo.toml gains column_metadata to rusqlite features (verified against pinned 0.39.0); __rdbms_* rejection extended to every new table-source slot; Amendment 1 narrows §12's resolution rule from a grammar-side structural classification to "trust the engine's column-origin metadata verbatim" after an empirical probe showed origin metadata follows through non-recursive CTEs, scalar subqueries, derived tables, set ops, and joins — the one structural exception is recursive CTE result columns, which return None and stay typeless; Amendment 2 records that §10.6's "rewrite the highlight class" prescription is realised via the two-pass schema-existence diagnostic + the renderer's diagnostic-overlay path (no separate per-byte rewrite step needed; no new HighlightClass variant), and that the projection-before-FROM completion narrowing has been improved by an src/completion.rs look-ahead probe when the leading walk's from_scope is empty but the full input parses
  • ADR-0033 — The full SQL DML grammar (INSERT / UPDATE / DELETE)Accepted (implemented + verified through sub-phase 3k, 2026-05-23; phase-exit report docs/handoff/20260523-phase-3-verification.md), the Phase-3 grammar commissioned by ADR-0030 §3: single- and multi-row INSERT (incl. INSERT … SELECT recursing through ADR-0032's SQL_SELECT_COMPOUND), UPDATE with SET assignment list, DELETE, all three optionally followed by RETURNING projection_list, plus full ON CONFLICT … DO NOTHING / DO UPDATE UPSERT on INSERT; fixes the DSL-vs-SQL dispatch architecture for shared entry words (insert/update/delete): SQL-first / DSL-fallback in Advanced mode via a Choice(SQL_shape, DSL_shape) per shape, gated by a new walker capability Node::Guard(fn) — a zero-byte-consumption gating node that fails the enclosing Seq with a ValidationError; carries Command::SqlInsert / SqlUpdate / SqlDelete variants and do_sql_* worker handlers each of which knows the target table (for re-persistence) and the returning: bool flag (for DataResult routing); shortid auto-fill mirrors the DSL do_insert mechanism via worker post-fill; SQL DELETE produces the same per-relationship cascade summary the DSL DELETE does (ADR-0014 parity); three new walker diagnostics (insert_arity_mismatch ERROR, auto_column_overridden WARNING, not_null_missing WARNING) with positive + negative tests each; OOS list explicitly carves out DEFAULT VALUES (the project's planned seed feature), SQLite-specific OR REPLACE / OR IGNORE / OR ABORT / OR FAIL / OR ROLLBACK prefixes, UPDATE FROM multi-table updates, and WITH-prefixed DML; the excluded keyword inside ON CONFLICT DO UPDATE is a deliberate carve-out from ADR-0030 §7's engine-neutral posture (no standard-SQL UPSERT spelling exists that SQLite and PostgreSQL share); eleven phased sub-phases each with explicit exit gates + written DA gate, opening with the dispatch mechanism before any DML grammar lands; initial DA review recorded seven critiques that were resolved before status moved to Proposed; Amendment 1 supersedes §2's dispatch mechanism: the originally-chosen Node::Guard(fn) + Choice(SQL_shape, DSL_shape) was found during 3a to be unworkable as framed (any guard-in-Choice mechanism forces a walk_choice change — walk_choice only falls through on NoMatch, so Simple-mode valid-DSL would wrongly surface "this is SQL", and walk_seq treats a NoMatch past idx 0 as a hard Failed, breaking Advanced-mode DSL fall-through); replaced by category-grouped, mode-aware dispatch in walker::walk (each REGISTRY entry tagged CommandCategory::{Simple, Advanced}, generalising the existing whole-command is_advanced_only gate), shared entry words carrying a node in both groups, no Node::Guard and no walk_choice/walk_seq change, advanced-mode completion SQL-first with DSL as a full-line fallback; Amendment 2 (sub-phase 3f) supersedes §7's cascade mechanism: the WHERE-injected per-child pre-count rested on a premise that was factually wrong about the DSL handler (which detects cascades by before/after row-count diffing inside a transaction, not by Expr-derived pre-count subqueries) and would have broken the §2 parity promise by reporting SET NULL the DSL path doesn't; replaced by mirroring do_delete's count-diff exactly (verbatim DELETE executes, child-count diff observes the cascade — ON DELETE CASCADE row removals only, SET NULL deferred for both paths to preserve parity), which shares the render-layer formatter for free via CommandOutcome::Delete and withdraws risk R2 (no WHERE-byte extraction, no N+1 subquery); Amendment 3 (sub-phase 3j) records the command-identity model and defers the execution-mode side-channel: a command is the typed outcome of a mode-rooted grammar path and its identity is intrinsic (Advanced mode tries SQL first, falls back to the Simple DSL command when no SQL branch matches a token, e.g. delete … --all-rows; note update … --all-rows does not fall back — the SQL SET expression eats --all-rows, harmless since the engine treats it as a comment); Simple mode commits the DSL candidate for shared words so the real DSL error surfaces, and when that line would also run in advanced mode the rendering layer combines them — DSL error plus an advanced_mode.also_valid_sql pointer ("… (valid as SQL in advanced mode)") — keeping the actionable DSL fix while pointing at advanced mode; bare "this is SQL" is reserved for entry words with no DSL form (select/with); a fully-overlapping input (insert … values …) legitimately yields two distinct commands (Command::Insert typed-AST vs Command::SqlInsert validated-text) that do the same thing but execute differently (ADR-0030 §4), so each is tested in the mode that produces it; corrects the plan's 3j exit-gate premise that the DSL DML tests run in Simple mode (they call parse_command, which defaults to Advanced) — the real invariant is "Simple-mode behaviour unchanged, Advanced mode SQL-first, DSL grammar tested in Simple mode, both variants tested in their producing mode", with §6/§7 parity keeping the paths observably equivalent; and defers to its own future ADR the execution-time mode side-channel (three-way Mode: simple/advanced/advanced-one-shot threaded through Action→worker, for mode-dependent output like echoing generated SQL) — today only the rendering side-channel OutputLine.mode_at_submission exists, and the three-way distinction is not required for Phase 3 dispatch correctness; Amendment 4, 2026-05-27 (design agreed, pending impl): reverses Amendment 3's update … --all-rows counter-example as a bug — surfaced by the ADR-0038 echo design. The walker has no -- comment support (it lexes two minus operators) while the engine treats -- as a comment, so update T set x=42 --all-rows was silently parsed as the expression 42 - -all - rows over non-existent columns all/rows (an ADR-0027 "flag-if-it-will-fail" case) and matched SqlUpdate. Decision: the --all-rows sequence makes the SQL UPDATE shape fail, so dispatch falls back to the DSL Update { AllRows } — symmetry with delete … --all-rows; no -- comment feature introduced (trailing comments stay unsupported). Inverts sql_dml_e2e.rs::e2e_update_all_rows_in_advanced_does_not_fall_back_to_dsl; mechanism settled test-first in the build; folded into the ADR-0038 effort (makes update … --all-rows echoable); Amendment 5, 2026-05-28 (implemented + verified, user-confirmed): advanced_mode.also_valid_sql (the cross-mode pointer from Amendment 3) fires on validity, not just parse"valid" meaning input_verdict_in_mode(input, schema, Mode::Advanced) == None in the ADR-0027 sense (parse succeeds and no Warning/Error diagnostic from any pass). Surfaced by issue #1: a positional INSERT INTO T VALUES (…) (no column list) with a value count that didn't match the target's column count parsed in advanced but failed at the engine, so the syntactic-only Amendment-3 gate promised a mode switch that wouldn't help. Closes the gap by (a) extending dml_insert_arity_diagnostics (§8.1, previously Form A only — its own doc-comment deferred Form B) to also check the no-column-list form against the schema's column count, emitting a new diagnostic.insert_arity_mismatch_form_b ERROR per offending tuple, and (b) refactoring advanced_alternative_note to read the validity verdict instead of running its own bespoke check — any static diagnostic added to the pipeline in the future automatically participates in the pointer gate. Side benefit: the [ERR] validity indicator now lights up at typing time for the reported scenario, no longer needing a submit to learn the line is wrong. Tests pinned: insert_form_b_arity_mismatch_under_supply_fires / _over_supply_fires / _match_is_silent / _unknown_table_is_silent (walker); ambient_hint_omits_advanced_pointer_when_form_b_value_count_wouldnt_match (gate); simple_mode_submit_of_sql_construct_appends_advanced_pointer (pointer still fires for genuine SQL-only constructs against a known schema). Amendment 3's "would parse in advanced mode" should henceforth be read as a synonym for "valid in advanced mode" in this stricter sense; the user-confirmed behavioural change is exactly the issue #1 bug case (no other input flips its pointer state)
  • ADR-0034 — history.log as a complete command journal; replay reads success-onlyAccepted, resolves a three-way tension in history.log's roles found while implementing ADR-0033 3f: (1) the persistent log is success-only while the in-memory Up/Down recall ring records every submission (success or failure, "so users can recall and edit typo'd commands"), and the ring is re-seeded from the log on project open — so failed commands are recallable within a session but silently lost across sessions; (2) replay wants the state-building (successful) commands while recall wants everything typed, which one success-only file cannot serve; (3) replay history.log never actually worked — run_replay parses each whole line through the DSL parser with no understanding of the <ts>|<status>|<source> record shape, so a real log fails on line 1, and no test ever fed the pipe format to replay (the replay_history_log_records_subcommands_only test only checks what replay writes, never replays the log as input). Decision: history.log becomes a complete journal — every submission recorded, tagged ok/err via the status field the format already reserved (ADR-0015 §5) — and each consumer filters: hydration reads all records (cross-session recall matches in-session), replay reads ok only (and learns the journal format, while still accepting bare-command .commands scripts; detection by the leading timestamp+status prefix so a | inside a bare command isn't misread). Successful commands stay journalled transactionally by the worker; failed commands are journalled err best-effort from the runtime/app error path (a parse failure never reaches the worker). Amends ADR-0006's "successfully executed" wording and ADR-0015 §5 ("status always ok") / §12 (hydration). Code deferred to two tracked test-first sub-tasks (journal-failures+filtering; replay-parses-journal-format); existing all-ok logs need no migration; implemented 2026-05-24 (plan docs/plans/20260524-adr-0034-history-journal.md); Amendment 1 (2026-05-24): replay filters out app-lifecycle commands — a working replay history.log (the §3 fix) exposed that the journal also records save as/load/new/export/import/rebuild/mode (which would panic the worker dispatch or abort the replay), so replay now re-applies only schema/data write commands and skips every Command::App + nested Command::Replay; all skips continue (never abort — reversing the prior nested-replay refusal, so a journal containing a once-run replay needs no hand-editing, and the infinite-loop footgun is closed by construction), with a [skip] warning on import and nested-replay skips (their omission can leave replayed state incomplete) and silent skips for the rest; replay.error_nested removed, replay.skipped_import/replay.skipped_replay added, ReplayCompleted carries warnings
  • ADR-0035 — Advanced-mode SQL DDLAccepted (design agreed 2026-05-24; validated end-to-end by sub-phases 4a/4a.2/4a.3/4b CREATE TABLE (incl. foreign keys) + 4c DROP TABLE [IF EXISTS] + 4d CREATE [UNIQUE] INDEX / DROP INDEX [IF EXISTS] + 4e ALTER TABLE add/drop/rename column + 4f ALTER TABLE … ALTER COLUMN TYPE + 4g ALTER TABLE add/drop constraint + add FK + 4h ALTER TABLE … RENAME TO + 4i verification sweep (completion merge + simple/advanced completion colour + describe of table-level constraints + self-ref FK indicator + CREATE-TABLE help/usage), implemented 2026-05-25/26 — Phase 4 complete; Amendment 1, 2026-05-26: drop a composite UNIQUE via a derived, engine-neutral unique_<cols> name that reuses the existing DROP CONSTRAINT <name> grammar — no new syntax, no metadata, §4g anonymity intact; describe shows the name; dropping a UNIQUE-covered column now refuses with that name + the drop command), Phase 4 of the ADR-0030 roadmap (peer of 0031/0032/0033) and clarifies ADR-0030 §4. Advanced-mode CREATE/DROP/ALTER TABLE + CREATE/DROP INDEX get their own per-statement commands (SqlCreateTable/SqlAlterTable/SqlDropTable/SqlCreateIndex/SqlDropIndex), like DML's Sql* set — but unlike DML they execute structurally, not verbatim (raw execution would lose the playground's types, named relationships, and STRICT; "verbatim" was a DML convenience, not a rule). Handlers reuse the low-level schema/metadata helpers where the operation matches simple mode and stand alone where the SQL surface is richer (clarity over forced refactoring); simple mode is untouched (additive). Dispatch: create/drop reuse ADR-0033 Amendment 1's category-grouped mode-aware dispatch (SQL-first, simple fallback); alter is a new advanced-only entry word. Full surface (no pre-emptive cuts, Q4): CREATE TABLE with column + table constraints, single/compound PRIMARY KEY, inline + table-level FOREIGN KEYnamed relationships (one statement = one command = one undo step, ADR-0006); ALTER TABLE add/drop/rename column, ALTER COLUMN TYPE, add/drop constraint, add FK, RENAME TO (advanced-only table rename — new low-level op renaming the table + its CSV + the relationship and table-CHECK metadata, closing the rename half of C1); CREATE [UNIQUE] INDEX / DROP INDEX. Type slot accepts the ten playground keywords and standard-SQL aliases (integerint, varchartext, timestampdatetime, …; length args accepted-and-ignored; no engine type names in/out — ADR-0030 §5). CHECK/DEFAULT reuse ADR-0031 sql_expr. Pre-implementation /runda refinements (2026-05-24, user-confirmed): CREATE TABLE/DROP TABLE admit IF [NOT] EXISTS (no-op-that-succeeds-with-a-note — a near-universal cross-vendor idiom, reclassified into scope, not engine-specific); INTEGER PRIMARY KEY maps to a plain int PK, not auto-increment (serial stays the sole auto-increment type). Column-type-conversion is unified (ADR-0017 engine, mode-appropriate policy): clean auto-converts and incompatible/own-type-static cases refuse in both modes, but a lossy change refuses-by-default in simple mode (--force-conversion opts in) while advanced mode performs it with a loss note and relies on undo as the safety net — no force flag, no dropping to simple mode (a payoff of shipping ADR-0006 first). OOS: views/triggers/txn-control/PRAGMA/etc. (ADR-0030 §3), the Postgres USING clause, and the DSL→SQL teaching echo (ADR-0030 Phase 5). Sub-phases 4a4i, plus 4a.2 (per-column CHECK/DEFAULT via raw sql_expr text — sql_expr is validate-only, no Expr AST — + composite UNIQUE(a,b); no new internal table) and 4a.3 (table-level/multi-column CHECK, landed via the new __rdbms_playground_table_checks metadata table because SQLite has no PRAGMA for CHECK; the builder tells a table-level CHECK from a column-level one by element position) and 4b (foreign keys — inline REFERENCES + table-level FOREIGN KEY → ADR-0013 named relationships in the create transaction, one undo step; self-references + bare REFERENCES <parent> supported, user-confirmed) and 4c (DROP TABLE [IF EXISTS]SqlDropTable, reusing do_drop_table; IF EXISTS is a no-op-with-note via DropOutcome::Skipped) and 4d (CREATE [UNIQUE] INDEX [IF NOT EXISTS] [<name>] ON <T> (cols)SqlCreateIndex and DROP INDEX [IF EXISTS] <name>SqlDropIndex, reusing do_add_index/do_drop_index; CREATE UNIQUE INDEX admitted — ADR-0025 Amendment 1 — via an additive IndexSchema.unique flag that round-trips through project.yaml and rebuild, with [unique] markers in the structure view + items panel, while simple-mode add unique index stays deferred; IF [NOT] EXISTS reuses the 4c skip path; create/drop each gain a second advanced node, exercising the all-candidates dispatch) and 4e (ALTER TABLE add/drop/rename column → SqlAlterTable; alter is a new advanced-only entry word, runtime-decomposed to the existing do_add_column/do_drop_column/do_rename_column — no new worker layer; do_add_column extended to consume raw default_sql/check_sql so ADD COLUMN reaches CREATE-TABLE constraint parity; drop/rename refuse a column any CHECK references (table-level AND column-level, incl. a column's own self-check on rename) — the 4a.3 deferral, via a raw-CHECK-text tokenizer in the shared executors, so it guards both surfaces and fixes a latent rename-drift bug; SQL DROP COLUMN refuses an index-covered column with no --cascade spelling; the column executors + do_add_index gained an internal-__rdbms_*-table guard — all user-confirmed) and 4f (ALTER TABLE … ALTER COLUMN TYPE → a fourth AlterTableAction, runtime-decomposed to the existing change_column_type with ChangeColumnMode::ForceConversion — which is the §7 advanced policy: lossy converts with a note (no force flag), incompatible + ADR-0017 static refusals (↔ blob, same-type, date ↔ datetime, non-int → serial) still refuse, while int → serial is allowed (auto-fills nulls + UNIQUE, ADR-0018 §8 — the §7 "→serial refused" summary is looser than the code); the builder discriminates the fourth branch by the type keyword (unique — ADD COLUMN's type is an ident), the type slot reuses SQL_TYPE; the internal-__rdbms_* guard was folded into do_change_column_type, closing the simple change column exposure too — user-confirmed) and 4g (ALTER TABLE … ADD [CONSTRAINT <name>] (CHECK | UNIQUE | FOREIGN KEY) + DROP CONSTRAINT <name>; ADD = CHECK + composite UNIQUE + FK, with ADD PRIMARY KEY and a named UNIQUE refused — composite UNIQUE is anonymous in our model; each ADD reuses a low-level path (table-CHECK/UNIQUE rebuild with a dry-run guard; FK → add_relationship, bare REFERENCES <P> → parent single-PK), DROP CONSTRAINT resolves the name to a table-CHECK then a child-side FK; named table-CHECKs round-trip via a nullable name column on __rdbms_playground_table_checks (rebuild-only arrival — pre-4g projects gain it on rebuild, a named add on an un-upgraded project is refused with a friendly "rebuild first" message) and a project.yaml check_constraints extension to an {expr, name} mapping (the bare-string form still reads); the internal-__rdbms_* guard was folded into do_add_constraint/do_add_relationship, completing that guard class — all user-confirmed) and 4h (ALTER TABLE … RENAME TO — the one genuinely new low-level op, do_rename_table: a native engine rename plus one-transaction reconciliation of every metadata row naming the table (__rdbms_playground_columns, both ends of __rdbms_playground_relationships, __rdbms_playground_table_checks), the CSV file (the existing rewrite+delete path — no new persistence method), and CHECK text that qualifies a column with the old table name (T.ageU.age, a planning-/runda finding — the engine rewrites the live CHECK but the stored text would drift and break a fresh rebuild; rewrite_check_table_qualifier keeps them in step); grammar splits the rename verb into one branch with an inner Choice on a distinct second keyword (column vs to), the new-name slot mirroring the CREATE TABLE name slot; refuses same-name / existing-target / __rdbms_* / non-existent, with case-insensitive collision checks behind an engine-neutral pre-check (a finished-slice /runda finding — the engine matches names case-insensitively); auto-named indexes and relationships keep their stale names (only table-name columns update — §6 scope); one undo step; advanced-only, closing the rename half of C1 — all user-confirmed) and 4i (the verification sweep that completes Phase 4: the shared-entry-word completion merge + the simple-vs-advanced completion colour-when-mixed with Both→Advanced→Simple block ordering; describe of table-level composite UNIQUE + table CHECK; the self-ref FK pre-submit indicator fix; and the CREATE-TABLE help/usage skeleton refresh). All of Phase 4 (4a4i) is shipped. Each sub-phase has exit + DA gates; Amendment 2, 2026-05-27 (design agreed, pending impl): a standard-first dialect stance (refines ADR-0030's "standard SQL" posture — ISO spelling is canonical + echoed where one exists; a vendor shorthand may be accepted but isn't canonical; where ISO offers none, one documented vendor spelling is a deliberate extension) + an ALTER COLUMN constraint gap-fill surfaced by the ADR-0038 echo design: makes ISO ALTER COLUMN … SET DATA TYPE the canonical type-change verb with TYPE retained as a synonym (reverses §4f's "no SET DATA TYPE"), and adds SET/DROP DEFAULT (ISO) + SET/DROP NOT NULL (the one documented extension — ISO has no in-place NOT-NULL verb; PostgreSQL's chosen for being type-independent), all rebuild-backed via the existing ADR-0029 do_add_constraint/do_drop_constraint executors (dry-run + internal-table guards free, no new worker layer), reaching simple↔advanced constraint-mod parity for NOT NULL + DEFAULT; the rebuild stays hidden (Category-1 engine detail, ADR-0038). Residual gap left open + flagged: dropping a column-level (anonymous) UNIQUE/CHECK (no portable name — same class as Am1's parallel gap), which ADR-0038's catalogue marks "no headline echo"
  • ADR-0036 — Value validation for advanced-mode DMLAccepted (design agreed + /runda'd 2026-05-26; mechanism then deliberately narrowed from "bind literals via the DSL path" to surgical "validate-and-retain, execute verbatim" after the user resisted consolidating the modes and a concrete auto-fill difference confirmed even the single-row literal case isn't identical across modes; Phases 12 implemented 2026-05-26 — INSERT … VALUES and UPDATE … SET literal validation + offending-value retention, capture-at-parse with no grammar change; Phase 3a implemented 2026-05-26 — live typed-slot hints + numeric-shape highlighting for UPDATE/UPSERT SET col = <literal> via a boundary-aware lookahead (Amendment 1 corrects this ADR's naive-Choice sketch); Phase 3b implemented 2026-05-27 — per-position typed slots for INSERT … VALUES (single/multi-row, Form A/B) via a new zero-width Node::SetColumn primitive + an arity-gating tuple lookahead that preserves the §8.1 arity diagnostic; fully implemented). Augments — does NOT supersede — ADR-0030 §4 / ADR-0033 §10: execution stays verbatim, ADR-0033 Amendment 3's two-command identity (Insert vs SqlInsert) stands. The problem (investigated 2026-05-26; characterization test sql_insert.rs::sql_dml_skips_app_level_value_validation_that_the_dsl_enforces proves it): advanced-mode SQL DML gets none of the DSL's value feedback — a malformed date like 2025/01/15 is silently written, and the offending value is missing from constraint errors — because literal values are spliced into text and discarded (only STRICT storage types check them). Fix (surgical): validate each literal value against its column type before the verbatim insert, and retain it for error reporting — sharing only the per-type validators (Value::bind_for_column/validate_date/shortid::validate), nothing else. No binding, no statement reconstruction, no auto-fill change, no command-identity collapse — because the two gaps are closed by validation + retention alone, and executing the user's own text is already safe. The literal set = NULL/boolean/string/signed-numeric; arithmetic/functions/subqueries/column-refs are expressions (skipped — the engine evaluates them). WHERE not validated (it's an expression in general; motivation met by VALUES/SET); SELECT/INSERT … SELECT/RETURNING/ON CONFLICT need no special handling since execution is untouched. Phased: Phase 1 capture-at-parse + validate + retain for INSERT … VALUES (no grammar change, no reparse — closes both proven gaps); Phase 2 UPDATE … SET literals; Phase 3 completion hinting/highlighting (the only part needing a grammar change — a typed-literal slot vs sql_expr reusing the DSL TypedValueSlots at data.rs:141/189/269, discriminated by a boundary-aware lookahead not a naive Choice per Amendment 1; split into 3a SET (done) and 3b VALUES (pending); supersedes only Phase 1/2's literal detection, not the validation/enrichment on top). Non-goals: binding/reconstruction, collapsing command identity (Am3 stands), changing serial/shortid auto-fill (requirements.md X4, a separate possible-bug), a structural SELECT, a full SQL-expression AST. Embodies requirements.md X5 (share a mechanic, not a command); the neutral "that value" safety net (ADR-0035 Amendment 1) stays correct for genuinely-computed values; Amendment 2 (issue #17, 2026-05-29) brings the §8.1 arity diagnostic to simple mode at parity with advanced: a tuple_value_list-style gate (dsl_insert_value_list, simple-mode-gated so advanced is byte-for-byte unchanged) routes a wrong-count DSL insert tuple to the type-blind fallback so it matches and the friendly arity diagnostic fires (instead of a bare expected ,/)``); dml_insert_arity_diagnostics is now mode-aware (advanced Form B = all columns, simple Form B/C = user-fillable since serial/shortid auto-fill, ADR-0018 §3), counts the DSL Form A role (insert_first_item) and the keyword-less Form C tuple, with new keys insert_arity_mismatch_form_b_simple / _all_auto; a wrong-count DSL insert now parses Ok + carries the ERROR diagnostic (the [ERR] verdict), with a unified Ok-arm submit pre-flight (dsl_insert_count_mismatch_notes) blocking dispatch + teaching (the issue #1 Err-arm note retires). Arity-UX parity only — no consolidation of value-handling/execution/auto-fill; the deliberate mode-distinctness stands
  • ADR-0037 — Execution-time mode side-channel (the three-way submission mode)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 shortids, 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. Amendment 1 (2026-05-31, issue #10): the output tag is colour-coded by message status (its OutputKind), not the mode — narrowing the side-channel to its stated purpose (the mode tint lives on the echo tag alone). [system] tag → green, [error] tag → red, Echo tag → mode tint (the sole exception); bodies go neutral (theme.fg), the error body bold (rustc-style: severity-coloured label, readable bold message — a wall of red prose is harder to read). Yields a status traffic-light (green = ok, red = error) matching the ADR-0040 ✓/✗ markers; supersedes issue #10's three sketched options and closes the tag-colour gap ADR-0040 had flagged as orthogonal
  • ADR-0038 — The DSL → SQL teaching echoAccepted (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 queriesAccepted (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 lineAccepted 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)
  • ADR-0041 — Copy the output panel to the system clipboardAccepted 2026-06-02 (issue #11), amends ADR-0003's app-command registry (adds copy / copy all / copy last). The friction it removes: filing a bug report meant terminal-selecting the output panel and fighting wrapping/borders. New app-level command (sigil-free, both modes): copy / copy all copy the whole panel; copy last copies from the most recent echo line to the end. Mechanism — OSC 52 and native (arboard), always both, because OSC 52 acceptance is undetectable (no terminal ack), so a true "fall back when unsupported" can't be built: emit the OSC 52 escape (no new dep — base64+crossterm; works over SSH; tmux-passthrough-wrapped via $TMUX), then a best-effort native write whose failure is ignored (headless host — OSC 52 carried it); the two carry identical content. Format — plain text verbatim as rendered (tags, /, box-drawing) joined by \n, without viewport padding/wrapping; a drift-lock test pins OutputLine::plain_text to render_output_line. arboard added --no-default-features (drops the image crate; X11-only on Linux — wayland-data-control deliberately omitted as it ~doubles the dep tree and OSC 52 covers native-Wayland). Security: write-only, scans clean for arboard's tree (cargo audit / osv-scanner / grype), 1Password-maintained, minimal surface. OOS: Markdown export, selection/range, a keybinding, OSC 52 read, screen passthrough
  • ADR-0042 — H1a parse-error pedagogy in the grammar-tree eraAccepted 2026-06-03. Continues H1a from ADR-0021 against the ADR-0024 grammar tree (ADR-0021's chumsky mechanism is dead). Records the baseline already shipped — per-command usage: block (38 parse.usage.* templates), available-commands fallback, structural "after , expected …" wording, source-derived ident slot labels ("table name"/"column name"), curated parse.custom.* near-miss messages, and the ADR-0027/0033/0036 schema-aware [ERR] diagnostics — so H1a is substantially delivered at the intent level. Defines the remaining work as (1) a verified per-command near-miss matrix (tests/typing_surface/ + tests/it/parse_error_pedagogy.rs) as the definition of done, test-first; (2) friendlier literal expectation labels — optional prose glosses on Word/Punct/Flag positions that add role context while always keeping the exact literal visible (e.g. "a filter clause: where … or --all-rows"); (3) advanced-mode SQL near-miss parity (RETURNING scope, CTE-arity positioning, CROSS JOIN … ON, INSERT…SELECT count) — in scope, kept distinct from ADR-0019 §OOS-2 which covers advanced-SQL engine-error sanitisation, a different layer. Catalog/anchor-phrase discipline (ADR-0019) preserved; no public API change. OOS: I3/I4, spell-correction, multi-error reporting, verbosity-gating the usage block
  • ADR-0043 — Compound-primary-key foreign-key references (T3)Accepted + implemented 2026-06-09 (all four forks confirmed at the recommended option: full-PK matching, house-style uniform lists, parenthesized DSL syntax, bare-SQL-FK auto-expansion). Closes requirements.md T3 [x] — the relationship model went list-based across six layers (single-column preserved, no migration), DSL from P.(a,b) to C.(x,y) + SQL FOREIGN KEY (a,b) REFERENCES P(x,y) parse/execute/enforce, 12 tests in tests/it/compound_fk.rs. Closes the open leg of requirements.md T3: a foreign key that references a parent's compound primary key. A 2026-06-09 audit found single-column FK woven through ~1520 sites (metadata table, RelationshipSchema, project.yaml RawEndpoint, both grammar surfaces, executor FK-DDL emission, per-column type-compat, display) — earns an ADR, not an inline build. Decision: reference the parent's full compound PK, matched positionally to an equal-length child column list, per-pair fk_target_type compat (ADR-0011, element-wise); DSL from <P>.(a, b) to <C>.(x, y) (single form unchanged), SQL FOREIGN KEY (x, y) REFERENCES P(a, b) (extend the existing one-cap lists; bare table-level FK auto-expands to the parent PK when arities match). Storage — no migration (back-compat not required, user-confirmed 2026-06-09; no installed base): the relationship endpoint joins the list convention project.yaml already uses — columns: [a, b] like primary_key: [id] and index columns: [...] (the endpoint was the lone scalar column: holdout); the metadata TEXT columns are unchanged and store the list comma-joined (a,b; the bare name for single — safe because identifiers are [A-Za-z0-9_]+). No F3 migrator, no version bump; accepted trade-off is that a pre-change project.yaml with relationships won't load (clean cutover). In-memory model goes list-based (Vec<String>) through all six layers; the enforced FK is the rebuilt child-table DDL (FOREIGN KEY (a,b) REFERENCES P(x,y)), one relationship = one undo step (ADR-0013). Genuine forks escalated: matching policy (full-PK vs subset), storage (house-style uniform lists vs normalized table), DSL syntax (parenthesized vs repeated-dotted), bare-SQL-FK auto-expansion. OOS: subset/non-PK (UNIQUE-targeted) FK references; any single-column behaviour change
  • ADR-0044 — Relationship visualization (two-table connector diagrams)Accepted 2026-06-09; implemented 2026-06-10 (closes requirements.md V1; second /runda pass over the implementation; §3 last-resort helper line considered and rejected). Resolves ADR-0016 OOS-1 and closes the open half of requirements.md V1 ("a selected relationship as two tables joined by a line"). Renders a relationship as Style A (two structure boxes + connector). Reach = "relationship-relevant" (user-chosen over global / show-only): diagrams on the surfaces where the relationship is the subjectshow relationship <name> (one full diagram), show table <T> (T's structure box then a Relationships section of stacked compact per-relationship diagrams — chosen over a focal-centred subgraph: no crossing lines, scales via scroll, two-boxes-wide fits any terminal), and relationship DDL echoes (add/drop/modify relationship); incidental DDL echoes (add column, drop index, change column, plain create table) keep the terse prose, via a Diagram|Prose render mode on render_structure. Reading convention child(FK)-left / parent-right, arrow →, n1 cardinality, applied uniformly; every box gets a bold title row + rule so the name can't read as a column. Compound FKs (ADR-0043) route one connector per positional pair + an explicit pairing line. Width-aware (first in the codebase) but App-side: render_structure/diagram rendering runs in app.rs (the worker only returns TableDescriptions), a new App::last_output_width (set from ui.rs) drives side-by-side vs a vertical-stack fallback + last-resort "run show relationship" pointer; rendered once at command time, no live reflow (V4). show relationship's worker path (do_show_one, prose-only) is restructured to return both endpoint TableDescriptions. Styling reuses ADR-0028 App-side styled runs (new classes: table-name/key/connector/cardinality/action) — no worker→UI contract change. Partially supersedes ADR-0016 §5 (prose block replaced on relationship-subject surfaces, retained on incidental ones); extends §4 (layout width-awareness, still no cell truncation) and §6 (per-span theming). Tests: insta snapshots (single, compound, vertical fallback, helper line, self-referential, multi-rel show table) + width-threshold/routing unit tests + Tier-3 wiring; enumerated prose-fallout updates (output_render.rs:121/135/793, the relationships snapshot, walking_skeleton.rs:477/530). A /runda DA pass corrected three inverted-architecture claims (App-side rendering, untracked width, prose-in-worker show-relationship) before acceptance. OOS: user-configurable display setting (OOS-7), live reflow (V4), whole-DB ER export (V3), m:n (C4), ASCII fallback (ADR-0016 OOS-5)
  • ADR-0045 — create m:n relationship convenience command (C4)Accepted + implemented 2026-06-10 (closes requirements.md C4; all forks user-confirmed + a /runda DA pass that verified the do_create_table reuse against code and corrected the "no PK-less tables" assumption — advanced SQL create table t (a int) has none, so a parent-PK guard is retained). Implementation corrected a second ADR premise: "the walker already dispatches multiple nodes per entry word" held only in advanced mode — two simple-mode spots (dispatcher decide, completion continuation-merge) assumed ≤1 DSL form per entry word and were generalized behaviour-preservingly (dispatch reduces to the old single-candidate commit; completion merge gated on simple_count > 1). Junction echo wired (render_create_m2n, round-trips as SQL). create m:n relationship from <T1> to <T2> [as <name>] generates a junction table with one FK column per parent PK column, a compound PK over all the FK columns (the textbook junction — the pair is unique, no duplicate links), and two 1:n relationships, all in one transaction = one undo step (built by reusing do_create_table, which already takes foreign_keys + writes relationship metadata — no batch bracketing). Forks all user-chosen: junction PK = compound-over-FKs (vs surrogate serial / no PK); referential actions = CASCADE on delete+update (vs NO ACTION / RESTRICT); naming = auto {T1}_{T2} + optional as (vs auto-only); available in both modes (Simple-category DSL, like the sibling relationship commands). FK columns named {parent_table}_{pk_column} (disambiguates shared id; generalises to compound parents via ADR-0043), typed via fk_target_type (ADR-0011). A distinct Command::CreateM2nRelationship (not lowered to CreateTable) preserves command identity (X5) and lets the teaching echo speak in m:n terms. Cross-cutting wiring enumerated: separate CREATE_M2N CommandNode (own help_id/usage_ids), ("m","m:n") completion composite, HintModes, grammar-driven highlighting, help/help create, parse_error_pedagogy near-miss matrix, teaching echo. OOS: self-referential m:n (from T to T) refused outright (user-confirmed "full stop" — directional column-naming is more than this beginner convenience warrants); per-relationship action overrides; extra junction payload columns; m:n diagram echo; renaming the auto-generated relationships
  • ADR-0046 — Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20/#21/#23)Accepted + implemented 2026-06-10, phased A→B→C (8 commits 9f5f76b22bec61; closes Gitea #20 hint jumpiness, #21 left-column improvements, #23 long input — all forks user-confirmed, including the persistent show/hide toggle which is deferred: the Ctrl-O peek covers #21's "keystroke to show and hide"). Two decisions landed differently from the draft (recorded inline): relationship data on App not SchemaCache (DB2); the nav overlay clears only the sidebar strip + a one-column gutter, panels staying visible behind (DC2). Treats the three UI issues as one coupled decision because they share the terminal's width/height budget. Phase A (input & hint): the hint panel's height becomes a function of terminal geometry, fixed between resizes (not of hint content), eliminating the #20 jump at its source — measured catalog shows ≥ ~54-col right-column width never needs > 2 hint lines, so 3 lines is a rare narrow-terminal-only case; height buckets H<40 compact (input 1 row + horizontal scroll / hint 2) vs H≥40 comfortable (input 2 rows soft-wrap / hint 2), output Min(5) honoured first under degradation; input gains horizontal scroll (input_scroll_offset, single logical Stringnot I1 multi-line) and 2-row soft-wrap display when tall, preserving ADR-0027's 6-col indicator reserve. Phase B (sidebar): the 26-col Tables column is kept but made optional and richer (not deleted — pedagogy wins ties) — width-derived session-only visibility (visible iff width > 90 or a Ctrl-O peek is active — no stored field; hides at width ≤ 90 so the 90-col screencasts drop it; ADR-0015 format untouched), plus a relationships panel rendered narrow with endpoints broken at the arrow, ellipsized — a separate sibling panel that overrides S2's nested-list extension model (relationships are cross-table). the full records live on a new App.relationships field (revised from the ADR's original SchemaCache.relationship_details at implementation — SchemaCache is walker-facing and needs only the names, kept in relationships: Vec<String>; details are UI-only, so App mirrors app.tables and avoids ~23 fixture edits), delivered by Database::read_all_relationships + an AppEvent::RelationshipsRefreshed; the two left panels split vertically with the relationships panel floored at 5 rows ("(none)" when empty) and capped at 50 % of the column (DB4). Phase C (navigation mode): Ctrl-O enters a focus cycle (Input → Tables → Relationships → Input; Esc exits) orthogonal to the ADR-0003 input mode — Ctrl-B was rejected on review as the default tmux prefix (unreachable inside tmux); the focused panel expands to ~4050 cols as a Clear overlay (right panels stay unchanging underneath) and scrolls via Up/Down (line) + PageUp/PageDown (page) (context-rebind, reusing the output-scroll viewport mechanism), with an accent focus border; all non-nav keys inert in nav mode (and nav keys inert while a modal is open). Forks all user-chosen: keep-optional-richer (vs remove/narrow); navigation-mode (vs modeless modifier scroll); Ctrl-O (Ctrl-B rejected = tmux prefix); overlay (vs layout re-split); inert-non-nav-keys; geometry-fixed hint height; H<40/≥40 thresholds; session-only persistence; Up/Down line-scroll; separate relationships panel overriding S2; no hint-area toggle (S4's stale "keyboard-toggleable" claim struck — never implemented, unwanted). A pre-build /runda DA pass drove these corrections: caught the Ctrl-B/tmux collision, the SchemaCache retype that would have broken completion, the 2-row-input/indicator placement, the missing nav-mode key disposition + modal gate, and three unreferenced requirements (S1 evolved, S2 overridden, S4 corrected); also cross-checked open issue #22 (overlay/annotation layer — separate ADR, adjacent). OOS: true multi-line input (I1); readline shortcuts (I1b); cross-session sidebar persistence; output as a third nav focus; relationship search/edit from the panel; hint-area toggle; #22's annotation layer. Accepted consequence: the 90-col visibility threshold makes a terminal's output narrower when widened across the boundary (sidebar appears)
  • ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22) (commits f879d542d0f4b2; no requirements.md item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build /runda pass that produced 10 tightening findings and a whole-implementation /runda pass that returned PASS, no blockers). An in-app demonstration mode (--demo flag / RDBMS_PLAYGROUND_DEMO env, off by default, zero footprint when off) that renders two transient overlays so autocast screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. Keystroke badges ([TAB], [ENTER], [UP], …): automatic, app-detected over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure demo_badge_label(&KeyEvent); the badge auto-expires on a ~1.5 s timer that extends the runtime's existing time-boxed-recv arm condition (debounce.is_armed() || badge_pending; expiry Instant in the runtime, App.demo_badge the render mirror — mirroring the input vs input_indicator split). Step captions: a stealth, control-code-delimited input buffer toggled by Ctrl+] (byte 0x1D → arrives as Char('5')+CONTROL, verified against crossterm 0.29 parse.rs:110-113; chosen over Ctrl+!, which is not a single ASCII byte so autocast cannot send it — the same wall as arrow keys, R4) — typed characters accumulate invisibly (prompt untouched, no echo/history), Backspace edits, other keys inert, a second Ctrl+] commits to the caption box (empty commit dismisses); lives in pure-sync App::update(), intercepted before the modal gate so captions/badges work over the load picker (the #24 projects cast). Both render as floating flat black-on-yellow rectangles (solid fill, no border glyphs — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, 2d0f4b2) at the output panel's inner bottom-right, drawn last over modals, badge stacked above the caption, no layout reflow; caption word-wraps to ≤ 3 lines (35 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new App.last_output_area: Rect (set in render_output_panel) gives the top-level draw the anchor. Caption persists until the next keystroke; badge suppressed while capturing. Forks all user-chosen: --demo activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); Ctrl+] trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (--demo plumbing, badge set/suppressed, caption-without-input wiring), CLI (--demo parse + env fallback) — with an honest limit noted: the tokio timer wiring inside run_loop is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing IndicatorDebounce). One intentional, user-acknowledged behaviour: Ctrl-C is inert while capturing (every non-Ctrl+] key is, by spec). Final tally 2290 passing / 0 failing / 0 skipped (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website casts.mjs scripts (website-branch follow-up). Implementation phased A (--demo plumbing) → B (badges) → C (captions) + a flat-rectangle restyle