Files
rdbms-playground/docs/adr
claude@clouddev1 6d54c1e96c
ci / gate (push) Successful in 1m59s
ci / manifests (push) Successful in 4s
ci(publish): wire Scoop bucket + Homebrew tap jobs (D3 §3b/§3c)
Add sibling publish.yaml jobs (scoop-bucket, homebrew-tap) that render a
manifest from the release .sha256 sidecars and idempotently push it to the
org-level lazyeval/scoop-bucket and lazyeval/homebrew-tap repos, using the
scoped lazyeval-ci bot token (LAZYEVAL_PKG_TOKEN).

Render logic lives in dependency-free bash (the CI image has no jq/ruby):
scripts/render-scoop-manifest.sh and scripts/render-homebrew-formula.sh.
scripts/test-package-renders.sh exercises both: it validates the Scoop JSON
with node and asserts fields on both manifests, and additionally runs
`ruby -c` on the formula where ruby is present (dev box), skipping it
gracefully otherwise.

A new ci.yaml `manifests` job runs that test on every push so a render
regression surfaces immediately, not at the next manual publish dispatch.
The CI image has no ruby, so in CI the gate covers the Scoop JSON (node) and
field assertions for both manifests; the formula's Ruby syntax is checked
dev-side only (the static heredoc's variable parts cannot introduce syntax
errors).

- Scoop: x64 (gnu) + arm64 (gnullvm); #/-rename fragment so the bin shim is
  version-stable; checkver, no autoupdate (the pipeline is the updater).
- Homebrew: on_macos/on_linux x arch bare-binary formula; no Windows.

Docs: ADR-0056 Amendment 2 (+ README index, requirements D3).

Unverified pending real use: scoop/brew install, the HEAD:main branch
assumption, macOS Gatekeeper-via-brew on the ad-hoc-signed binary.
2026-06-19 21:30:18 +00:00
..

Architecture Decision Records

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

