6d54c1e96c
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.
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, idADR-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 framework — Amendment 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/Advancedmode and the:one-shot escape. The startup mode is no longer alwayssimple: it is restored from the project's stored mode and overridable with--mode(see ADR-0015 Amendment 1, issue #14). The app-command registry gainscopy(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;decimalstored as exact TEXT. Amendment 1, 2026-06-12 (issue #32): SQLite has no native decimal/BCD type, so arithmetic/aggregation over a TEXTdecimalis implicitly coerced to an IEEE-754 double and the computed (typeless) result leaked float noise (298.59999999999997for298.60); floating-point values are now rounded to 15 significant figures for display only (format_real_displayindb.rs, wired intoformat_cell— the result-set/show datacell 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_valueis a canonical identity key for the uniqueness dry-runs (dry_run_uniqueADR-0029 §5,check_uniqueness_collisionsADR-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 storedreal/decimalround-trips stay byte-exact and rawdecimalcolumns render verbatim - ADR-0006 — Undo snapshots and replay log — Accepted. 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 insrc/undo.rs, worker hook insrc/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 plusproject.yaml/data/*.csvas 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.redosupported, redo stack discarded on new work. Batch ops record one undo step (replay+ future batch via a Begin/EndBatch worker primitive);importis outside undo (it switches projects per ADR-0015 §11, leaving the current project untouched). A--no-undoCLI flag disables snapshotting (hardware escape hatch). Adds thebackupfeature torusqlite - 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 runtime — Amendment 1 (2026-05-31, issue #14): the input mode is per-project state in
project.yaml(a new optionalmode:key underproject:, alongsidecreated_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 (rebuildignores it) and not stored in the database: the persistence handle carries the current mode and the worker stamps it intoproject.yamlon 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 tosimple, no migration). New CLI flag--mode simple|advanced, precedence--mode> stored >simple; combines with--resume. Mid-sessionmodechanges persist viaAction::PersistMode→Database::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 rewritingproject.yamlon 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 theProjectSwitchedevent - ADR-0016 — Pretty table rendering for data and structure views
- ADR-0017 — Column type-change compatibility
- ADR-0018 — Auto-fill contracts for
serialandshortidcolumns - ADR-0019 — Friendly error layer (H1) and i18n message catalog
- ADR-0020 — Tokenization layer for the DSL parser — Superseded 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 removedchumskyentirely; 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_idson eachCommandNode, theparse.usage.*templates, and theavailable_commandsfallback all exist — but via grammar nodes, not thechumskyUsageEntryregistry /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_snapshotthreadMode;render_hint_panelcalls ambient for all modes (no more advanced-modeNone); 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; thetok_identifier/tok_keywordcolour split marks the boundary); shipped with awalk_repeatedfix 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-rungparse_command_in_modewas 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 usesparse_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 — bothNode::Ident.highlight_overrideand theWord.highlight_overridefield were dead (driver destructured the former to_,walk_wordhardcodedKeyword); now both wired through, with a newHighlightClass::Type+ eighthThemefieldtok_type(a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the threeIdentSource::Typesslots opt in viaSome(Type)(advanced-mode single-word SQL aliases —float,varchar, … per ADR-0035 §3 — ride along for free), and the two-worddouble precisionalias opts in via the newWord::type_keywordconstructor 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_linesnow pre-wraps prose andrender_right_columnsizes the panel to the line count (1 row default, up toMAX_HINT_ROWS=3, reclaimed when short) with aclamp_wrappedellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-charparse.usage.sql_create_tablesynopsis to a terse one-liner (full grammar remains inhelp.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;castdeliberately excluded as itsCAST(x AS type)syntax isn't a plain-call shape) as the single source of truth shared by two consumers at thesql_expr_identslot (ADR-0031 §1): issue #15 offers the functions as Tab candidates under a newCandidateKind::Function+ ninthThemecolourtok_function(a blue distinct from keyword/identifier/type, parallel to Amendment 4'stok_type) so a learner discoverssum/upper/…; issue #16 restores the typing-time column-typo flag the issue-#6 fix had dropped wholesale at this slot —invalid_ident_at_cursornow bails only when the partial prefix-matches a known function, else falls through to the schema-column check, soselect Agxwarns again at typing time whileselect sumdoes not (the issue-#6 lockdown tests + the submit-timeunknown_columndiagnostic 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): atseed <table> ▮the optional row count (a bareNumberLitwith no candidate) was invisible next to theset/--seedchips, and the resolver short-circuits on the already-complete command. Extends the issue-#4IntroProseHintMode(ADR-0024 §HintMode-per-node) to survive trailing optionals:walk_optionalstashes a skipped inner'sIntroProsekey into a newWalkContext.surviving_intro_hint(key + position) before the empty match clearspending_hint_mode, and the snapshot keeps it only when the skip position is the cursor (so it never leaks past a later-consumedset …clause or once the count is given); the resolver returns it ahead of the empty-expected short-circuit. The seed count is wrappedHinted{IntroProse("hint.seed_count")}; prose names the count (default 20), the.columncolumn-fill form, andset/--seed(user-chosen scope). OnlyIntroProseis carried;ProseOnly/ForceProseand the CREATE-TABLE element (a requiredRepeated) are untouched; noAmbientHint/renderer change - ADR-0023 — Unified declarative grammar tree — direction (superseded for execution detail by ADR-0024)
- ADR-0024 — Unified grammar tree: execution plan — Accepted, the executable spec — implemented (Phases A–F; Phase F shipped "minimal",
parser.rsretained as the router — see the ADR's Phase F implementation note) - ADR-0025 — Indexes — Accepted (Amendment 1, 2026-05-25: UNIQUE indexes admitted on the advanced-mode surface via
CREATE UNIQUE INDEX— ADR-0035 §4d; theIndexSchema.uniqueflag round-trips throughproject.yamlwith no new metadata table since the engine reports uniqueness natively; simple-modeadd unique indexstays deferred),add index/drop index, persistence, rebuild-table preservation, and items-list display (C3index portion +S2) - ADR-0026 — Complex WHERE expressions — Accepted, stratified recursive expression grammar (
AND/OR/NOT, comparisons,LIKE,IS NULL,IN,BETWEEN) forupdate/delete/show datafilters;show datagainswhere+limit; adds theSubgrammarnode and a recursiveExprAST (C5a) - ADR-0027 — Input-field validity indicator — Accepted, 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 aLIKE-on-numeric-column WARNING - ADR-0028 — Query plans (
EXPLAIN QUERY PLAN) — Accepted, anexplainprefix command overshow data/update/delete; an annotated, span-styled plan tree; introduces theOutputLinestyled-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 viaadd constraint …/drop constraint …; a pre-flight dry-run guards populated columns;CHECKreuses the ADR-0026 expression grammar viaSubgrammar(C3) - ADR-0030 — Advanced mode: the standard-SQL surface — Accepted, 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
Commandexecutor (metadata + type vocabulary preserved), DML andSELECTexecute as validated SQL; engine-neutral posture, the DSL→SQL teaching echo; supersedes ADR-0001'ssqlparser-rsreservation; phased plan (Q1/Q2/Q4); §13 OOS-2 (EXPLAIN of advanced SQL) superseded by ADR-0039 - ADR-0031 — The SQL expression grammar — Accepted, the stratified SQL expression grammar fragment commissioned by ADR-0030 §3: a single precedence ladder (
OR/AND/NOT, the comparison/LIKE/IN/BETWEEN/IS NULLpredicate set, arithmetic incl.||, function calls,CASE) — the superset of ADR-0026's DSLWHEREgrammar, 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'sSubgrammarrecursion + 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 thesql_expr_identslot (§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
SELECTgrammar — Accepted, the Phase-2 grammar commissioned by ADR-0030 §3: fullSELECTwithINNER/LEFT/RIGHT/FULL OUTER/CROSSjoins,GROUP BY/HAVING, all four set ops (UNION/UNION ALL/INTERSECT/EXCEPT),WITHandWITH RECURSIVECTEs,LIMIT … OFFSET,DISTINCT,t.*, and bare-alias projection (lifting Phase-1 §4.2); additive extensions to ADR-0031'ssql_exprfor scalar subqueries,IN (SELECT …),[NOT] EXISTS, and qualified column refs (redeeming ADR-0031 §7 OOS-1/OOS-2); grammar-recursion viaSubgrammar(&SQL_SELECT_COMPOUND)reuses ADR-0026'sMAX_SUBGRAMMAR_DEPTH = 64cap unchanged; softens ADR-0030 §8's "ambient assistance comes for free" claim: completion scope needs newWalkContextaccumulators (afrom_scope_stackofScopeFrames holdingfrom_scope/cte_bindings/projection_aliases), a new walker node variantNode::ScopedSubgrammar(&Node)as the push/pop trigger (existingNode::Subgrammarunchanged so DSLExprandsql_exprrecursion are unaffected), qualified-prefix completion narrowing, body-projection-derived CTE column resolution (soSELECT *and explicit-projection CTE bodies both yield real column completion pastcte_alias.|), and a post-walk fixup pass that re-resolves projection-list identifier highlighting/validity onceFROMis parsed (the projection-before-FROM problem); classifies every Phase-2 validation case against ADR-0027's ERROR/WARNING guideline (§11): five newdiagnostic.*keys for parse-time-detectable cases (unknown qualifier, ambiguous column, projection-alias misplaced, CTE/compound arity mismatch) plus eightengine.*translation keys; a MatchedPath-walking predicate-warnings variant that closes the Phase-1 gap where SQLWHEREexpressions emitted noLIKE-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.tomlgainscolumn_metadatato 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 ansrc/completion.rslook-ahead probe when the leading walk'sfrom_scopeis 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,oaliasingFROM Orders o) is no longer a blind spot — completion now offers each FROM source's qualifier (alias-if-present-else-table-name) at a baresql_expr_identslot (folded into the column candidate list; the alias source steps aside on an exact-qualifier partial so the diagnostic can surface), and thematched.len()==0bare-reference arm emits a targeteddiagnostic.alias_used_as_column/diagnostic.table_used_as_column("ois a table alias — writeo.<column>…") instead of the misleadingunknown_column(a drop-in replacement at the same span/Errorseverity, 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 DSLexpr_columnpath keepsunknown_columnsince the DSL has notable.columnsyntax) 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 asname.<column>); a genuine unknown column still reportsunknown_column - ADR-0033 — The full SQL DML grammar (
INSERT/UPDATE/DELETE) — Accepted (implemented + verified through sub-phase 3k, 2026-05-23; phase-exit reportdocs/handoff/20260523-phase-3-verification.md), the Phase-3 grammar commissioned by ADR-0030 §3: single- and multi-rowINSERT(incl.INSERT … SELECTrecursing through ADR-0032'sSQL_SELECT_COMPOUND),UPDATEwithSETassignment list,DELETE, all three optionally followed byRETURNING projection_list, plus fullON CONFLICT … DO NOTHING / DO UPDATEUPSERT on INSERT; fixes the DSL-vs-SQL dispatch architecture for shared entry words (insert/update/delete): SQL-first / DSL-fallback in Advanced mode via aChoice(SQL_shape, DSL_shape)per shape, gated by a new walker capabilityNode::Guard(fn)— a zero-byte-consumption gating node that fails the enclosing Seq with aValidationError; carriesCommand::SqlInsert/SqlUpdate/SqlDeletevariants anddo_sql_*worker handlers each of which knows the target table (for re-persistence) and thereturning: boolflag (for DataResult routing);shortidauto-fill mirrors the DSLdo_insertmechanism 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_mismatchERROR,auto_column_overriddenWARNING,not_null_missingWARNING) with positive + negative tests each; OOS list explicitly carves outDEFAULT VALUES(the project's planned seed feature), SQLite-specificOR REPLACE/OR IGNORE/OR ABORT/OR FAIL/OR ROLLBACKprefixes,UPDATE FROMmulti-table updates, and WITH-prefixed DML; theexcludedkeyword insideON CONFLICT DO UPDATEis 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-chosenNode::Guard(fn)+Choice(SQL_shape, DSL_shape)was found during 3a to be unworkable as framed (any guard-in-Choicemechanism forces awalk_choicechange —walk_choiceonly falls through onNoMatch, so Simple-mode valid-DSL would wrongly surface "this is SQL", andwalk_seqtreats aNoMatchpastidx 0as a hardFailed, breaking Advanced-mode DSL fall-through); replaced by category-grouped, mode-aware dispatch inwalker::walk(eachREGISTRYentry taggedCommandCategory::{Simple, Advanced}, generalising the existing whole-commandis_advanced_onlygate), shared entry words carrying a node in both groups, noNode::Guardand nowalk_choice/walk_seqchange, 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 byExpr-derived pre-count subqueries) and would have broken the §2 parity promise by reportingSET NULLthe DSL path doesn't; replaced by mirroringdo_delete's count-diff exactly (verbatim DELETE executes, child-count diff observes the cascade —ON DELETE CASCADErow removals only, SET NULL deferred for both paths to preserve parity), which shares the render-layer formatter for free viaCommandOutcome::Deleteand 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; noteupdate … --all-rowsdoes not fall back — the SQLSETexpression 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 anadvanced_mode.also_valid_sqlpointer ("… (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::Inserttyped-AST vsCommand::SqlInsertvalidated-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 callparse_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-wayMode: simple/advanced/advanced-one-shot threaded throughAction→worker, for mode-dependent output like echoing generated SQL) — today only the rendering side-channelOutputLine.mode_at_submissionexists, 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'supdate … --all-rowscounter-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, soupdate T set x=42 --all-rowswas silently parsed as the expression42 - -all - rowsover non-existent columnsall/rows(an ADR-0027 "flag-if-it-will-fail" case) and matchedSqlUpdate. Decision: the--all-rowssequence makes the SQLUPDATEshape fail, so dispatch falls back to the DSLUpdate { AllRows }— symmetry withdelete … --all-rows; no--comment feature introduced (trailing comments stay unsupported). Invertssql_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 (makesupdate … --all-rowsechoable); 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" meaninginput_verdict_in_mode(input, schema, Mode::Advanced) == Nonein the ADR-0027 sense (parse succeeds and no Warning/Error diagnostic from any pass). Surfaced by issue #1: a positionalINSERT 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) extendingdml_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 newdiagnostic.insert_arity_mismatch_form_bERROR per offending tuple, and (b) refactoringadvanced_alternative_noteto 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.logas a complete command journal; replay reads success-only — Accepted, resolves a three-way tension inhistory.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.lognever actually worked —run_replayparses 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 (thereplay_history_log_records_subcommands_onlytest only checks what replay writes, never replays the log as input). Decision:history.logbecomes a complete journal — every submission recorded, taggedok/errvia 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 readsokonly (and learns the journal format, while still accepting bare-command.commandsscripts; 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 journallederrbest-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 alwaysok") / §12 (hydration). Code deferred to two tracked test-first sub-tasks (journal-failures+filtering; replay-parses-journal-format); existing all-oklogs need no migration; implemented 2026-05-24 (plandocs/plans/20260524-adr-0034-history-journal.md); Amendment 1 (2026-05-24): replay filters out app-lifecycle commands — a workingreplay history.log(the §3 fix) exposed that the journal also recordssave 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 everyCommand::App+ nestedCommand::Replay; all skips continue (never abort — reversing the prior nested-replayrefusal, so a journal containing a once-runreplayneeds no hand-editing, and the infinite-loop footgun is closed by construction), with a[skip]warning onimportand nested-replayskips (their omission can leave replayed state incomplete) and silent skips for the rest;replay.error_nestedremoved,replay.skipped_import/replay.skipped_replayadded,ReplayCompletedcarrieswarnings - ADR-0035 — Advanced-mode SQL DDL — Accepted (design agreed 2026-05-24; validated end-to-end by sub-phases 4a/4a.2/4a.3/4b
CREATE TABLE(incl. foreign keys) + 4cDROP TABLE [IF EXISTS]+ 4dCREATE [UNIQUE] INDEX/DROP INDEX [IF EXISTS]+ 4eALTER TABLEadd/drop/rename column + 4fALTER TABLE … ALTER COLUMN TYPE+ 4gALTER TABLEadd/drop constraint + add FK + 4hALTER 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-neutralunique_<cols>name that reuses the existingDROP CONSTRAINT <name>grammar — no new syntax, no metadata, §4g anonymity intact;describeshows 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-modeCREATE/DROP/ALTER TABLE+CREATE/DROP INDEXget their own per-statement commands (SqlCreateTable/SqlAlterTable/SqlDropTable/SqlCreateIndex/SqlDropIndex), like DML'sSql*set — but unlike DML they execute structurally, not verbatim (raw execution would lose the playground's types, named relationships, andSTRICT; "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/dropreuse ADR-0033 Amendment 1's category-grouped mode-aware dispatch (SQL-first, simple fallback);alteris a new advanced-only entry word. Full surface (no pre-emptive cuts,Q4):CREATE TABLEwith column + table constraints, single/compoundPRIMARY KEY, inline + table-levelFOREIGN KEY→ named relationships (one statement = one command = one undo step, ADR-0006);ALTER TABLEadd/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 ofC1);CREATE [UNIQUE] INDEX/DROP INDEX. Type slot accepts the ten playground keywords and standard-SQL aliases (integer→int,varchar→text,timestamp→datetime, …; length args accepted-and-ignored; no engine type names in/out — ADR-0030 §5).CHECK/DEFAULTreuse ADR-0031sql_expr. Pre-implementation/rundarefinements (2026-05-24, user-confirmed):CREATE TABLE/DROP TABLEadmitIF [NOT] EXISTS(no-op-that-succeeds-with-a-note — a near-universal cross-vendor idiom, reclassified into scope, not engine-specific);INTEGER PRIMARY KEYmaps to a plainintPK, not auto-increment (serialstays 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-conversionopts in) while advanced mode performs it with a loss note and relies onundoas 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 PostgresUSINGclause, and the DSL→SQL teaching echo (ADR-0030 Phase 5). Sub-phases 4a–4i, plus 4a.2 (per-columnCHECK/DEFAULTvia rawsql_exprtext —sql_expris validate-only, noExprAST — + compositeUNIQUE(a,b); no new internal table) and 4a.3 (table-level/multi-columnCHECK, landed via the new__rdbms_playground_table_checksmetadata 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 — inlineREFERENCES+ table-levelFOREIGN KEY→ ADR-0013 named relationships in the create transaction, one undo step; self-references + bareREFERENCES <parent>supported, user-confirmed) and 4c (DROP TABLE [IF EXISTS]→SqlDropTable, reusingdo_drop_table;IF EXISTSis a no-op-with-note viaDropOutcome::Skipped) and 4d (CREATE [UNIQUE] INDEX [IF NOT EXISTS] [<name>] ON <T> (cols)→SqlCreateIndexandDROP INDEX [IF EXISTS] <name>→SqlDropIndex, reusingdo_add_index/do_drop_index;CREATE UNIQUE INDEXadmitted — ADR-0025 Amendment 1 — via an additiveIndexSchema.uniqueflag that round-trips throughproject.yamland rebuild, with[unique]markers in the structure view + items panel, while simple-modeadd unique indexstays deferred;IF [NOT] EXISTSreuses the 4c skip path;create/dropeach gain a second advanced node, exercising the all-candidates dispatch) and 4e (ALTER TABLEadd/drop/rename column →SqlAlterTable;alteris a new advanced-only entry word, runtime-decomposed to the existingdo_add_column/do_drop_column/do_rename_column— no new worker layer;do_add_columnextended to consume rawdefault_sql/check_sqlso 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--cascadespelling; the column executors +do_add_indexgained an internal-__rdbms_*-table guard — all user-confirmed) and 4f (ALTER TABLE … ALTER COLUMN TYPE→ a fourthAlterTableAction, runtime-decomposed to the existingchange_column_typewithChangeColumnMode::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, whileint → serialis 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 thetypekeyword (unique — ADD COLUMN's type is an ident), the type slot reusesSQL_TYPE; the internal-__rdbms_*guard was folded intodo_change_column_type, closing the simplechange columnexposure too — user-confirmed) and 4g (ALTER TABLE … ADD [CONSTRAINT <name>] (CHECK | UNIQUE | FOREIGN KEY)+DROP CONSTRAINT <name>; ADD = CHECK + composite UNIQUE + FK, withADD PRIMARY KEYand 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, bareREFERENCES <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 nullablenamecolumn on__rdbms_playground_table_checks(rebuild-only arrival — pre-4g projects gain it onrebuild, a named add on an un-upgraded project is refused with a friendly "rebuild first" message) and aproject.yamlcheck_constraintsextension to an{expr, name}mapping (the bare-string form still reads); the internal-__rdbms_*guard was folded intodo_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.age→U.age, a planning-/rundafinding — the engine rewrites the live CHECK but the stored text would drift and break a fresh rebuild;rewrite_check_table_qualifierkeeps them in step); grammar splits therenameverb into one branch with an inner Choice on a distinct second keyword (columnvsto), the new-name slot mirroring theCREATE TABLEname slot; refuses same-name / existing-target /__rdbms_*/ non-existent, with case-insensitive collision checks behind an engine-neutral pre-check (a finished-slice/rundafinding — 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 ofC1— 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;describeof 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 (4a–4i) 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) + anALTER COLUMNconstraint gap-fill surfaced by the ADR-0038 echo design: makes ISOALTER COLUMN … SET DATA TYPEthe canonical type-change verb withTYPEretained as a synonym (reverses §4f's "noSET DATA TYPE"), and addsSET/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-0029do_add_constraint/do_drop_constraintexecutors (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 DML — Accepted (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 1–2 implemented 2026-05-26 —INSERT … VALUESandUPDATE … SETliteral validation + offending-value retention, capture-at-parse with no grammar change; Phase 3a implemented 2026-05-26 — live typed-slot hints + numeric-shape highlighting forUPDATE/UPSERTSET col = <literal>via a boundary-aware lookahead (Amendment 1 corrects this ADR's naive-Choicesketch); Phase 3b implemented 2026-05-27 — per-position typed slots forINSERT … VALUES(single/multi-row, Form A/B) via a new zero-widthNode::SetColumnprimitive + 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 (InsertvsSqlInsert) stands. The problem (investigated 2026-05-26; characterization testsql_insert.rs::sql_dml_skips_app_level_value_validation_that_the_dsl_enforcesproves it): advanced-mode SQL DML gets none of the DSL's value feedback — a malformeddatelike2025/01/15is silently written, and the offending value is missing from constraint errors — because literal values are spliced into text and discarded (onlySTRICTstorage 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).WHEREnot validated (it's an expression in general; motivation met byVALUES/SET);SELECT/INSERT … SELECT/RETURNING/ON CONFLICTneed no special handling since execution is untouched. Phased: Phase 1 capture-at-parse + validate + retain forINSERT … VALUES(no grammar change, no reparse — closes both proven gaps); Phase 2UPDATE … SETliterals; Phase 3 completion hinting/highlighting (the only part needing a grammar change — a typed-literal slot vssql_exprreusing the DSLTypedValueSlots atdata.rs:141/189/269, discriminated by a boundary-aware lookahead not a naiveChoiceper Amendment 1; split into 3aSET(done) and 3bVALUES(pending); supersedes only Phase 1/2's literal detection, not the validation/enrichment on top). Non-goals: binding/reconstruction, collapsing command identity (Am3 stands), changingserial/shortidauto-fill (requirements.mdX4, a separate possible-bug), a structuralSELECT, a full SQL-expression AST. Embodiesrequirements.mdX5 (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: atuple_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 bareexpected,/)``);dml_insert_arity_diagnosticsis 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 keysinsert_arity_mismatch_form_b_simple/_all_auto; a wrong-count DSL insert now parsesOk+ 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-4790479cb(full Bucket A),275c726(Bucket B resolved-name + multi-statement renderers),e6ad1ae(the category-3--dont-convertcaveat — gated on this channel too), and2aab457(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-formcreate tableechoing the equivalent SQL when run in advanced mode, silent in simple — ADR-0030 §10, realised by ADR-0038). Introduces a new per-submission enumSubmissionMode{ Simple, Advanced, AdvancedOneShot } — refining Amendment 3's "widenMode" sketch: the persistent inputModestays two-way (mode.rskeeps 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 throughAction::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, generatedshortids, conversion counts) are worker-computed facts, and gating on mode means the work happens only when shown. Alternatives weighed + rejected: wideningMode(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-literalare ADR-0038, theALTER COLUMNgap-fill is the ADR-0035 amendment. Amendment 1 (2026-05-31, issue #10): the output tag is colour-coded by message status (itsOutputKind), 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,Echotag → 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 echo — Accepted (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
04c8e42shipped the channel + create-table slice, handoff-4790479cbthe full Bucket A expansion + a skeleton contract-gap fix (dropped per-columnDEFAULT/CHECK),275c726the Bucket B resolved-name + multi-statement renderers (auto- and user-namedadd index, positionaldrop index,add/drop relationshipin both selector forms,drop column --cascade,add relationship --create-fk),e6ad1aethe last category-3 line — thechange column --dont-convertcaveat (shortid + transform notes were already surfaced via pre-existingclient_side.*keys), and2aab457the §4 styled-runs polish: a newOutputKind::TeachingEchocustom rendering branch (dimmedExecuting SQL:prefix + the SQL re-lexed in advanced mode for token-class colouring, same as the input echo) plus a newOutputStyleClass::Hintfor 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 (theSubmissionModegate) and ADR-0035 Amendment 2 (standard-first dialect +ALTER COLUMNgap-fill). When a DSL-form command runs in advanced/one-shot mode, the worker emits the equivalent SQL beneath[ok]as a de-emphasised styledOutputLine(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 byfrom_sql_name, decision (a)); statement shape = the standard-first dialect (Am2). DML uses substituted literals, not?(per-typeValue → SQL-literal, round-trip-safe;blobmoot — no literal syntax exists; auto-gen columns omitted to matchdo_insert+ X4). Firing reality — a DDL +show datafeature: in advanced modeinsert/update/delete … whereare SQL-first (Sql*= already SQL = nothing to echo per §10); only DSL-only spellings echo (DDL +show data+ thedelete/update … --all-rowsfall-throughs — the latter via ADR-0033 Amendment 4, a bug-fix folded in here that reverses Amendment 3'supdate … --all-rowsmisparse). Three-category framework for "what happens beyond the literal SQL": (1) engine-implementation-hiding (the rebuild, rowid PK, non-PKserialMAX+1) — never surfaced; (2) decomposable into advanced SQL (drop column --cascade,--create-fkrelationship) — shown as the runnable multi-line sequence, one statement per line; (3) playground type-behaviour with no SQL-expressible form (shortidgeneration — noshortid(); type-conversion transforms — noUSING) — de-emphasised prose expansion from the worker'sclient_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, ablobliteral, 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 queries — Accepted (2026-05-27), implemented 2026-05-30 (issue #7), supersedes ADR-0030 §13 OOS-2. Lets
explainwrap the advanced SQL commands (Select/SqlInsert/SqlUpdate/SqlDelete, pluswith/CTE which builds aSelect) in addition to the DSLShowData/Update/Deleteit already covers (ADR-0028), runningEXPLAIN QUERY PLANover the validated SQL text through the existing ADR-0028 span-styled plan tree (advanced mode only; DSLexplainunchanged in both modes). Implemented via a secondAdvancedexplainCommandNode (EXPLAIN_SQL) registered under the sharedexplainentry word — reusing the establishedinsert/update/deleteshared-word dispatch (decide: SQL-first / DSL-fallback), soexplain show data …and DSL-only--all-rowsstill reach the DSL node; rejected aDynamicSubgrammarmode-gate (its resolution cache key omitsmode).build_explain_sqlslices the inner SQL off the source (excludesexplain) and reuses the existing SQL builders;do_explain_planruns the carried text verbatim, no params. Advancedexplain update/deletenow 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 line — Accepted 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 (andexplain selecteven rendered[ok] explainwith 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 existingrfind(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.summaryretired;dsl.failedreduced 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 clipboard — Accepted 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 allcopy the whole panel;copy lastcopies 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 pinsOutputLine::plain_texttorender_output_line.arboardadded--no-default-features(drops theimagecrate; X11-only on Linux —wayland-data-controldeliberately 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,screenpassthrough - ADR-0042 — H1a parse-error pedagogy in the grammar-tree era — Accepted 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 (38parse.usage.*templates), available-commands fallback, structural "after…, expected …" wording, source-derived ident slot labels ("table name"/"column name"), curatedparse.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 onWord/Punct/Flagpositions 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.mdT3[x]— the relationship model went list-based across six layers (single-column preserved, no migration), DSLfrom P.(a,b) to C.(x,y)+ SQLFOREIGN KEY (a,b) REFERENCES P(x,y)parse/execute/enforce, 12 tests intests/it/compound_fk.rs. Closes the open leg ofrequirements.mdT3: a foreign key that references a parent's compound primary key. A 2026-06-09 audit found single-column FK woven through ~15–20 sites (metadata table,RelationshipSchema,project.yamlRawEndpoint, 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-pairfk_target_typecompat (ADR-0011, element-wise); DSLfrom <P>.(a, b) to <C>.(x, y)(single form unchanged), SQLFOREIGN 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 conventionproject.yamlalready uses —columns: [a, b]likeprimary_key: [id]and indexcolumns: [...](the endpoint was the lone scalarcolumn:holdout); the metadataTEXTcolumns 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-changeproject.yamlwith 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.mdV1; second/rundapass over the implementation; §3 last-resort helper line considered and rejected). Resolves ADR-0016 OOS-1 and closes the open half ofrequirements.mdV1 ("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 subject —show 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, plaincreate table) keep the terse prose, via aDiagram|Proserender mode onrender_structure. Reading convention child(FK)-left / parent-right, arrow →,n…1cardinality, 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 inapp.rs(the worker only returnsTableDescriptions), a newApp::last_output_width(set fromui.rs) drives side-by-side vs a vertical-stack fallback + last-resort "runshow 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 endpointTableDescriptions. 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-relshow 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/rundaDA 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 relationshipconvenience command (C4) — Accepted + implemented 2026-06-10 (closesrequirements.mdC4; all forks user-confirmed + a/rundaDA pass that verified thedo_create_tablereuse against code and corrected the "no PK-less tables" assumption — advanced SQLcreate 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 (dispatcherdecide, 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 onsimple_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 reusingdo_create_table, which already takesforeign_keys+ writes relationship metadata — no batch bracketing). Forks all user-chosen: junction PK = compound-over-FKs (vs surrogate serial / no PK); referential actions =CASCADEon delete+update (vs NO ACTION / RESTRICT); naming = auto{T1}_{T2}+ optionalas(vs auto-only); available in both modes (Simple-category DSL, like the sibling relationship commands). FK columns named{parent_table}_{pk_column}(disambiguates sharedid; generalises to compound parents via ADR-0043), typed viafk_target_type(ADR-0011). A distinctCommand::CreateM2nRelationship(not lowered toCreateTable) preserves command identity (X5) and lets the teaching echo speak in m:n terms. Cross-cutting wiring enumerated: separateCREATE_M2NCommandNode(ownhelp_id/usage_ids),("m","m:n")completion composite,HintModes, grammar-driven highlighting,help/help create,parse_error_pedagogynear-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
9f5f76b…22bec61; 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 onAppnotSchemaCache(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 bucketsH<40compact (input 1 row + horizontal scroll / hint 2) vsH≥40comfortable (input 2 rows soft-wrap / hint 2), outputMin(5)honoured first under degradation; input gains horizontal scroll (input_scroll_offset, single logicalString— not 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 newApp.relationshipsfield (revised from the ADR's originalSchemaCache.relationship_detailsat implementation —SchemaCacheis walker-facing and needs only the names, kept inrelationships: Vec<String>; details are UI-only, soAppmirrorsapp.tablesand avoids ~23 fixture edits), delivered byDatabase::read_all_relationships+ anAppEvent::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-Oenters a focus cycle (Input → Tables → Relationships → Input;Escexits) orthogonal to the ADR-0003 input mode —Ctrl-Bwas rejected on review as the default tmux prefix (unreachable inside tmux); the focused panel expands to ~40–50 cols as aClearoverlay (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/≥40thresholds; 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/rundaDA pass drove these corrections: caught theCtrl-B/tmux collision, theSchemaCacheretype 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), sopanel_border_stylecarries noModifier::BOLDon 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
f879d54→2d0f4b2; norequirements.mditem — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build/rundapass that produced 10 tightening findings and a whole-implementation/rundapass that returned PASS, no blockers). An in-app demonstration mode (--demoflag /RDBMS_PLAYGROUND_DEMOenv, off by default, zero footprint when off) that renders two transient overlays soautocastscreencasts — 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 puredemo_badge_label(&KeyEvent); the badge auto-expires on a ~1.5 s timer that extends the runtime's existing time-boxed-recvarm condition (debounce.is_armed() || badge_pending; expiryInstantin the runtime,App.demo_badgethe render mirror — mirroring theinputvsinput_indicatorsplit). Step captions: a stealth, control-code-delimited input buffer toggled byCtrl+](byte0x1D→ arrives asChar('5')+CONTROL, verified against crossterm 0.29parse.rs:110-113; chosen overCtrl+!, 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),Backspaceedits, other keys inert, a secondCtrl+]commits to the caption box (empty commit dismisses); lives in pure-syncApp::update(), intercepted before the modal gate so captions/badges work over the load picker (the#24projects 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 (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a newApp.last_output_area: Rect(set inrender_output_panel) gives the top-level draw the anchor. Caption persists until the next keystroke; badge suppressed while capturing. Forks all user-chosen:--demoactivation (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 (--demoplumbing, badge set/suppressed, caption-without-input wiring), CLI (--demoparse + env fallback) — with an honest limit noted: thetokiotimer wiring insiderun_loopis exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existingIndicatorDebounce). One intentional, user-acknowledged behaviour:Ctrl-Cis 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 websitecasts.mjsscripts (website-branch follow-up). Implementation phased A (--demoplumbing) → B (badges) → C (captions) + a flat-rectangle restyle. Amendment 1 (2026-06-15): in demo mode,Ctrl-Galiases F1 (the ADR-0053 hint overlay) and badges as[F1], so anautocastcast — which can't emit F1's escape sequence — looks identical to a real F1 press;Ctrl-Gis the only fit (Ctrl+digit is unencodable in a legacy terminal, e.g.Ctrl-1arrives as a bare1), demo-gated so the shipped keymap stays F1-only; also cleaned two stray</content>/</invoke>lines from the ADR file. - ADR-0048 —
seedfake-data generation command — Accepted 2026-06-11; Phase 1 + Phase 2 implemented 2026-06-11 (Phase 1 commits202e25a→fbd219b; design settled with the user across an extended fork dialogue, hardened by a pre-build/rundapass (six blockers folded in), a post-implementation/rundapass (eight gaps closed — FK/shortid determinism so D4 holds with no exceptions, plus six untested ADR decisions), and a Phase-2 pre-build/rundapass (which caught the no-date-literal-token reality → the D2 quoted-dates amendment), and a post-implementation/rundapass (which added a friendly error for a bounded override on a UNIQUE column — see D2); 2400 tests pass, clippy clean). Closesrequirements.mdSD1 and the core of SD2; closes theseedhalf of A1. Phase 1 shipped: whole-rowseed <table> [count] [--seed <n>]with realistic name-aware generation (thefakecrate + a type-gated heuristic catalogue, table-context name disambiguation, hand-rolledproductgenerator, 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,--seedreproducibility (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): thesetoverride clause (D2 — fixed value / pick-list /as <generator>/betweenrange, 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 newKNOWN_GENERATORSvocabulary (D9), a rangeGenerator, 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. Closesrequirements.mdSD1 and the core of SD2; closes theseedhalf of A1 (the other beinghint/H2). A dedicatedseedcommand (own AST variant +do_seedexecutor, both modes) generating realistic, name-aware fake data. Two forms:seed <table> [count](new rows, default 20, capped) andseed <table>.<column>(fill a column on existing rows, an UPDATE). Generation adds thefakecrate (v5, English) driven by a type-gated, token-matched name-heuristic catalogue (~30 patterns, documented false-positive guards), with table-context disambiguating thename/titlefamily (products.name→product,users.name→person,vendors.name→company), a hand-rolledproductgenerator (fakehas no commerce module), bounded dates (date/timestamp/dob/*_atrecognised, 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 atset … in (…). Asetoverride 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 aNOT NULLcolumn it can't fill (e.g.NOT NULL blob). Full ambient wiring (completion incl. a new generator-name vocabulary highlighted astok_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 X5 —do_seedreuses insert/update mechanics as helpers, not by emittingCommand::Insert. Implementation phased: (1) core whole-row seed → (2)setoverrides → (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 boundedintyear, 1950–2025, or thedob-style birth window 1945–2007 forbirth/born/dob; fixes nonsense like9419;int-gated, after the quantity rule soyear_countstays a count; two newYearRecent/YearBirthgenerators, not added to the D9 vocabulary) and conventional choice sets (priority/prio,severity,rating/stars→ type-gated built-inPickFromvalue sets reusing the existing generator;priorityleavesENUM_TOKENS).statusis deliberately excluded (user-confirmed — values too domain-specific; keeps the D12 "don't guess" + advisory); a userIN-CHECK still wins. Websiteseedcast re-record tracked on thewebsitebranch - 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):
Escclears a partly-typed command (empty buffer, cursor→0, scroll→0);Ctrl-A/Ctrl-Ealias Home/End (line start/end);Ctrl-Wdeletes 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-Kkills to end of line;Ctrl-Ukills 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. Helpersclear_input/delete_prev_word/kill_to_end/kill_to_startinsrc/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 noReferences:/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 unchanged —show table,add/drop relationship,show relationshipstill render ADR-0044 diagrams; relationships appear only when the user asks for (show table) or acts on (add/drop relationship) one, and are oneshow table <T>away — no information lost. Forks both user-chosen: scope = all incidental DDL (not justadd 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: thehandle_dsl_successmatches!routing is unchanged (relationship-subject → diagrams; else →render_structure); the change is one line insiderender_structure(output_render.rs— drop the relationship-block call) since all its callers are incidental DDL, plus deletion of the orphanedrelationship_prose_lines+cols_disphelpers. 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 assertsrender_structureon a description carrying both inbound and outbound relationships emits the box but no prose; the misnamedadd_relationship_flow_shows_inbound_section_on_parentintegration test (which sent anAddColumn) 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.mdunaffected (ADR-tracked refinement of a decided area, like ADR-0044 itself) - ADR-0051 — Bottom keybinding strip: context- and state-aware — Accepted + 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 focus →Ctrl-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 (newApp::is_browsing_history()exposing the privatehistory_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 simpleswitch,: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-shotlabel 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 recall — Accepted + 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:advtag; journaling moves from the worker to the dispatch layer), ADR-0015 §5/§6 (history.log leaves the worker transaction —commit-db-lastnow 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 1while the worker journalled the strippedselect 1, so the:was lost on disk. Fix: record the submission mode per entry as a:advsuffix on the status token (ok/ok:adv/err/err:adv) —sourcestays last + canonical so replay is unaffected; the in-memory ring (stillVec<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 tospawn_dsl_dispatch/run_replay/ the app-command sites (next to the failure path), the worker'sfinalize_persistencenow 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) andsubmitexcludes them from the ring's advanced flag, soundo/mode advancedrecall bare. Forks user-chosen: status-tag format (vs 4th field /:-in-source); unified scope; dispatch-layer best-effort journaling (vs worker-coupled-fatal). Two/rundapasses (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 initeration6; replay tests coverrun_replayjournaling). 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 workersourceplumbing was fully unwound (compiler-guided, no behaviour change) —_sourceremoved fromfinalize_persistence/do_rebuild_from_text, the three*_requestwrappers inlined+deleted, the deadsourceparam dropped from the ~30 forwarding worker handlers, and thesourcefield removed from theDescribeTable/QueryData/RunSelectrequests + theirDatabaseHandlemethods (~164 mostly-test call sites); the only workersourceleft is the snapshot/undo label (see ADR-0052 Consequences) - ADR-0053 — Contextual
hintcommand and keybinding — Accepted, implemented 2026-06-15 (Phases A–D; closes A1 + requirements H2). Settles thehintslot 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 submittedhintcommand can't see the buffer it would help with, since Enter empties it), and a submittedhintcommand 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 errorhint:, which is shown by default sinceVerbosity::Verboseis the default —messages shortis the opt-out); without ithintwould just duplicate what's already on screen. Tier-3 content lives in the catalogue underhint.cmd.<hint_id>(per command form) andhint.err.<class>(per error/diagnostic class), each a structuredwhat/example/conceptblock rendered via a newnote_hint*family withOutputStyleClass::Hint. Keyed per-form via a newhint_ids: &[&str]field onCommandNodemirroringusage_ids(revised in Phase B): a per-node key proved too coarse —add/drop/show/createare each one node spanning many forms, and a live-input hint foradd 1:n relationshipmust be specific to relationships;hint_key_for_input_in_modereusesusage_key_for_input_in_mode's form-word disambiguation, and covers the advanced-SQL forms whoseusage_idsare empty. Not keyed offhelp_id(it isNoneon the advanced-SQL nodes purely to dedup thehelplist; that parallel gap is issue #36). Clause-concept hints (on deleteactions, 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 liveNext:line. Runtimetranslate_errorclasses resolve via storedlast_error_hint_key(hintcommand / empty-F1). (The second route — pre-submitdiagnostic.*read live from the walker on the F1 path — is deferred, issue #38:Diagnosticcarries no class key.) AddsAppCommand::Hint, aHINTgrammar node + REGISTRY entry, thehint_idsfield, andlast_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/rundareview, 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 + ~33diagnostic.*blocks were deferred (issue #38) —Diagnosticcarries 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-topichint <topic>(rejected — overlapshelp); always-on tier-3 (rejected — keeps ambient terse); non-en-USlocales + success-command teaching (deferred); clause-concept hints (issue #37); the diagnostic route (issue #38); thehelp-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.mditem — an untracked gap). Fixes the tag↔crate-version decoupling:Cargo.tomlbuilt0.1.0whilerelease.yamlnamed assets from the git tag, so a binary could report a version different from the asset it shipped in. Decision:Cargo.tomlversionis the single source of truth (read viaenv!("CARGO_PKG_VERSION"), no tag-injection); two surfaces report it through onecli::version_text()→ catalogcli.version_line— a--version/-VCLI flag (mirrors--help, prints+exits inmain.rs) and an in-appversioncommand (REGISTRY nodeapp::VERSION,AppCommand::Version, emits vianote_system); and a release-CI version guard (release.yamltestjob reads the[package]version fromCargo.tomland fails the release unless thev*tag equalsv<version>; the guard's parse was later switched fromcargo metadata | nodeto agrepon Cargo.toml after the former broke on the flake devShell's stdout banner). Release ritual: bumpCargo.toml→ commit → tag → push. New keyscli.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 sameversion_text()seam); UI placement beyond the command. Tested test-first: CLI parse (--version/-V/default-off),version_text()carriesCARGO_PKG_VERSION, the in-app command parses + emits. Also corrected a stalerelease.yamlheader comment ("macOS is deferred" → built by the dispatchedrelease-macos.yaml). - ADR-0055 —
curl | shinstall 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. POSIXsh(shellcheck-clean), detectsunameOS/arch → target triple (Linux → the fully-static*-muslbuild, macOS →*-apple-darwin;amd64/arm64aliased; Windows rejected → Scoop/winget/releases page), resolves the version from thereleases/latestAPI (orRDBMS_VERSIONto pin), downloads the asset and its.sha256and verifies it (mismatch aborts), installs to~/.local/bin(RDBMS_INSTALL_DIRoverride) with a PATH hint. Testing seams:RDBMS_OS/RDBMS_ARCH+--print-target. macOS note:curldownloads 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 publicv0.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 Windowsscripts/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 binstallmetadata (D3) — Prepared 2026-06-17 (plan step 3a; tracked by plan + ADR). Makes the crate ready to publish to crates.io (user decision) and addscargo-binstallmetadata; the actualcargo publishis a gated, irreversible maintainer step. Manifest: dropspublish = false; addshomepage(relplay.org),keywords,categories, and anexclude(/website,/docs,/.gitea,/.codegraph) trimming the crate from 585 files/8.3 MiB → 353/913 KiB compressed (code-only). AuthorsREADME.md(engine-neutral, simple/advanced-mode wording; install via curl|sh/binstall/source/prebuilt) andLICENSE-MIT(© Lazy Evaluation Ltd — confirm holder); the canonicalLICENSE-APACHEis deferred to the maintainer (don't ship retyped legal text) — the SPDXlicensefield already satisfies crates.io. binstall (syntax verified vs cargo-binstall SUPPORT.md):pkg-fmt = "bin"(bare binaries),pkg-urlspelledv{ version }(the placeholder omits thev), plus per-targetoverridesmapping the common host triples to the assets we ship —*-linux-gnu→ the static*-linux-muslbuild,*-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 — not0.1.0(diverges from the already-released 0.1.0 binaries that predate--version). Verified:cargo publish --dry-runpackages + verify-builds;cargo metadataconfirms the binstall block + 4 overrides. Unverified: a realcargo binstallrun (not a dep; nothing on crates.io yet) — validate at first publish. Rejected: cargo-dist (GitHub-centric). Maintainer follow-ups: confirm © holder, add canonicalLICENSE-APACHE, real binstall validation. Amendment 1 (2026-06-18):0.2.0published live (crates.io;cargo install+cargo binstallverified — the unverified-overrides caveat is resolved), via a new manualworkflow_dispatchworkflow.gitea/workflows/publish.yaml(mirrorsrelease-macos.yaml;taginput;cargo publishwith a crate-scopedCARGO_REGISTRY_TOKENsecret). 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 siblingpublish.yamljobs (scoop-bucket,homebrew-tap) that render manifests from the release.sha256sidecars and push to org-level, multi-package reposlazyeval/scoop-bucket+lazyeval/homebrew-tap. Credential: a scoped bot userlazyeval-ci(Gitea PATs scope by permission-category, not per-repo, so anolitoken would over-reach to the main repo) on alazyevalorg team with Write to the package repos only; its PAT is theLAZYEVAL_PKG_TOKENsecret onoli/rdbms-playground. Render scripts (scripts/render-{scoop-manifest,homebrew-formula}.sh) are dependency-free bash (CI imagenode:22-slimhas no jq/ruby), tested byscripts/test-package-renders.sh. Scoop:#/-rename fragment +checkver, noautoupdate. Homebrew:on_macos/on_linux×arch bare-binary formula, no Windows. Unverified: realscoop/brew install, theHEAD:mainbranch assumption, macOS Gatekeeper-via-brew (ad-hoc sign). Remaining D3: winget.