Website subproject ADRs live in a separate namespace — docs/website/adr/ — with their own dated sequence (<date>-adr-website-<NNN>.md, id ADR-website-NNN), so they never compete with this global integer pool for numbers (see ADR-0000 "Numbering discipline"). The website itself was decided in ADR-website-001 (formerly ADR-0044 in this index).

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 — the ten-type set (text/int/real/decimal/bool/date/datetime/blob/serial/shortid), compound PKs, no true UUIDs; decimal stored as exact TEXT. Amendment 1, 2026-06-12 (issue #32): SQLite has no native decimal/BCD type, so arithmetic/aggregation over a TEXT decimal is implicitly coerced to an IEEE-754 double and the computed (typeless) result leaked float noise (298.59999999999997 for 298.60); floating-point values are now rounded to 15 significant figures for display only (format_real_display in db.rs, wired into format_cell — the result-set/show data cell formatter, the only surface where arithmetic noise surfaces) while every other f64→string path keeps full precision because the distinction is semantic: persistence (csv_io::format_real) stays byte-exact for round-trip; render_value is a canonical identity key for the uniqueness dry-runs (dry_run_unique ADR-0029 §5, check_uniqueness_collisions ADR-0017 §4.3) so rounding it would report collisions the exact-valued engine wouldn't; FK-key matching and EXPLAIN-SQL literals likewise stay exact — so stored real/decimal round-trips stay byte-exact and raw decimal columns render verbatim
  • 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; Amendment 7 surfaces optional positional args in the hint panel (issue #26): at seed <table> ▮ the optional row count (a bare NumberLit with no candidate) was invisible next to the set/--seed chips, and the resolver short-circuits on the already-complete command. Extends the issue-#4 IntroProse HintMode (ADR-0024 §HintMode-per-node) to survive trailing optionals: walk_optional stashes a skipped inner's IntroProse key into a new WalkContext.surviving_intro_hint (key + position) before the empty match clears pending_hint_mode, and the snapshot keeps it only when the skip position is the cursor (so it never leaks past a later-consumed set … clause or once the count is given); the resolver returns it ahead of the empty-expected short-circuit. The seed count is wrapped Hinted{IntroProse("hint.seed_count")}; prose names the count (default 20), the .column column-fill form, and set/--seed (user-chosen scope). Only IntroProse is carried; ProseOnly/ForceProse and the CREATE-TABLE element (a required Repeated) are untouched; no AmbientHint/renderer change
  • 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; Amendment 3, 2026-06-12 (issue #31): a bare in-scope table alias at an expression slot (… GROUP BY o, o aliasing FROM Orders o) is no longer a blind spot — completion now offers each FROM source's qualifier (alias-if-present-else-table-name) at a bare sql_expr_ident slot (folded into the column candidate list; the alias source steps aside on an exact-qualifier partial so the diagnostic can surface), and the matched.len()==0 bare-reference arm emits a targeted diagnostic.alias_used_as_column / diagnostic.table_used_as_column ("o is a table alias — write o.<column> …") instead of the misleading unknown_column (a drop-in replacement at the same span/Error severity, so verdict/overlay/hint paths are unchanged), checked after the projection-alias check so ORDER-BY alias refs still win; two guards keep the advice correct — SQL-only (role == "sql_expr_ident", so the DSL expr_column path keeps unknown_column since the DSL has no table.column syntax) and effective-qualifier match (alias-if-present-else-table, so an aliased source referenced by its shadowed real name falls through rather than being advised as name.<column>); a genuine unknown column still reports unknown_column
  • 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); Amendment 1, 2026-06-12 (issue #25): DC3's focus accent is now a non-bold accent colour (theme.mode_simple, blue) rather than bold bright-fg — bold box-drawing glyphs render as broken/gapped line-art in the asciinema cast player (and are fragile in some terminals), so panel_border_style carries no Modifier::BOLD on a border (bold stays fine on text spans); pure style change — the text-only Tier-2 snapshots were unaffected, the Tier-1 assertion was updated, and a render-level test now checks the focused border cells carry the accent and no bold
  • 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. Amendment 1 (2026-06-15): in demo mode, Ctrl-G aliases F1 (the ADR-0053 hint overlay) and badges as [F1], so an autocast cast — which can't emit F1's escape sequence — looks identical to a real F1 press; Ctrl-G is the only fit (Ctrl+digit is unencodable in a legacy terminal, e.g. Ctrl-1 arrives as a bare 1), demo-gated so the shipped keymap stays F1-only; also cleaned two stray </content>/</invoke> lines from the ADR file.
  • ADR-0048 — seed fake-data generation commandAccepted 2026-06-11; Phase 1 + Phase 2 implemented 2026-06-11 (Phase 1 commits 202e25afbd219b; design settled with the user across an extended fork dialogue, hardened by a pre-build /runda pass (six blockers folded in), a post-implementation /runda pass (eight gaps closed — FK/shortid determinism so D4 holds with no exceptions, plus six untested ADR decisions), and a Phase-2 pre-build /runda pass (which caught the no-date-literal-token reality → the D2 quoted-dates amendment), and a post-implementation /runda pass (which added a friendly error for a bounded override on a UNIQUE column — see D2); 2400 tests pass, clippy clean). Closes requirements.md SD1 and the core of SD2; closes the seed half of A1. Phase 1 shipped: whole-row seed <table> [count] [--seed <n>] with realistic name-aware generation (the fake crate + a type-gated heuristic catalogue, table-context name disambiguation, hand-rolled product generator, bounded dates), identifier + constraint uniqueness incl. junction distinct-combos, FK sampling from existing parent rows (empty-parent error), IN-CHECK derivation + complex-CHECK advisory, a required-column block guard, --seed reproducibility (serial/FK/shortid all deterministic), undo as one batch step, replay as a data write, a capped auto-show preview, the enum/CHECK advisory, and an O(N) single-transaction insert path. Phase 2 shipped (2026-06-11): the set override clause (D2 — fixed value / pick-list / as <generator> / between range, quoted dates per the D2 amendment, type-aware, override drops the column from the advisory) and the <table>.<column> column-fill form (D1 form 2 — an UPDATE over existing rows, refusing PK/autogen targets, empty-table no-op, FK/unique-respecting, one undo step), with the new KNOWN_GENERATORS vocabulary (D9), a range Generator, full completion/highlight (HighlightClass::Function)/validity (IdentSource::Generators)/help/pedagogy wiring, and the D13 advisory's Phase-2/3 wording. Further SD2 increments (custom generators, NULL injection, multi-locale, recursive auto-seed) out of scope. Closes requirements.md SD1 and the core of SD2; closes the seed half of A1 (the other being hint/H2). A dedicated seed command (own AST variant + do_seed executor, both modes) generating realistic, name-aware fake data. Two forms: seed <table> [count] (new rows, default 20, capped) and seed <table>.<column> (fill a column on existing rows, an UPDATE). Generation adds the fake crate (v5, English) driven by a type-gated, token-matched name-heuristic catalogue (~30 patterns, documented false-positive guards), with table-context disambiguating the name/title family (products.name→product, users.name→person, vendors.name→company), a hand-rolled product generator (fake has no commerce module), bounded dates (date/timestamp/dob/*_at recognised, recent windows — never "all of history"), the identifier family (id/code/ref/number, non-FK/non-PK) → unique sequential, and enum-ish names (role/status/type/…) left generic + a post-seed Hint advisory pointing at set … in (…). A set override clause= value / in (a,b,c) / as <generator> / between a and b (numeric and date), reusing ADR-0026 operators — answers the heuristic-miss case. --seed <n> makes runs reproducible (and enables exact-value tests). FK columns sampled uniformly from existing parent rows (empty parent → friendly error, no recursion v1); junction/compound-PK tables seeded with distinct combinations, capped + noted (SD1). A required-column block guard refuses rather than NULL-violate a NOT NULL column it can't fill (e.g. NOT NULL blob). Full ambient wiring (completion incl. a new generator-name vocabulary highlighted as tok_function, hints, help seed, ADR-0042 near-miss matrix, ADR-0027 validity); no DSL→SQL teaching echo (seed is a utility command, not a SQL twin). Honours X5do_seed reuses insert/update mechanics as helpers, not by emitting Command::Insert. Implementation phased: (1) core whole-row seed → (2) set overrides → (3) column-fill. Deferred (future SD2): recursive auto-seed, NULL injection, multi-locale, user-defined custom generators, full per-column report. Amendment 1, 2026-06-12 (issues #33/#34): two additive D7 catalogue rules — year-as-int (year/*_year/published/founded → a bounded int year, 19502025, or the dob-style birth window 19452007 for birth/born/dob; fixes nonsense like 9419; int-gated, after the quantity rule so year_count stays a count; two new YearRecent/YearBirth generators, not added to the D9 vocabulary) and conventional choice sets (priority/prio, severity, rating/stars → type-gated built-in PickFrom value sets reusing the existing generator; priority leaves ENUM_TOKENS). status is deliberately excluded (user-confirmed — values too domain-specific; keeps the D12 "don't guess" + advisory); a user IN-CHECK still wins. Website seed cast re-record tracked on the website branch
  • ADR-0049 — Input-field readline keymap: Esc-clear + Ctrl-A/E/W/K/U (I1b)Accepted + implemented 2026-06-12 (issue #29), closes Gitea #29 and the deferred I1b readline requirement. Amends ADR-0046, which listed "readline shortcuts (I1b)" as out-of-scope — that item is now in scope and decided here; orthogonal to ADR-0003's input-mode model and extends the I1a single-line cursor editing already shipped. Binds, in the input field (non-modal, non-nav, both modes): Esc clears a partly-typed command (empty buffer, cursor→0, scroll→0); Ctrl-A/Ctrl-E alias Home/End (line start/end); Ctrl-W deletes the previous word (readline-style — eats trailing whitespace then the preceding non-whitespace run, UTF-8-safe on char boundaries, only back to the cursor); Ctrl-K kills to end of line; Ctrl-U kills to start. Esc precedence: a live Tab-completion memo still wins (Esc undoes the completion first, ADR-0022; Esc clears only when no memo) — Esc-once backs out the completion, Esc-again clears. Forks all user-chosen: single-Esc-clears (not double-Esc — discoverable over accident-proof; an unsubmitted draft can be lost, a submitted line is always in history); the full I1b set (not just the issue's literal Ctrl-A/E + Esc); a new ADR (not an ADR-0046 amendment / no-ADR). Cursor-only keys (Ctrl-A/E) leave history navigation intact like Home/End; buffer-mutating keys (Esc-clear, Ctrl-W/K/U) end it like Backspace. Helpers clear_input/delete_prev_word/kill_to_end/kill_to_start in src/app.rs; 22 new Tier-1 tests, 2458 pass / 0 fail / 0 skip (1 ignored), clippy clean. OOS: on-screen keybinding hints (issue #27 owns surfacing per-focus keybindings in the bottom status line — this ADR makes the keys work, #27 makes them discoverable); demo-mode badges for the new chords (ADR-0047 follow-up — Esc already badges [ESC], the glyph-less Ctrl-chords are flagged but not added); multi-line input (I1); word-wise cursor motion (Alt-B/F) / transpose / yank
  • ADR-0050 — Incidental-DDL confirmations omit relationship info (structure-only)Accepted + implemented 2026-06-12 (issue #28), closes Gitea #28. Supersedes the incidental-DDL clause of ADR-0044 §1 and the relationship-block half of ADR-0016 §5. Incidental-DDL confirmation echoes (create table, add/drop/rename/change column, add/drop index) now render structure only — header + column box + Indexes: + constraints — with no References: / Referenced by: block (neither prose nor diagram), even when the table carries relationships the user did not touch. Rationale (owner): a confirmation echo reports the change just made, not untouched relationships; ADR-0044's terse prose was the lesser of "prose vs diagram", but the right answer for these surfaces is neither. Relationship-subject surfaces are unchangedshow table, add/drop relationship, show relationship still render ADR-0044 diagrams; relationships appear only when the user asks for (show table) or acts on (add/drop relationship) one, and are one show table <T> away — no information lost. Forks both user-chosen: scope = all incidental DDL (not just add column — the rationale is uniform, the mental model clean, and it's the simpler edit) and delete the prose renderer (not retain it dormant — no dead code). Mechanism: the handle_dsl_success matches! routing is unchanged (relationship-subject → diagrams; else → render_structure); the change is one line inside render_structure (output_render.rs — drop the relationship-block call) since all its callers are incidental DDL, plus deletion of the orphaned relationship_prose_lines + cols_disp helpers. The prose format survives in ADR-0016 §5 + git history for a future OOS-7 always-prose setting. Tests: the prose-presence unit test + its snapshot removed; a new unit test asserts render_structure on a description carrying both inbound and outbound relationships emits the box but no prose; the misnamed add_relationship_flow_shows_inbound_section_on_parent integration test (which sent an AddColumn) inverted + renamed to assert the add-column echo omits the prose; the diagram tests (show table, add relationship) unaffected. 2458 pass / 0 fail / 0 skip (1 ignored), clippy clean. requirements.md unaffected (ADR-tracked refinement of a decided area, like ADR-0044 itself)
  • ADR-0051 — Bottom keybinding strip: context- and state-awareAccepted + implemented 2026-06-13 (issue #27), closes Gitea #27. Repurposes the bottom status line into a keystrokes-only, state-selected strip (builds on ADR-0046 nav focus, ADR-0003 modes, ADR-0049 the #29 readline keys it now advertises, ADR-0022 the completion memo). A pure status_bar_bindings(app) -> Vec<(key,label)> chooses the strip by priority, first match wins: (1) sidebar focusCtrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input; (2) completion memo live (last_completion) → Tab/Shift-Tab cycle · Esc cancel · Enter run; (3) history navigation (new App::is_browsing_history() exposing the private history_cursor) → ↑↓ browse · Esc clear · Enter run; (4) editing (input non-empty) → Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run (surfaces the #29 keys, closing ADR-0049's deferred advertisement); (5) default (empty) → Ctrl-O sidebar · Tab complete · ↑ history · Enter run. Priority is correct because Up clears the completion memo and Tab cancels history nav, so states 2/3 never co-occur, and the five are exhaustive for Input focus. Typed-command words leave the strip (mode advanced/mode simple switch, : one-shot) and mode discovery moves to the empty-input hint (resolve_hint_lines), simple mode only: \mode advanced` for SQL(the verb "type" omitted — the prompt implies it; advanced mode shows **no** pointer per a post-trial user decision — a switcher knows how they got there andhelpcovers the way back). The one-shot's oldBackspace cancel one-shot label is subsumed by the editing state (behaviour intact). Forks all user-chosen: **editing state shows the #29 keys** (vs unadvertised); **Ctrl-C quitomitted** from the strip (vs always shown); **no width-drop machinery** — the longest strip (~65 cols) fits all supported widths, so a **width-budget unit test** keeps it lean by construction instead (the user's own observation). Catalog: 12 newshortcut.*labels + thepanel.hint_mode_advancedstring added toen-US.yaml+keys.rs (validator-checked 1:1), 5 now-dead strip strings removed. **Modal-aware strip is OOS** (pre-existing: a modal owns the keyboard and carries its own hints; the strip under it is unchanged-in-kind, not worsened). Tests: 9 Tier-1 unit (per-state key sets — completion/history driven through real key events; width budget; mode-pointer presence/absence), 1 Tier-3 rewritten (status_bar_is_keystroke_only_and_state_aware`), 15 full-panel snapshots re-accepted (reviewed — strip/hint only). 2467 pass / 0 fail / 0 skip (1 ignored), clippy clean. OOS: modal-aware strip; a full-key cheatsheet overlay; Ctrl-K/U advertisement (editing strip shows the highest-value subset within the width budget)
  • ADR-0052 — Mode-tagged history for cross-mode recallAccepted + implemented 2026-06-13 (issue #30), closes Gitea #30 — the feature (advanced history reusable in simple mode) and the bug in its comment (the : one-shot prefix lost across sessions). Amends ADR-0034 (status field gains a :adv tag; journaling moves from the worker to the dispatch layer), ADR-0015 §5/§6 (history.log leaves the worker transaction — commit-db-last now scopes yaml/csv/db only), and ADR-0040 (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. Root cause: history carried no mode, and the in-memory ring stored the raw :select 1 while the worker journalled the stripped select 1, so the : was lost on disk. Fix: record the submission mode per entry as a :adv suffix on the status token (ok/ok:adv/err/err:adv) — source stays last + canonical so replay is unaffected; the in-memory ring (still Vec<String>) stores advanced entries in their : -prefixed simple-mode runnable form (a leading : unambiguously marks advanced since simple DSL never starts with :); recall strips the : in advanced mode (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the : -prefix from the tag, so cross-session = in-session. The architectural turn (user's call): the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the failure path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So success journaling moved up to spawn_dsl_dispatch / run_replay / the app-command sites (next to the failure path), the worker's finalize_persistence now writes only yaml/csv, and the journal write became best-effort (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). App commands journal simple (dispatched outside the spawn) and submit excludes them from the ring's advanced flag, so undo/mode advanced recall bare. Forks user-chosen: status-tag format (vs 4th field / :-in-source); unified scope; dispatch-layer best-effort journaling (vs worker-coupled-fatal). Two /runda passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + :-reconstruct; app.rs recall matrix; the #30 cross-session regression in iteration6; replay tests cover run_replay journaling). 2471 pass / 0 fail / 0 skip (1 ignored), clippy clean. replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression). Follow-up done 2026-06-14: the vestigial worker source plumbing was fully unwound (compiler-guided, no behaviour change) — _source removed from finalize_persistence/do_rebuild_from_text, the three *_request wrappers inlined+deleted, the dead source param dropped from the ~30 forwarding worker handlers, and the source field removed from the DescribeTable/QueryData/RunSelect requests + their DatabaseHandle methods (~164 mostly-test call sites); the only worker source left is the snapshot/undo label (see ADR-0052 Consequences)
  • ADR-0053 — Contextual hint command and keybindingAccepted, implemented 2026-06-15 (Phases AD; closes A1 + requirements H2). Settles the hint slot ADR-0003 left "ADR pending"; closes the last open piece of A1 and tracks requirements H2. Two surfaces: an F1 keybinding that renders a deep hint for the live partial input without submitting (the primary path — a submitted hint command can't see the buffer it would help with, since Enter empties it), and a submitted hint command that expands on the most recent error. No topic argument (contextual only — help <topic> already owns explicit reference). Introduces a tier-3 teaching layer, deeper than the existing tier-1 (colour / error headline) and tier-2 (ambient one-liner; and the error hint:, which is shown by default since Verbosity::Verbose is the default — messages short is the opt-out); without it hint would just duplicate what's already on screen. Tier-3 content lives in the catalogue under hint.cmd.<hint_id> (per command form) and hint.err.<class> (per error/diagnostic class), each a structured what/example/concept block rendered via a new note_hint* family with OutputStyleClass::Hint. Keyed per-form via a new hint_ids: &[&str] field on CommandNode mirroring usage_ids (revised in Phase B): a per-node key proved too coarse — add/drop/show/create are each one node spanning many forms, and a live-input hint for add 1:n relationship must be specific to relationships; hint_key_for_input_in_mode reuses usage_key_for_input_in_mode's form-word disambiguation, and covers the advanced-SQL forms whose usage_ids are empty. Not keyed off help_id (it is None on the advanced-SQL nodes purely to dedup the help list; that parallel gap is issue #36). Clause-concept hints (on delete actions, constraint slots, with pk, cardinality) are a recorded deferred extension (hint.concept.<topic>, issue #37) — per-form is the right tier-3 granularity, with position-awareness owned by tier-2 + the live Next: line. Runtime translate_error classes resolve via stored last_error_hint_key (hint command / empty-F1). (The second route — pre-submit diagnostic.* read live from the walker on the F1 path — is deferred, issue #38: Diagnostic carries no class key.) Adds AppCommand::Hint, a HINT grammar node + REGISTRY entry, the hint_ids field, and last_error_hint_key; F1 is a read-only overlay (buffer + completion memo untouched). Content is the bulk of the work (the mechanism is ~a day): v1 scope = ~37 command forms + 9 runtime error classes (comprehensive for those, ~57 blocks), authored exemplars-first (voice approved in this ADR's /runda review, then mass-authored in batches), enforced by a comprehensiveness coverage test, with graceful fall-back to tier-2 if a key is ever missing. The pre-submit-diagnostic route + ~33 diagnostic.* blocks were deferred (issue #38) — Diagnostic carries no class key, so the route needs a broad change for marginal value (tier-2 already surfaces diagnostics; many duplicate runtime classes). Forks user-chosen: two-surface model; F1 (vs ? / a chord); no-arg; comprehensive-for-commands-and-errors scope; exemplars-first; diagnostics deferred. OOS: per-topic hint <topic> (rejected — overlaps help); always-on tier-3 (rejected — keeps ambient terse); non-en-US locales + success-command teaching (deferred); clause-concept hints (issue #37); the diagnostic route (issue #38); the help-side advanced-SQL gap (issue #36)
  • ADR-0054 — Release versioning policy + version surfaces (--version / version)Accepted + implemented 2026-06-16 (plan: docs/plans/20260616-public-availability.md, step 1 on the road to public availability; no prior issue/requirements.md item — an untracked gap). Fixes the tag↔crate-version decoupling: Cargo.toml built 0.1.0 while release.yaml named assets from the git tag, so a binary could report a version different from the asset it shipped in. Decision: Cargo.toml version is the single source of truth (read via env!("CARGO_PKG_VERSION"), no tag-injection); two surfaces report it through one cli::version_text() → catalog cli.version_line — a --version / -V CLI flag (mirrors --help, prints+exits in main.rs) and an in-app version command (REGISTRY node app::VERSION, AppCommand::Version, emits via note_system); and a release-CI version guard (release.yaml test job reads the [package] version from Cargo.toml and fails the release unless the v* tag equals v<version>; the guard's parse was later switched from cargo metadata | node to a grep on Cargo.toml after the former broke on the flake devShell's stdout banner). Release ritual: bump Cargo.toml → commit → tag → push. New keys cli.version_line + help.app.version + parse.usage.version + hint.cmd.version.{what,example} (the new REGISTRY command pulls in the comprehensiveness coverage gate). Rejected: tag-as-source (makes Cargo.toml lie). Deferred: git-hash/build-date enrichment (behind the same version_text() seam); UI placement beyond the command. Tested test-first: CLI parse (--version/-V/default-off), version_text() carries CARGO_PKG_VERSION, the in-app command parses + emits. Also corrected a stale release.yaml header comment ("macOS is deferred" → built by the dispatched release-macos.yaml).
  • ADR-0055 — curl | sh install script (scripts/install.sh)Accepted + implemented 2026-06-17 (plan: docs/plans/20260616-public-availability.md, step 2; tracked by plan + ADR, no Gitea issue — user decision). A one-line installer (curl -fsSL <gitea-raw>/scripts/install.sh | sh) so beginners don't hand-pick an asset + chmod +x. POSIX sh (shellcheck-clean), detects uname OS/arch → target triple (Linux → the fully-static *-musl build, macOS → *-apple-darwin; amd64/arm64 aliased; Windows rejected → Scoop/winget/releases page), resolves the version from the releases/latest API (or RDBMS_VERSION to pin), downloads the asset and its .sha256 and verifies it (mismatch aborts), installs to ~/.local/bin (RDBMS_INSTALL_DIR override) with a PATH hint. Testing seams: RDBMS_OS/RDBMS_ARCH + --print-target. macOS note: curl downloads aren't Gatekeeper-quarantined so the ad-hoc binary runs as-is (Developer-ID + notarization is the postponed signing task). Verified end-to-end against the live public v0.1.0 (all platform mappings, pinned + latest, checksum incl. tamper-rejection, install + run). Rejected: website-domain hosting (extra moving part; Gitea raw is simplest); deferred: uploading the script as a release asset, and a shellcheck CI gate (shellcheck isn't in the flake — touches ADR-ci-002). Amendment 1 (2026-06-17): added a Windows scripts/install.ps1 (irm | iex; maps host CPU → our *-windows-gnu/-gnullvm .exe, SHA-256-verifies, installs to %LOCALAPPDATA%\Programs\… + user PATH) — user chose both a one-liner and Scoop/winget; written but untested from this env (no PowerShell — validate on Windows).
  • ADR-0056 — crates.io publish-readiness + cargo binstall metadata (D3)Prepared 2026-06-17 (plan step 3a; tracked by plan + ADR). Makes the crate ready to publish to crates.io (user decision) and adds cargo-binstall metadata; the actual cargo publish is a gated, irreversible maintainer step. Manifest: drops publish = false; adds homepage (relplay.org), keywords, categories, and an exclude (/website,/docs,/.gitea,/.codegraph) trimming the crate from 585 files/8.3 MiB → 353/913 KiB compressed (code-only). Authors README.md (engine-neutral, simple/advanced-mode wording; install via curl|sh/binstall/source/prebuilt) and LICENSE-MIT (© Lazy Evaluation Ltd — confirm holder); the canonical LICENSE-APACHE is deferred to the maintainer (don't ship retyped legal text) — the SPDX license field already satisfies crates.io. binstall (syntax verified vs cargo-binstall SUPPORT.md): pkg-fmt = "bin" (bare binaries), pkg-url spelled v{ version } (the placeholder omits the v), plus per-target overrides mapping the common host triples to the assets we ship — *-linux-gnu → the static *-linux-musl build, *-pc-windows-msvc*-gnu/-gnullvm .exe (macOS matches directly; the docs promise no automatic fallback). Ordering: publish at a new tagged version whose release exists, after the release — not 0.1.0 (diverges from the already-released 0.1.0 binaries that predate --version). Verified: cargo publish --dry-run packages + verify-builds; cargo metadata confirms the binstall block + 4 overrides. Unverified: a real cargo binstall run (not a dep; nothing on crates.io yet) — validate at first publish. Rejected: cargo-dist (GitHub-centric). Maintainer follow-ups: confirm © holder, add canonical LICENSE-APACHE, real binstall validation. Amendment 1 (2026-06-18): 0.2.0 published live (crates.io; cargo install + cargo binstall verified — the unverified-overrides caveat is resolved), via a new manual workflow_dispatch workflow .gitea/workflows/publish.yaml (mirrors release-macos.yaml; tag input; cargo publish with a crate-scoped CARGO_REGISTRY_TOKEN secret). Publish stays manual by decision — irreversible (keeps the token off every tag push), the split release (tag Linux/Windows + dispatched macOS) makes a human the "all assets up" gate, and crates.io has no Gitea-Actions trusted-publishing path. Each registry is its own idempotent job (crates.io job no-ops if the version exists) so Scoop/Homebrew/winget can be added as sibling jobs without interfering. Amendment 2 (2026-06-19): Scoop + Homebrew wired (D3 §3b/§3c) as sibling publish.yaml jobs (scoop-bucket, homebrew-tap) that render manifests from the release .sha256 sidecars and push to org-level, multi-package repos lazyeval/scoop-bucket + lazyeval/homebrew-tap. Credential: a scoped bot user lazyeval-ci (Gitea PATs scope by permission-category, not per-repo, so an oli token would over-reach to the main repo) on a lazyeval org team with Write to the package repos only; its PAT is the LAZYEVAL_PKG_TOKEN secret on oli/rdbms-playground. Render scripts (scripts/render-{scoop-manifest,homebrew-formula}.sh) are dependency-free bash (CI image node:22-slim has no jq/ruby), tested by scripts/test-package-renders.sh. Scoop: #/-rename fragment + checkver, no autoupdate. Homebrew: on_macos/on_linux×arch bare-binary formula, no Windows. Unverified: real scoop/brew install, the HEAD:main branch assumption, macOS Gatekeeper-via-brew (ad-hoc sign). Remaining D3: winget.