Merge branch 'main' into website (Gitea migration + ADR renumber)

Brings website up to date with main (18 commits): H1a parse-error
pedagogy, V5/H3/V5a show+help commands, ADR-0043 compound-PK FK,
handoffs 58-59, and the GitHub->Gitea doc scrub (Cargo.toml repository,
CLAUDE.md, ADR-0001 amendment, requirements).

Conflict: docs/adr/README.md. main and website had each created an
ADR-0042 (main: H1a parse-error pedagogy; website: public website &
docs site). Renumbered the website ADR to 0044 (next free after main's
0042/0043) and updated all references (ADR file, plan file, STYLE.md,
astro.config.mjs, README index). Website build verified green.
This commit is contained in:
claude@clouddev1
2026-06-09 20:28:27 +00:00
53 changed files with 4394 additions and 690 deletions
@@ -45,3 +45,25 @@ package managers (`cargo binstall`, Homebrew, Scoop, `winget`).
- TUI styling will require explicit work to match the polish that
Bubble Tea / Lipgloss give for free; budget for it in the design
pass.
## Amendment 1 — Distribution channel is open after the Gitea migration (2026-06-09)
The *Decision* block above assumed prebuilt binaries would ship "via
GitHub releases plus package managers." Since then the repository has
been migrated off GitHub to a self-hosted Gitea instance
(`git.lazyeval.net/oli/rdbms-playground`), and `tea` is the forge CLI
in use. The "GitHub releases" half of that sentence is therefore no
longer a settled assumption.
This amendment does **not** pick a replacement. Binary distribution is
not built yet (no release pipeline, no CI — `requirements.md` TT5/E*
remain open), so the channel for prebuilt binaries is an **open
choice** — Gitea releases, a GitHub mirror's releases, or both — to be
settled by a dedicated ADR when distribution is actually implemented.
The package-manager channels named in the Decision (`cargo binstall`,
Homebrew, Scoop, `winget`) are independent of the forge and are
unaffected.
(For the same supersede-don't-rewrite reason, the Decision block also
still names `sqlparser-rs`, which ADRs 00300036 replaced with a
hand-rolled grammar; that is recorded there, not by editing this ADR.)
@@ -2,7 +2,29 @@
## Status
Accepted.
**Superseded by ADR-0024** (2026-05-14). Accepted then superseded
without being implemented.
> **Superseding note (2026-06-03).** This ADR was never built. It
> specifies a `chumsky`-over-tokens architecture — a separate lexer
> producing `Vec<Token>`, a `define_keywords!` macro, and chumsky
> grammar combinators consuming `&[Token]`. ADR-0024 (unified grammar
> tree) instead adopted a **scannerless hand-rolled walker** that
> operates directly on source bytes, and **removed chumsky from the
> project entirely** (it is no longer a dependency). The lexer,
> `keyword.rs`, and the token model described below do not exist.
>
> What this ADR got *right* survives in ADR-0024: the
> expected-set aggregation it wanted (one branch's report no longer
> swallowing the others) is delivered by the walker's structural
> `expected` derivation, and the I3 (completion) / I4 (highlighting)
> hooks it anticipated are served by the same walker. Read ADR-0024
> for the architecture as built; this ADR remains as institutional
> memory of the path not taken and the reasoning that led there.
---
*Original status (historical):* Accepted.
Amends ADR-0001 (language and TUI framework) by adding a
tokenization layer between the source string and the chumsky
@@ -2,7 +2,37 @@
## Status
Accepted.
**Mechanism superseded by ADR-0024; H1a scope continued in ADR-0042.**
Accepted then superseded.
> **Superseding note (2026-06-03).** The *intent* of this ADR — surface
> the grammar of the command at the point of error, not just the next
> token — survived and is largely delivered. The *mechanism* did not.
> This ADR specifies a `chumsky`-based design: a separate `UsageEntry`
> registry in `src/dsl/usage.rs`, `parse.token.*` catalog keys driven
> by chumsky's `RichPattern<Token>` expected sets, and a renderer over
> chumsky output. ADR-0024 (unified grammar tree) replaced chumsky with
> a scannerless walker and **folded usage info onto the grammar nodes
> themselves**: `usage_ids` live on each `CommandNode`, the per-command
> `parse.usage.*` templates and the `parse.available_commands` fallback
> ship as designed here, and the expected-set vocabulary
> (`format_expectation` in `parser.rs`) renders directly from walker
> `Expectation` variants — no `UsageEntry` registry, no `parse.token.*`
> keys, no `src/dsl/usage.rs`.
>
> So: the §1 usage registry, §3 "deepest consumed keyword" mechanism,
> §4 `parse.token.*` catalog, and §7 validator details below describe
> code that does not exist. What shipped equivalently: §1's per-command
> templates (as `usage_ids` + `parse.usage.*`), §2's three-block render
> (echo+caret / structural error / usage), and §5's available-commands
> fallback. **ADR-0042 picks up H1a from here** — it records what is
> actually shipped and defines the remaining systematic-pass scope
> against the grammar-tree architecture. Read ADR-0042 for the live
> plan; this ADR remains as the design rationale for the pedagogy goal.
---
*Original status (historical):* Accepted.
Builds on ADR-0020 (tokenization layer). Addresses H1a from
`requirements.md` — the parse-error pedagogy gap that
@@ -0,0 +1,385 @@
# ADR-0042: H1a parse-error pedagogy in the grammar-tree era
## Status
**Accepted** — 2026-06-03.
Continues H1a (`requirements.md`) from **ADR-0021**, whose
chumsky-based mechanism was superseded by **ADR-0024** (unified
grammar tree). ADR-0021's *intent* — surface the grammar of the
command at the point of error, not just the next token — is
re-stated here against the architecture as actually built, with
an inventory of what already ships and a definition of done for
the remaining work.
Cross-references ADR-0019 (friendly-error layer + i18n catalog
conventions; H1a output shares the catalog), ADR-0022 (ambient
typing assistance, which shares the walker's expected-set
machinery), ADR-0024 (the grammar tree), and ADR-0009 (DSL
surface conventions; usage templates render in the documented
surface form).
## Context
### Why a new ADR rather than amending ADR-0021
ADR-0021 specifies a `UsageEntry` registry in `src/dsl/usage.rs`,
`parse.token.*` catalog keys, and a renderer over chumsky's
`RichPattern<Token>` expected sets. None of that exists. ADR-0024
removed chumsky from the project, deleted `usage.rs`, and folded
usage information onto the grammar nodes themselves. Amending
ADR-0021 in place would force every reader to mentally translate a
dead mechanism; a fresh ADR records the live state directly.
ADR-0020 and ADR-0021 keep their superseding notes and remain as
institutional memory.
### What H1a is
When a learner types something near-correct, the error should
*name the missing keyword or clause* and *show the shape of the
command*, rather than point a caret at the unexpected character.
The user-reported gap: typing `create` once produced
`parse error: after \`create\`, expected \`table\`` — structurally
true, pedagogically silent.
### What already ships (the baseline — do not re-build)
Verified against code on 2026-06-03. The grammar-tree migration
delivered most of ADR-0021's intent through different machinery:
1. **Per-command usage block.** Every `CommandNode` carries
`usage_ids: &'static [&'static str]`
(`src/dsl/grammar/mod.rs`). On any parse error the renderer
emits a `usage:` block listing every form of the matched
command family — 38 templates under `parse.usage.*`
(`src/friendly/strings/en-US.yaml:499-571`), resolved by
`grammar::usage_keys_for_input` and rendered by
`render_usage_block` (`src/app.rs:2560`).
2. **Available-commands fallback.** When no command keyword was
consumed, the block becomes
`available commands: …` (`parse.available_commands`,
`en-US.yaml:493`; `app.rs:2593`).
3. **Structural error names the consumed prefix and expected
set.** `format_walker_error` (`src/dsl/parser.rs:289`) renders
`after \`<consumed>\`, expected <set>, found <token|end of
input>`, distinguishing incomplete-at-EOF (`at_eof = true`,
more input would help) from a definite mid-input mismatch.
4. **Friendly slot labels for identifiers.** `format_expectation`
(`src/dsl/parser.rs:262`) renders `Ident` slots by source —
"table name", "column name", "relationship name", "index
name", "type" — instead of a bare "identifier" (ADR-0022 stage
8c).
5. **Curated custom messages** for high-value near-misses under
`parse.custom.*` (`en-US.yaml:443-478`): `create_table_needs_pk`,
`insert_form_a_missing_values` ("looks like Form A — add
`values (...)`"), `change_column_flags_exclusive`,
`bind_type_mismatch`, the redundant-constraint and
alter-add-primary-key cases, etc.
6. **Schema-aware pre-flight diagnostics** that light the
`[ERR]` validity indicator *at typing time* (ADR-0027 /
ADR-0033 / ADR-0036): INSERT arity for Forms A/B/C, unknown
table/column, type mismatch, `= NULL`, NOT-NULL-missing, and —
on the advanced-SQL surface — `cte_arity_mismatch`,
`compound_arity_mismatch`, and `projection_alias_misplaced`
(`diagnostic.*`, `en-US.yaml:577-620`; walker logic in
`src/dsl/walker/mod.rs`).
7. **Ambient "Next:" hints** and the **simple→advanced cross-mode
pointer** (ADR-0022 / `advanced_alternative_note`,
`src/input_render.rs`).
So H1a is *substantially* delivered at the intent level. The
handoff's two canonical examples already behave: `insert into T
('Oli')` → custom Form-A message; `update T set x=1` → structural
"expected `where` or `--all-rows`" + usage block.
### What remains — the genuine gap
The remaining work is **systematic verification plus targeted
polish**, not a missing feature:
- **No enumerated coverage guarantee.** Coverage is curated
case-by-case; nothing asserts that *every required slot in every
command* produces a pedagogically-sound near-miss message.
- **Literal expectations render terse.** `Word`/`Literal`/`Punct`/
`Flag` slots come out as backticked literals (`` `where` ``,
`` `=` ``, `` `--all-rows` ``). Correct, but a learner is helped
more by a short prose gloss in select high-value positions.
- **Advanced-mode SQL parse pedagogy is thinner** than the DSL
surface (RETURNING scope, CTE-arity diagnostic positioning,
`CROSS JOIN … ON`, INSERT…SELECT column-count). No other ADR or
open issue covers this (ADR-0019 §OOS-2 covers advanced-SQL
*engine-error sanitisation* — a different layer).
## Decision
### 1. Definition of done — a verified near-miss matrix
H1a is "done" when there is a test matrix that, for **every
command in the REGISTRY**, exercises its salient near-miss inputs
and asserts the rendered output reads pedagogically. "Salient
near-misses" per command means at minimum:
- the bare entry keyword alone (`create`, `add`, `update`);
- each required clause omitted (e.g. `update T set x=1` with no
filter rail; `insert into T (cols)` with no `values`);
- a wrong token where a specific slot is expected (e.g. a number
where a table name belongs);
- the zero-prefix / unknown-command case (available-commands
fallback).
The matrix lives in the existing surfaces — `tests/typing_surface/`
(snapshot-based, the standalone `typing_surface_matrix` binary) for
the typing-time hint/validity view, and
`tests/it/parse_error_pedagogy.rs` (the consolidated `it` binary)
for the submit-time rendered three-block output. New integration
tests go in `tests/it/` per the handoff-57 §3 layout rule — **not**
as new top-level `tests/*.rs`.
Work is **test-first**: add the matrix entry, observe the current
rendering, and only then adjust wording/labels where it reads
poorly. A near-miss whose current rendering is already good is
locked by a snapshot, not rewritten.
### 2. Friendlier literal expectation labels
`format_expectation` gains, for high-value keyword/punct positions,
an optional prose gloss while **always keeping the exact literal
visible** — a learner must still see the precise token to type.
The principle: a label may *add* role context, never *replace* the
literal.
Illustrative target (final wording settled per-case against the
matrix, as is normal for pedagogical text):
- `expected \`where\` or \`--all-rows\`` →
`expected a filter clause: \`where …\` or \`--all-rows\``
- `expected \`values\`` (after a Form-A column list) →
already covered by `parse.custom.insert_form_a_missing_values`;
the matrix confirms it fires.
Mechanism (illustrative, finalised at implementation time): a
grammar `Word`/`Punct` node may carry an optional expectation-label
key, mirroring how `Ident` slots derive a label from
`IdentSource`. Absent an override, rendering is unchanged (the
backticked literal). This keeps the change additive and per-slot —
no blanket reword that would churn the anchor-phrase tests
needlessly.
New glosses are catalog-sourced (`parse.expect.*` or reuse of
`parse.usage.*` fragments — chosen at implementation time) so
wording stays in `en-US.yaml`, not in code, consistent with
ADR-0019.
### 3. Advanced-mode SQL parse pedagogy — in scope
The same matrix discipline (§1) extends to the advanced-mode SQL
surface. Two of the relevant arity diagnostics **already exist** and
must not be re-built — `cte_arity_mismatch` and
`compound_arity_mismatch` (`en-US.yaml:590-591`); for these the work
is matrix coverage and, for CTE, auditing whether the diagnostic is
*positioned at the CTE name* (easiest to fix) rather than the body.
The remaining items were re-checked empirically at implementation
time (2026-06-05) and **most turned out already handled** — see the
Implementation-outcome section's advanced-SQL paragraph for the
corrected picture. The `:` one-shot escape (a simple-mode line run
once in advanced mode) is part of the advanced surface and is
covered by the mode-aware usage threading (G3).
This stays clear of ADR-0019 §OOS-2 (advanced-SQL *engine-error*
sanitisation): §OOS-2 reworks errors raised by *executing* SQL;
H1a here concerns errors raised while *parsing* it. If a near-miss
turns out to be an engine error rather than a parse error, it is
out of H1a scope and noted against §OOS-2 instead.
### 4. Catalog and anchor-phrase discipline
All new or reworded user-facing strings go through the i18n catalog
(`en-US.yaml`) and the `KEYS_AND_PLACEHOLDERS` validator, per
ADR-0019. No engine vocabulary in any string (CLAUDE.md).
Two anchor styles constrain §2's glosses and both are preserved by
its "literal always visible" rule:
- The **substring assertions** in `src/dsl/parser.rs` tests
("after ``", "expected table name", "found end of input",
"unknown type", "expected one of").
- The **substring assertions** in `tests/it/parse_error_pedagogy.rs`,
which check for backticked literals and usage fragments
(e.g. `` `column` ``, `` `1` ``, "create table", "with pk"). This
test is `.contains()`-based, not snapshot-based, so a §2 gloss
that dropped the bare literal would fail it — which is precisely
the regression §2's rule prevents.
The snapshot-based `tests/typing_surface/` matrix will re-baseline
on any §2 wording change (expected; reviewed via `cargo insta`),
but the two substring suites above must stay green without edits to
their assertions.
## Implementation outcome (2026-06-05)
The baseline capture (§Implementation notes step 1) triaged four
gaps; all four are fixed test-first, locked by the near-miss matrix
in `tests/it/parse_error_pedagogy.rs`:
- **G1** — the bare `1` cardinality literal opening `add 1:n
relationship …` rendered cryptically. Render it as
`` `1:n relationship` `` in `format_expectation` (error wording
only; completion still offers the literal `1`).
- **G2** — bare `select` dumped the 14-item expression first-set.
Collapse it to "a projection: `*`, a column, or an expression"
in `format_walker_error`, detected by the `distinct`+`all`
quantifier pair being *jointly* expectable — a signature unique
to a projection start (empirically verified not to misfire at
`count(`, `union`, `union all`, `select distinct`, or mid-list).
Render-only; the completion/hint layer still expands the full
set.
- **G3** — the usage block was mode-blind (`render_usage_block`
resolved shared entry words to the first-registered Simple node).
`usage_key(s)_for_input` gain mode-aware `_in_mode` variants.
**Decision (user-confirmed, after the DA pass).** In advanced
mode the DSL forms remain *valid input* via fallback — verified:
`create table Foo with pk`, `drop column from table T: c`,
`drop relationship r`, `add column …` all parse and dispatch in
advanced mode. So the advanced usage block shows **every form
valid in the mode, mode-primary (SQL) first, then the DSL
fallback forms** — a usage hint must never hide input that works.
(An initial implementation that showed SQL-only was flagged by
the DA pass as hiding `create table … with pk` / `drop column …`
and corrected.) Simple mode shows DSL forms only — the SQL-only
forms hit the "this is SQL" rail and are unreachable.
- **G4** — `with` borrowed `select`'s usage; it gains its own
`parse.usage.with` CTE template.
**Advanced-SQL pedagogy (§3) — empirical re-check (2026-06-05).**
§3 (drafted from a code survey) listed `RETURNING` scope,
`CROSS JOIN … ON`, and INSERT…SELECT column-count as absences.
Verifying each against the running app **reversed two of three**:
- **INSERT…SELECT column-count** is *already handled* — a count
mismatch fires `verdict = Error` with "the column list names N
column(s) but M value(s) are given" (walker test
`insert_select_arity_mismatch_fires`). It is a structural
list-vs-list check, so it fires even without a schema. Not a gap.
*Caveat (pre-existing, not addressed here):* a `SELECT *`
projection is not expanded for arity, so `insert into T (one_col)
select * from Multi` is not pre-caught — the engine rejects it at
execution. Star-expansion for pre-flight arity would be a separate
enhancement (and brushes ADR-0019 §OOS-2 engine-error territory).
- **RETURNING scope** is *already handled* — at a bare `returning`
position completion offers the table's columns; `returning
<unknown>` fires the `unknown_column` diagnostic. Not a gap.
- **`CROSS JOIN … ON`** *was* a genuine residual: the grammar
rejects the `on` but the structural error said only "expected end
of input". **Fixed** — `parse.cross_join_no_on` renders "a CROSS
JOIN has no ON clause — …" when the failing token is `on` and the
most recent consumed join is a CROSS join (a precise signature:
every other join requires `on`, so there `on` is expected, not a
failure). Render-only, no grammar change; two misfire guards
(plain join still asks for `on`; a stray `on` with no join does
not fire). The CTE/compound arity diagnostics noted above remain
present and correct.
**Known low-priority residual (user-confirmed to defer).** At
*submit* time, an incomplete expression position that is not a
SELECT projection (bare `where `, `returning `, `having `, `set
col=`) still renders the raw ~14-item expression first-set; only the
SELECT projection is glossed (G2, keyed on the `distinct`+`all`
quantifier pair). This is low-impact because *typing*-time
completion already offers the correct candidates (columns,
functions, expression keywords) at those positions. Generalising the
gloss was considered and deferred — the payoff is small and a
broader render-side collapse adds misfire surface.
Coverage: the matrix covers, in both modes, every entry word's bare
/ missing-clause / wrong-token near-misses, the app-lifecycle
trailing-junk cases, **and** the committed *multi-form* variants
(`add index` / `add constraint` / `add 1:n relationship`, `drop
index` / `drop constraint` / `drop relationship`, `show table`,
`change column …`, `create index`, `alter table … add` / `… drop`).
The committed forms were audited 2026-06-05 and each renders its own
form-specific missing-keyword message + usage (e.g. `add index` →
"expected `on` or `as`"; `drop constraint` → "expected `not`,
`unique`, `default`, or `check`"), regression-locked in
`near_miss_matrix_committed_multiforms`.
## Out of scope
1. **Advanced-SQL engine-error sanitisation** — ADR-0019 §OOS-2.
2. **Tab completion (I3) and syntax highlighting (I4)** as
features — they share the walker but are separate ADRs.
3. **Schema-aware "did you mean `Customers`?" spell-correction** —
ADR-0021's out-of-scope §2; belongs with I3.
4. **Multi-error reporting.** The walker reports the first error
and stops; unchanged.
5. **`messages`-style verbosity gating of the usage block.** Per
ADR-0021 §8 the usage block is always shown; parse errors are
exactly when pedagogical surface should be maximal. Unchanged.
6. **Auto-generating usage/help text from the grammar.** ADR-0024
left help prose hand-curated; templates stay hand-written.
## Consequences
### Positive
- H1a gains an explicit, enumerated definition of done instead of
an open-ended "systematic pass still pending".
- The matrix becomes a regression lock: future grammar changes
that degrade a near-miss message fail a snapshot.
- Literal-label glosses close the last terse-wording gap without a
blanket reword.
- The advanced-SQL surface reaches parity with the DSL surface for
the audience that has switched to raw SQL.
### Costs
- Wording iteration across many near-miss cases — but cheap,
catalog-driven, and snapshot-guarded.
- The §2 per-node label field is one more annotation a new command
may set (optional; default unchanged).
- Snapshot volume grows; acceptable given the existing ~160-entry
typing-surface matrix.
### Neutral
- No public API change. `parse_command*` signatures, the
`ParseError` shape, and the three-block render path are all
unchanged; this ADR adds wording, labels, and tests within them.
## Implementation notes
Order of operations (test-first throughout):
1. Enumerate the per-command near-miss matrix (§1) as failing/asserting
tests in `tests/typing_surface/` + `tests/it/parse_error_pedagogy.rs`.
Capture current rendering as the starting baseline.
2. Triage: which entries read poorly? Only those get wording work.
3. Add the optional expectation-label mechanism (§2) and apply it
to the high-value keyword/punct positions surfaced in triage.
4. Advanced-SQL near-miss audit + fixes (§3), distinguishing parse
from engine errors as they arise.
5. Catalog validator + anchor-phrase checks stay green (§4).
6. Update `requirements.md` H1a with the matrix as the done-marker;
flip to `[x]` only when the matrix is complete and green.
## See also
- ADR-0021 — Parser-as-source-of-truth for H1a (mechanism
superseded; intent continued here).
- ADR-0020 — Tokenization layer (superseded by the scannerless
walker).
- ADR-0024 — Unified grammar tree (the architecture H1a is built
on).
- ADR-0022 — Ambient typing assistance (shares the expected-set
machinery).
- ADR-0019 — Friendly-error layer and i18n catalog (§OOS-2 is the
adjacent engine-error scope).
- ADR-0009 — DSL command-syntax conventions (usage surface form).
- `requirements.md` — H1a tracking entry.
@@ -0,0 +1,339 @@
# ADR-0043: Compound-primary-key foreign-key references (T3)
## Status
**Accepted + implemented** — 2026-06-09. Implementation landed the
same day: the relationship model went list-based through all six
layers (refactor commit `b14f019`, single-column preserved), then
the DSL + SQL grammars gained multi-column parsing and the
executor the full-PK/auto-expand/per-pair-type-compat/auto-name/
`--create-fk`-per-column logic. Verified by 12 integration tests in
`tests/it/compound_fk.rs` (parse both surfaces, engine-enforced FK,
arity + partial-PK refusal, `--create-fk`, single-column
preserved) on top of the existing single-column relationship
suite. `requirements.md` **T3** is `[x]`. All four genuine forks
confirmed by the user at the recommended option: **F-A** full PK in order, **F-B**
house-style uniform column lists (no migration; back-compat not
required), **F-C** parenthesized DSL lists, **F-D** bare table-level
SQL FK auto-expands to the parent's full PK. Closes the one open
leg of
`requirements.md` **T3** ("compound primary keys handled
end-to-end (DSL, storage, display, **FK reference**)"): a foreign
key that *references* a compound (multi-column) primary key.
Cross-references **ADR-0011** (FK column type compatibility —
`Type::fk_target_type`), **ADR-0013** (relationships, naming, the
rebuild-table strategy, and the `__rdbms_playground_relationships`
metadata table), **ADR-0035 §4b** (the SQL `FOREIGN KEY` surface),
**ADR-0004 / ADR-0015** (`project.yaml` as the authoritative
format; `playground.db` is a derived artifact), and **ADR-0009**
(DSL surface conventions).
## Context
Compound PRIMARY KEYs are declared, stored, and displayed today
(`create table T with pk a(int), b(int)``primary_key:
Vec<String>`). The missing leg is the *reference*: a child table
whose foreign key points at a parent's compound PK. A 2026-06-09
codebase audit found single-column FK is a pervasive assumption —
~1520 sites across 6+ files:
- **Metadata** — `__rdbms_playground_relationships` stores scalar
`parent_column TEXT` / `child_column TEXT`
(`PRIMARY KEY (child_table, child_column)`).
- **Persistence** — `RelationshipSchema { parent_column: String,
child_column: String }`; `project.yaml` `RawEndpoint { table,
column }`.
- **Grammar** — `add 1:n relationship … from <P>.<col> to
<C>.<col>` (one ident per side); SQL `FOREIGN KEY (<col>)
REFERENCES <P>(<col>)` (parens that hold exactly one ident).
- **AST** — `Command::AddRelationship { parent_column: String,
child_column: String }`; `SqlForeignKey { child_column: String,
parent_column: Option<String> }`.
- **Executor** — `schema_to_ddl` emits a single-column
`FOREIGN KEY (c) REFERENCES P(p)`; `check_fk_type_compat`
compares one parent type to one child type; bare
`REFERENCES <P>` on a compound-PK parent is refused as
ambiguous (`resolve_create_table_fks`,
`do_alter_add_foreign_key`).
- **Display** — `RelationshipEnd { other_column: String,
local_column: String }`.
This is not a sweep-sized change, which is why it earns an ADR
rather than an inline build. The decisions below also turn the
audit's worst-case framing (a metadata-schema + yaml-format
migration via the F3 framework) into a **no-migration** change.
### Why no migration is needed
**Decision input (user, 2026-06-09): back-compatibility with
existing saved projects is not required.** The project is
pre-release; there is no installed base of `project.yaml` /
`playground.db` files to preserve. This removes the only force
that would have demanded an F3 migrator or a version bump, and —
more importantly — it lets the representation be chosen for
*cleanliness and consistency* rather than for byte-identical
back-compat. The consequence is explicit and accepted: a
`project.yaml` written before this change that contains
relationships will not load under the new format.
Freed of back-compat, the storage follows the convention the file
**already uses** for ordered column lists rather than inventing a
new one:
- `project.yaml` already writes `primary_key: [id]` (a compound PK
is `primary_key: [a, b]`) and index `columns: [a, b]`
(`RawIndex { columns: Vec<String> }`). The relationship endpoint
is the lone multi-column-capable slot still using a scalar
`column:`. It joins the house style (D5).
- The metadata columns are `TEXT`; SQLite has no array type, so a
list lives in a text cell as JSON regardless. That JSON is now a
*uniform* encoding (a one-element array for the single-column
case), not a "bare-name-or-JSON, sniff which" fallback — the
fallback only existed to keep old rows identical, which is no
longer a goal.
So this is not a clever back-compat dodge; it is "use the existing
list convention, uniformly." No version bump, no F3 migrator.
## Decision
Support a foreign key that references a parent's **full** compound
primary key, matched **positionally** to an equal-length child
column list, with per-pair type compatibility — across both the
DSL and SQL surfaces — using format-flexible storage that needs no
migration.
### D1 — Matching policy: the full PK, in order
A compound-PK FK references **all** columns of the parent's
primary key, in PK declaration order, matched 1:1 to the child's
column list (same length). Referencing a *subset* of a compound PK
is **out of scope**: SQL/SQLite require FK parent columns to form a
PK or UNIQUE key, and a strict subset of a compound PK is not
itself unique unless separately constrained. Teaching-clean rule:
*a foreign key to a compound key names every column of that key.*
A length mismatch (child supplies N columns, parent PK has M ≠ N)
is a friendly error naming both counts.
### D2 — Type compatibility: per pair, positional
Each child column's type must satisfy
`parent_pk_col.fk_target_type() == child_col` for the
corresponding pair (the existing ADR-0011 rule, applied
element-wise in order). `check_fk_type_compat` generalises to walk
the pairs and report the **first** offending pair with the same
wording it uses today.
### D3 — DSL syntax: parenthesized column lists
`add 1:n relationship [as <name>]
from <P>.(<a>, <b>) to <C>.(<x>, <y>)
[on delete …] [on update …] [--create-fk]`
The single-column form `from <P>.<col> to <C>.<col>` is unchanged
(no parens) — back-compatible and the common case. The
parenthesized list is the multi-column form. Both sides must use
the same arity (enforced as a D1 length check). Parentheses mirror
the existing compound-PK *declaration* syntax (`with pk a(int),
b(int)` uses parens around the per-column type; the FK list uses
parens around the column names) and the SQL `FOREIGN KEY (…)`
shape, so the surface stays internally consistent.
### D4 — SQL syntax: extend the existing lists
`FOREIGN KEY (<x>, <y>) REFERENCES <P> (<a>, <b>)` — the grammar's
child and parent column slots become comma-separated **lists**
(today capped at one). Inline `<col> <type> REFERENCES <P>(<a>,
<b>)` stays single-child-column (one inline column can't match a
2-column key) — a compound FK uses the table-level form. Bare
table-level `FOREIGN KEY (x, y) REFERENCES <P>` (no parent
columns) **auto-expands to the parent's full PK** when the arities
match; bare inline `<col> REFERENCES <P>` on a compound-PK parent
keeps today's friendly refusal, with the message pointing at the
table-level multi-column form.
### D5 — Storage: uniform column lists, matching the house style
Both stores hold an **ordered column list**, uniformly (a
one-element list for the single-column case), following the
convention `project.yaml` already uses for `primary_key` and index
`columns`.
- **`project.yaml`**: `RawEndpoint` becomes `{ table, columns:
Vec<String> }` and writes `columns: [a, b]` (single-column →
`columns: [id]`), exactly parallel to `primary_key: [id]`. No
scalar `column:` form, no dual-shape reader.
- **Metadata** (`__rdbms_playground_relationships`): no
`CREATE TABLE` change (the `TEXT` columns and
`PRIMARY KEY (child_table, child_column)` are untouched).
`parent_column` / `child_column` store the list **comma-joined**
in the same text cell (`a,b`; a single column is just its bare
name). *As-built note:* the ADR first said "JSON array"; the
implementation uses a comma delimiter, which is safe because
column identifiers are `[A-Za-z0-9_]+` (no commas — `parser.rs`)
and simpler (no `serde_json` dependency). This is an internal
encoding detail below fork F-B — the user-visible `project.yaml`
is still the `columns: [a, b]` list.
The actual enforced FK lives on the rebuilt child table's DDL
(`FOREIGN KEY (a, b) REFERENCES P(x, y)`), emitted by
`schema_to_ddl`, exactly as the single-column FK is today via the
rebuild-table primitive (ADR-0013) — one relationship, one undo
step.
### D6 — In-memory model: `Vec<String>` column lists
`Command::AddRelationship`, `SqlForeignKey`, `RelationshipSchema`,
the internal `ReadForeignKey`, and `RelationshipEnd` (display) all
carry `parent_columns: Vec<String>` / `child_columns: Vec<String>`
(or `Option<Vec<String>>` for the bare-SQL parent case). A
one-element vec is the single-column case; nothing about the
single-column UX changes.
## Genuine forks (escalated for sign-off)
These are decisions, not facts. Recommendations are marked; the
user confirms before this ADR moves to Accepted.
- **F-A — matching policy.** Full PK only (D1, *recommended*) vs.
allow a subset (needs a separate UNIQUE key; larger, less
teaching-clean).
- **F-B — storage encoding.** Uniform column lists in the existing
house style — `columns: [a, b]` in yaml (like `primary_key`),
JSON-array in the unchanged metadata `TEXT` columns; no
back-compat, no migration (D5, *recommended*) vs. a normalized
relationship-columns child table (more "correct" but a schema
change with joins on read, no learner-visible payoff). Premise:
no existing projects to preserve (confirmed).
- **F-C — DSL multi-column syntax.** `from P.(a, b) to C.(x, y)`
parenthesized (D3, *recommended*) vs. a repeated-dotted form
(`from P.a, P.b to C.x, C.y`, more ambiguous to parse and read).
- **F-D — bare table-level SQL FK auto-expansion.** Auto-expand
`FOREIGN KEY (x,y) REFERENCES P` to P's full PK when arities
match (D4, *recommended*) vs. always require explicit parent
columns.
## Implementation sketch (change sites)
Grouped; each lands behind tests. No migration step.
1. **AST** — `AddRelationship` + `SqlForeignKey` column fields →
`Vec<String>` / `Option<Vec<String>>` (`command.rs`).
2. **Grammar** — DSL endpoint column slot → optional
parenthesized list (`ddl.rs`); SQL child/parent column slots →
comma lists (`sql_create_table.rs`). Builders collect lists.
3. **Metadata** — `insert_relationship_metadata` /
`read_all_relationships` encode/decode bare-or-JSON
(`db.rs`); no `CREATE TABLE` change.
4. **Persistence** — `RelationshipSchema` → `Vec<String>`;
`RawEndpoint` becomes `{ table, columns: Vec<String> }`, written
`columns: [a, b]` like `primary_key`
(`persistence/mod.rs`, `persistence/yaml.rs`).
5. **Executor** — `do_add_relationship` /
`resolve_create_table_fks` / `do_alter_add_foreign_key` walk
column lists; `schema_to_ddl` emits multi-column `FOREIGN KEY
(…) REFERENCES P(…)`; `check_fk_type_compat` loops pairs;
bare-reference paths auto-expand to the full PK (D4) or refuse
with the improved message; the default relationship-name
generator (`db.rs:6850`) joins the column lists; `--create-fk`
creates one child column per parent PK column (`db.rs`).
6. **Display** — `RelationshipEnd` → column lists; `describe`
renders `(a, b) → (x, y)` symmetrically (outbound + inbound,
ADR-0013) (`db.rs`, `output_render.rs`).
7. **Teaching echo (ADR-0038)** — `render_add_relationship` and
`render_add_relationship_create_fk` (`echo.rs`) go multi-column:
the FK line emits `FOREIGN KEY (a, b) REFERENCES P (x, y)`, and
`--create-fk` emits **one `ADD COLUMN` line per newly-created
child column** (each typed to the matching parent PK column's
`fk_target_type`) before the FK line. Copy-paste contract
(ADR-0038) holds: every echoed line is runnable advanced SQL.
8. **Tests** — parse (DSL + SQL: single-col still works; multi
parses; arity mismatch errors; empty `()` rejected; inline
`col REFERENCES P(a,b)` rejected with the table-level pointer);
worker round-trip (declare a 2-col FK, rebuild, the FK is
**enforced** — an insert violating it is refused; per-pair
type-mismatch refused; bare-FK **auto-expand** to the parent PK;
`--create-fk` creates both child columns); persistence
round-trip (a single-col relationship writes `columns: [id]` and
reads back; a 2-col writes `columns: [a, b]` and reads back;
full save→rebuild reconstructs the FK); **undo** (add a 2-col
relationship, undo, it is gone — one step); display
(`describe` shows `(a, b) → (x, y)` both directions).
## Implementation-readiness notes (DA pass, 2026-06-09)
Verified against the code before build; folded in so the plan is
complete.
- **SQLite precondition holds.** A FK's parent columns must be a
PK or a UNIQUE-indexed set. A SQLite `PRIMARY KEY (a, b)` creates
the requisite unique index, so `FOREIGN KEY (x, y) REFERENCES
P(a, b)` is valid against a compound PK with no extra index.
STRICT tables do not change FK rules. (F-A's "full PK" therefore
always targets a valid key; a subset would not be unique — the
reason F-A excludes it.)
- **Explicit parent columns must be exactly the PK set.** Under
F-A, `REFERENCES P(<cols>)` is accepted iff `<cols>` is the
parent's PK column **set**; any ordering is accepted and maps
positionally to the child list (SQLite matches the set to the
unique index; the child↔parent pairing is positional). A
non-PK, partial, or super-set list is refused with a friendly
message naming the parent's actual PK (subset/UNIQUE targets are
OOS).
- **Arity + emptiness.** Child and parent lists must be equal,
non-zero length; a mismatch reports both counts
("N child column(s) but M in `P`'s key"). An empty `()` list is
a parse error. Inline single-column `col REFERENCES P(a, b)` is
refused (one inline column can't satisfy a 2-column key) with a
pointer to the table-level `FOREIGN KEY (…)` form (D4).
- **DSL `from P.(a)` (single in parens)** is accepted — equivalent
to bare `from P.a` — so the parenthesized form is uniform across
arities; the bare form stays the idiomatic single-column
spelling.
- **`--create-fk` is per-column.** When child columns are missing,
one is created per parent PK column, each typed to that parent
column's `fk_target_type` (ADR-0011) — generalising today's
single-column behaviour; the echo mirrors this (sketch step 7).
- **Metadata identity unchanged.** `PRIMARY KEY (child_table,
child_column)` still holds with the JSON-array string as the
key — so a child column **set** still participates in at most one
relationship (pre-existing behaviour, now per-set). Distinct
sets on the same child table are distinct keys.
- **Auto-name generation** (`db.rs:6850`, the `[as <name>]`-less
default) is single-column today
(`{parent_table}_{parent_column}_to_{child_table}_{child_column}`)
— it must join the column lists (e.g.
`Orders_a_b_to_Customers_x_y`). A found change site the first
sketch missed; added to the executor step.
- **Undo / batch unchanged.** One `add 1:n relationship` is one
rebuild = one undo step (ADR-0013/0006), independent of arity.
## Consequences
- T3 closes; a learner can model a real composite-key relationship
end to end.
- No migration, and the on-disk representation gets *more*
consistent: the relationship endpoint joins the `primary_key:
[...]` / index `columns: [...]` list convention. The in-app
single-column UX is untouched (one-element vecs).
- Accepted trade-off (user, 2026-06-09): a `project.yaml` written
before this change that contains relationships will not load
under the new format. There is no installed base to preserve, so
this is a clean cutover, not data loss.
- The relationship model becomes list-based throughout, which is
the natural foundation if subset/UNIQUE-targeted FKs are ever
wanted (explicitly OOS here).
- A modest, broad refactor (the `Vec` field change ripples through
the 6 layers) — methodical, not deep; locked by tests at each
layer.
## Out of scope
- Subset/non-PK FK targets (referencing a UNIQUE key that isn't
the PK) — possible later on this list-based foundation.
- Any change to single-column behaviour, the rebuild-table
primitive, or the undo model (one relationship = one undo step
stands).
- A `project.yaml` version bump or F3 migrator (not needed —
no installed base to migrate; clean cutover per D5).
@@ -1,9 +1,14 @@
# ADR-0042: Public website and documentation site
# ADR-0044: Public website and documentation site
## Status
Accepted (2026-06-04). Implementation plan:
[`docs/plans/20260604-adr-0042-website.md`](../plans/20260604-adr-0042-website.md).
[`docs/plans/20260604-adr-0044-website.md`](../plans/20260604-adr-0044-website.md).
> Renumbered from ADR-0042 to ADR-0044 when the `website` branch merged
> `main` (2026-06-09): `main` had independently used 0042 for the H1a
> parse-error ADR and 0043 for compound-PK FK references. Content is
> unchanged from the original draft.
## Context
+6 -4
View File
@@ -6,7 +6,7 @@ This directory contains the project's ADRs, recorded per
## Index
- [ADR-0000 — Record architecture decisions](0000-record-architecture-decisions.md)
- [ADR-0001 — Language and TUI framework](0001-language-and-tui-framework.md)
- [ADR-0001 — Language and TUI framework](0001-language-and-tui-framework.md)**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](0002-database-engine.md)
- [ADR-0003 — Input modes and command dispatch](0003-input-modes-and-command-dispatch.md) — the persistent `Simple`/`Advanced` mode and the `:` one-shot escape. The **startup mode is no longer always `simple`**: it is restored from the project's stored mode and overridable with `--mode` (see **ADR-0015 Amendment 1**, issue #14). The app-command registry gains **`copy`** (ADR-0041, issue #11)
- [ADR-0004 — Project file format](0004-project-file-format.md)
@@ -25,8 +25,8 @@ This directory contains the project's ADRs, recorded per
- [ADR-0017 — Column type-change compatibility](0017-column-type-change-compatibility.md)
- [ADR-0018 — Auto-fill contracts for `serial` and `shortid` columns](0018-auto-fill-contracts-for-serial-and-shortid.md)
- [ADR-0019 — Friendly error layer (H1) and i18n message catalog](0019-friendly-error-layer-and-i18n.md)
- [ADR-0020 — Tokenization layer for the DSL parser](0020-tokenization-layer-for-the-dsl-parser.md)
- [ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)](0021-parser-as-source-of-truth-for-h1a.md)
- [ADR-0020 — Tokenization layer for the DSL parser](0020-tokenization-layer-for-the-dsl-parser.md)**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 removed `chumsky` entirely; the lexer/keyword/token model here does not exist. Kept as institutional memory of the path not taken.
- [ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)](0021-parser-as-source-of-truth-for-h1a.md)**Mechanism superseded by ADR-0024; H1a scope continued in ADR-0042.** The *intent* (show the command's grammar at the point of error) shipped — `usage_ids` on each `CommandNode`, the `parse.usage.*` templates, and the `available_commands` fallback all exist — but via grammar nodes, not the `chumsky` `UsageEntry` registry / `parse.token.*` keys described here (which were never built).
- [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ``asc`/`desc`, `select Name ``as`, `create table … Code(text) ``not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung; **Amendment 4 gives column types a dedicated highlight class** — both `Node::Ident.highlight_override` *and* the `Word.highlight_override` field were dead (driver destructured the former to `_`, `walk_word` hardcoded `Keyword`); now both wired through, with a new `HighlightClass::Type` + eighth `Theme` field `tok_type` (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three `IdentSource::Types` slots opt in via `Some(Type)` (advanced-mode single-word SQL aliases — `float`, `varchar`, … per ADR-0035 §3 — ride along for free), and the two-word `double precision` alias opts in via the new `Word::type_keyword` constructor so it matches its synonyms; **Amendment 5 lets the hint panel grow for long prose hints** — a fixed one-row panel clipped long field-value/usage hints past the first line (issue #12); `resolve_hint_lines` now pre-wraps prose and `render_right_column` sizes the panel to the line count (1 row default, up to `MAX_HINT_ROWS`=3, reclaimed when short) with a `clamp_wrapped` ellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-char `parse.usage.sql_create_table` synopsis to a terse one-liner (full grammar remains in `help.ddl.sql_create_table`); **Amendment 6 adds a curated SQL function-name list** (`src/dsl/sql_functions.rs`, `KNOWN_SQL_FUNCTIONS` — aggregates + common + broader scalars; `cast` deliberately excluded as its `CAST(x AS type)` syntax isn't a plain-call shape) as the single source of truth shared by two consumers at the `sql_expr_ident` slot (ADR-0031 §1): **issue #15** offers the functions as Tab candidates under a new `CandidateKind::Function` + ninth `Theme` colour `tok_function` (a blue distinct from keyword/identifier/type, parallel to Amendment 4's `tok_type`) so a learner discovers `sum`/`upper`/…; **issue #16** restores the typing-time column-typo flag the issue-#6 fix had dropped wholesale at this slot — `invalid_ident_at_cursor` now bails only when the partial prefix-matches a known function, else falls through to the schema-column check, so `select Agx` warns again at typing time while `select sum` does not (the issue-#6 lockdown tests + the submit-time `unknown_column` diagnostic path are untouched, and the no-validation-allowlist posture stands); see ADR-0031's status note for the grammar-side anchor
- [ADR-0023 — Unified declarative grammar tree](0023-unified-grammar-tree.md) — direction (superseded for execution detail by ADR-0024)
- [ADR-0024 — Unified grammar tree: execution plan](0024-unified-grammar-tree-execution-plan.md) — **Accepted**, the executable spec — implemented (Phases AF; Phase F shipped "minimal", `parser.rs` retained as the router — see the ADR's Phase F implementation note)
@@ -47,4 +47,6 @@ This directory contains the project's ADRs, recorded per
- [ADR-0039 — EXPLAIN over advanced-mode SQL queries](0039-explain-over-advanced-sql.md) — **Accepted** (2026-05-27), **implemented 2026-05-30 (issue #7)**, **supersedes ADR-0030 §13 OOS-2**. Lets `explain` wrap the advanced SQL commands (`Select`/`SqlInsert`/`SqlUpdate`/`SqlDelete`, plus `with`/CTE which builds a `Select`) in addition to the DSL `ShowData`/`Update`/`Delete` it already covers (ADR-0028), running `EXPLAIN QUERY PLAN` over the validated SQL text through the existing ADR-0028 span-styled plan tree (advanced mode only; DSL `explain` unchanged in both modes). Implemented via a second `Advanced` `explain` CommandNode (`EXPLAIN_SQL`) registered under the shared `explain` entry word — reusing the established `insert`/`update`/`delete` shared-word dispatch (`decide`: SQL-first / DSL-fallback), so `explain show data …` and DSL-only `--all-rows` still reach the DSL node; rejected a `DynamicSubgrammar` mode-gate (its resolution cache key omits `mode`). `build_explain_sql` slices the inner SQL off the source (excludes `explain`) and reuses the existing SQL builders; `do_explain_plan` runs the carried text verbatim, no params. Advanced `explain update`/`delete` now route through SQL (identical plan, full SQL syntax); DSL-explain tests pinned to simple mode. Reframed OOS-2 as a *deferred* exclusion (per ADR-0000's out-of-scope discipline), not a rejection. OOS (deferred): EXPLAIN of DDL (no query plan exists)
- [ADR-0040 — A per-command completion marker (✓/✗) replaces the `[ok]` summary line](0040-completion-marker-replaces-ok-summary.md) — **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 (and `explain select` even rendered `[ok] explain` with an empty subject post-ADR-0039). Decision: drop the `[ok]` line and the symmetric `"…" failed:` prefix; the echo line gains a trailing inline **✓** (green, success) / **✗** (red, failure) — `running:` becomes a pending state that resolves to `<input> ✓/✗` on completion (status set via the existing `rfind(Echo)` lookup). Content (row counts, structure, data, plan tree, teaching echo) unchanged. Scoped to the DSL/data/SQL family that has the redundant echo+`[ok]` pair; app-command `[ok]` lines (`rebuild`/`export`/`now editing`) are payload-bearing, have no echo to mark, and stay as-is. `ok.summary` retired; `dsl.failed` reduced to the rendered reason. Broad but mechanical snapshot churn. OOS: app-command `[ok]` lines, the `[WRN]` validity indicator, and the tag colours (issue #10)
- [ADR-0041 — Copy the output panel to the system clipboard](0041-copy-output-to-clipboard.md) — **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 all` copy the whole panel; `copy last` copies from the most recent echo line to the end. **Mechanism — OSC 52 *and* native (`arboard`), always both**, because OSC 52 acceptance is undetectable (no terminal ack), so a true "fall back when unsupported" can't be built: emit the OSC 52 escape (no new dep — `base64`+`crossterm`; works over SSH; tmux-passthrough-wrapped via `$TMUX`), then a best-effort native write whose failure is ignored (headless host — OSC 52 carried it); the two carry identical content. **Format — plain text verbatim as rendered** (tags, `✓`/`✗`, box-drawing) joined by `\n`, without viewport padding/wrapping; a drift-lock test pins `OutputLine::plain_text` to `render_output_line`. `arboard` added **`--no-default-features`** (drops the `image` crate; X11-only on Linux — `wayland-data-control` deliberately omitted as it ~doubles the dep tree and OSC 52 covers native-Wayland). Security: write-only, scans clean for arboard's tree (cargo audit / osv-scanner / grype), 1Password-maintained, minimal surface. OOS: Markdown export, selection/range, a keybinding, OSC 52 read, `screen` passthrough
- [ADR-0042 — Public website and documentation site](0042-public-website-and-documentation-site.md) — **Accepted 2026-06-04**. The first public website: a marketing landing page plus the **canonical** user docs. Stack **Astro 6 + Starlight + Tailwind v4** (chosen over SvelteKit + Tailwind for a docs-heavy + marketing site; interactive bits as Astro islands). Showcase demos are **asciinema** `.cast` recordings (scripted-input driver for paced, re-recordable sessions — *not* `history.log` replay), reused inline in docs. The **in-page WASM playground is deferred** (OOS: deferred) behind a stable `Demo` seam, with the portable-core (`dsl`/`app`/`ui`, in-memory `rusqlite` via `ffi-sqlite-wasm-rs`) vs native-edge (Tokio/worker-thread/`crossterm`/persistence/backup-API) boundary recorded for a future ADR + iteration plan. Portable **static build** (Vercel target, but host-agnostic); **no CI yet**; **monorepo** (`website/`). Docs cover the **full supported feature set** with "planned" callouts for the unshipped minority; two wording rules bind user-facing copy — **no engine name** (continues ADR-0002) and **no "DSL"** ("simple mode" / "advanced mode"). Install docs cover **prebuilt binaries + package managers** (D1D3 track the release tooling). Plan: `docs/plans/20260604-adr-0042-website.md`
- [ADR-0042 — H1a parse-error pedagogy in the grammar-tree era](0042-h1a-parse-error-pedagogy-grammar-tree.md) — **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 (38 `parse.usage.*` templates), available-commands fallback, structural "after `…`, expected …" wording, source-derived ident slot labels ("table name"/"column name"), curated `parse.custom.*` near-miss messages, and the ADR-0027/0033/0036 schema-aware `[ERR]` diagnostics — so H1a is *substantially* delivered at the intent level. Defines the remaining work as **(1)** a verified per-command **near-miss matrix** (`tests/typing_surface/` + `tests/it/parse_error_pedagogy.rs`) as the definition of done, test-first; **(2)** **friendlier literal expectation labels** — optional prose glosses on `Word`/`Punct`/`Flag` positions that *add* role context while always keeping the exact literal visible (e.g. "a filter clause: `where …` or `--all-rows`"); **(3)** **advanced-mode SQL** near-miss parity (RETURNING scope, CTE-arity positioning, `CROSS JOIN … ON`, INSERT…SELECT count) — **in scope**, kept distinct from ADR-0019 §OOS-2 which covers advanced-SQL *engine*-error sanitisation, a different layer. Catalog/anchor-phrase discipline (ADR-0019) preserved; no public API change. OOS: I3/I4, spell-correction, multi-error reporting, verbosity-gating the usage block
- [ADR-0043 — Compound-primary-key foreign-key references (T3)](0043-compound-pk-foreign-key-references.md) — **Accepted + implemented 2026-06-09** (all four forks confirmed at the recommended option: full-PK matching, house-style uniform lists, parenthesized DSL syntax, bare-SQL-FK auto-expansion). Closes `requirements.md` **T3** `[x]` — the relationship model went list-based across six layers (single-column preserved, no migration), DSL `from P.(a,b) to C.(x,y)` + SQL `FOREIGN KEY (a,b) REFERENCES P(x,y)` parse/execute/enforce, 12 tests in `tests/it/compound_fk.rs`. Closes the open leg of `requirements.md` **T3**: a foreign key that *references* a parent's compound primary key. A 2026-06-09 audit found single-column FK woven through ~1520 sites (metadata table, `RelationshipSchema`, `project.yaml` `RawEndpoint`, both grammar surfaces, executor FK-DDL emission, per-column type-compat, display) — earns an ADR, not an inline build. **Decision:** reference the parent's **full** compound PK, matched **positionally** to an equal-length child column list, per-pair `fk_target_type` compat (ADR-0011, element-wise); DSL `from <P>.(a, b) to <C>.(x, y)` (single form unchanged), SQL `FOREIGN KEY (x, y) REFERENCES P(a, b)` (extend the existing one-cap lists; bare table-level FK auto-expands to the parent PK when arities match). **Storage — no migration (back-compat not required, user-confirmed 2026-06-09; no installed base):** the relationship endpoint joins the list convention `project.yaml` *already* uses — `columns: [a, b]` like `primary_key: [id]` and index `columns: [...]` (the endpoint was the lone scalar `column:` holdout); the metadata `TEXT` columns are unchanged and store the list **comma-joined** (`a,b`; the bare name for single — safe because identifiers are `[A-Za-z0-9_]+`). No F3 migrator, no version bump; accepted trade-off is that a pre-change `project.yaml` with relationships won't load (clean cutover). In-memory model goes list-based (`Vec<String>`) through all six layers; the enforced FK is the rebuilt child-table DDL (`FOREIGN KEY (a,b) REFERENCES P(x,y)`), one relationship = one undo step (ADR-0013). Genuine forks escalated: matching policy (full-PK vs subset), storage (house-style uniform lists vs normalized table), DSL syntax (parenthesized vs repeated-dotted), bare-SQL-FK auto-expansion. OOS: subset/non-PK (UNIQUE-targeted) FK references; any single-column behaviour change
- [ADR-0044 — Public website and documentation site](0044-public-website-and-documentation-site.md) — **Accepted 2026-06-04** (originally drafted as ADR-0042 on the `website` branch; renumbered on merge to avoid colliding with the H1a ADR-0042). The first public website: a marketing landing page plus the **canonical** user docs. Stack **Astro 6 + Starlight + Tailwind v4** (chosen over SvelteKit + Tailwind for a docs-heavy + marketing site; interactive bits as Astro islands). Showcase demos are **asciinema** `.cast` recordings (scripted-input driver for paced, re-recordable sessions — *not* `history.log` replay), reused inline in docs. The **in-page WASM playground is deferred** (OOS: deferred) behind a stable `Demo` seam, with the portable-core (`dsl`/`app`/`ui`, in-memory `rusqlite` via `ffi-sqlite-wasm-rs`) vs native-edge (Tokio/worker-thread/`crossterm`/persistence/backup-API) boundary recorded for a future ADR + iteration plan. Portable **static build** (Vercel target, but host-agnostic); **no CI yet**; **monorepo** (`website/`). Docs cover the **full supported feature set** with "planned" callouts for the unshipped minority; two wording rules bind user-facing copy — **no engine name** (continues ADR-0002) and **no "DSL"** ("simple mode" / "advanced mode"). Install docs cover **prebuilt binaries + package managers** (D1D3 track the release tooling). Plan: `docs/plans/20260604-adr-0044-website.md`
+185
View File
@@ -0,0 +1,185 @@
# Session handoff — 2026-06-06 (58)
Fifty-eighth handover. Continues from handoff-57, whose **next job
was H1a**. This session did exactly that, end to end: **H1a
(strong syntax-help in parse errors) is now done and marked `[x]`**
in `requirements.md`, via a new ADR-0042 systematic pass. The arc
was audit → ADR → test-first matrix → four gap fixes → two
adversarial review passes that each caught a real defect. See §2.
## §1. State at handoff
**Branch:** `main`. **HEAD `c305dc7`.** **origin/main at `10f8c2a`**
(pushed mid-session) → **4 commits unpushed** (`649fdcb`, `1d4923b`,
`d6e229f`, `c305dc7`). Push is the user's step.
**Tests: 2158 passing / 0 failing / 1 ignored** (lib 1578, it 388,
typing_surface_matrix 192; the 1 ignored is the pre-existing one).
**Clippy clean** (nursery, all targets, `-D warnings`).
This session's commits (since handoff-57's `a8d0138`):
```
c305dc7 docs: mark H1a done via the ADR-0042 systematic pass
d6e229f feat: H1a CROSS JOIN ON teaching message; advanced-SQL gaps re-verified (ADR-0042)
1d4923b fix: H1a G3 advanced usage shows all valid forms; complete near-miss matrix (ADR-0042)
649fdcb feat: H1a parse-error gaps G2G4 + advanced near-miss matrix (ADR-0042)
10f8c2a test: H1a near-miss matrix + friendlier `add 1:n relationship` label (ADR-0042)
0e6f767 docs: ADR-0042 — continue H1a parse-error pedagogy on the grammar tree
```
## §2. What happened this stretch — H1a, start to finish
### Audit first (the big correction)
The handoff-57 §6 pointer said "read ADR-0021 + ADR-0020". **Both
are obsolete.** They specify a `chumsky`-over-tokens mechanism
(`UsageEntry` registry, `parse.token.*` keys, a lexer) that
**ADR-0024 deleted** — chumsky is not a dependency; the parser is
the scannerless unified grammar-tree walker. ADR-0020/0021 are now
marked **Superseded** (status notes + README), kept as memory.
ADR-0021's *intent* survived and was already ~80% shipped via the
grammar tree. **Read `docs/adr/0042-h1a-parse-error-pedagogy-grammar-tree.md`,
not 0020/0021, for the live H1a design.**
### ADR-0042 + the user's three scope decisions
Wrote **ADR-0042** (continues H1a against the grammar tree). Three
forks escalated and decided by the user: (1) ADR hygiene =
superseded-notes + new ADR; (2) scope = matrix-verify + friendlier
literal labels; (3) advanced-SQL **in scope**.
### The near-miss matrix (the definition of done)
`tests/it/parse_error_pedagogy.rs` now holds a per-command
near-miss matrix, built **test-first** from an empirical baseline
capture. Covers, in both modes: every entry word's bare /
missing-clause / wrong-token cases, the app-lifecycle trailing-junk
cases, and the **committed multi-forms** (`add index`,
`add constraint`, `add 1:n relationship`, `drop index/constraint/
relationship`, `show table`, `change column`, `create index`,
`alter table add/drop`). Tests: `near_miss_matrix_simple_mode`,
`near_miss_matrix_advanced_mode`, `near_miss_matrix_committed_multiforms`,
plus per-gap tests.
### Four gap fixes (each test-first)
- **G1** — bare `1` cardinality literal → `` `1:n relationship` `` in
`format_expectation` (render-only; completion still offers `1`).
- **G2** — bare `select`'s 14-item expression first-set → "a
projection: `*`, a column, or an expression", detected by the
`distinct`+`all` quantifier pair (unique to a projection start;
empirically verified non-misfiring). Render-only in
`format_walker_error`.
- **G3** — usage block was mode-blind. Added `usage_*_in_mode`
(`src/dsl/grammar/mod.rs`) + mode threading through
`render_usage_block` (`app.rs`) and the ambient usage
(`input_render.rs`). **Decision (user, after review):** advanced
mode shows **all forms valid in the mode, SQL-first then the DSL
fallback forms** — DSL forms (`create table … with pk`,
`drop column …`) remain valid input in advanced mode (verified),
so a usage hint must not hide them. Simple mode = DSL only.
- **G4** — `with` got its own `parse.usage.with` CTE template
(was borrowing `select`'s).
- **CROSS JOIN ON** — `parse.cross_join_no_on` teaches "a CROSS JOIN
has no ON clause …" when the failing token is `on` and the most
recent consumed join is a CROSS join. `is_cross_join_on` in
`parser.rs`; render-only.
### Two adversarial review passes earned their keep
- Pass 1 caught **G3 over-correction**: an initial "SQL-only"
advanced usage block *hid* valid DSL fallback forms. Escalated →
user chose "show all valid forms" → fixed (`1d4923b`).
- "Verify, don't trust the survey" **reversed two of three**
advanced-SQL "gaps": INSERT…SELECT count and RETURNING scope were
*already handled* (the Explore-survey list was wrong, twice).
Only CROSS JOIN ON was real.
- The matrix itself caught a regression mid-work (advanced
insert/update/delete falling back to available-commands because
the SQL nodes have empty `usage_ids`; fixed with a union fallback).
### Deferred by decision (low-priority residual)
At **submit** time, a non-projection expression position (bare
`where `, `returning `, `having `, `set col=`) still renders the raw
~14-item expression first-set; only the SELECT projection is glossed
(G2). Low-impact because **typing-time completion already offers the
right candidates** there. User chose to leave it. Documented in
ADR-0042 + `requirements.md`.
Plus one **pre-existing caveat** (not this session's work, noted in
ADR-0042): `insert into T (one_col) select * from Multi` isn't
pre-caught for arity — `SELECT *` isn't expanded; the engine rejects
it at execution (brushes ADR-0019 §OOS-2 engine-error territory).
## §3. ⚠️ Where parse-error pedagogy lives now (read before touching)
- **Usage templates:** `parse.usage.*` in `src/friendly/strings/en-US.yaml`;
`usage_ids` on each `CommandNode` (`src/dsl/grammar/mod.rs`).
Mode-aware selection: `usage_keys_for_input_in_mode` /
`usage_key_for_input_in_mode`.
- **Structural error wording:** `format_walker_error` +
`format_expectation` in `src/dsl/parser.rs` (this is where the G1
label, G2 projection gloss, and CROSS JOIN message live —
render-only, they do **not** mutate the `Expectation` set the
completion/hint layer consumes).
- **Catalog discipline (ADR-0019):** every new key goes in
`en-US.yaml` **and** `src/friendly/keys.rs::KEYS_AND_PLACEHOLDERS`
(the `keys_validate_against_catalog` test enforces it).
- **Tests:** integration parse-error tests live in `tests/it/` per
the handoff-57 §3 rule — drop the file in `tests/it/` + add a
`mod` line to `tests/it/main.rs`. Schema-aware diagnostics are
tested at the walker level with a `SchemaCache` (`vschema` helper
in `tests/it/sql_insert.rs`).
## §4. Carried / unchanged
- **arboard decisions** (handoff-55 §3): X11-only on Linux; `copy`
reproduces `[system]` tags. One-line changes if revisited.
- No open GitHub issues (`gh issue list` empty; the project's issue
tracker is GitHub, not the gitea host `tea` is configured for).
## §5. Other tracks (from `requirements.md`)
Unchanged: Track 2 Iter 6 leftovers (history.log input-history
hydration polish, migration-framework exercise); C3a modify
relationship; C4 m:n convenience; **H1 done** (ADR-0019), **H1a
done** (ADR-0042); H2 `hint`; H3 `help` (partial — general
reference + `help <command>` still missing); V4 session-log /
Markdown export; I1/I1b multi-line + readline; I3 tab completion /
I4 syntax highlighting (the walker already exposes the hooks); TU1
tutorial (needs ADR); TT5 CI (not configured).
## §6. Next job — pick one
No single forced next step. Candidates, roughly by readiness:
1. **H3 `help` completion** — the grammar tree already iterates the
REGISTRY for the command list; the missing pieces are a general
reference and `help <command>` per-command detail. The
`help_id` per `CommandNode` is the hook. Small-to-medium.
2. **I3 tab completion** — the walker's expected-set + completion
candidates already exist (used by ambient hints); I3 is the
**UI/UX** (cursor handling, menu, accept). Needs its own ADR.
3. **The deferred H1a residual** (§2) — generalise the projection
gloss to other expression positions. Low value (completion
already covers typing-time); only if it bugs you.
4. **CI (TT5)** — test infra is solid (2158 green); no workflow yet.
## §7. How to take over
1. Read handoffs 56 → 57 → 58, then `CLAUDE.md`, then
`docs/requirements.md` (H1 and **H1a now `[x]`**),
`docs/adr/README.md`.
2. **For anything parse-error/pedagogy: read ADR-0042, not
ADR-0020/0021** (those are superseded; chumsky is gone).
3. Codebase on `main` at `c305dc7`, clean, 4 unpushed.
4. Process pins that paid off this arc, again: **audit before
assuming** (ADR-0021 was obsolete; H1a was mostly already
shipped), **verify empirically — don't trust the docs/survey**
(the advanced-SQL gap list was wrong twice; a regression hid in
"looks fine"), **escalate genuine forks** (the G3 all-forms
decision was the user's, not mine), and **test-first + matrix as
a regression lock** (it caught a regression I introduced).
Commits user-confirmed, append-only, no AI attribution.
+182
View File
@@ -0,0 +1,182 @@
# Session handoff — 2026-06-09 (59)
Fifty-ninth handover. Continues from handoff-58 (H1a done). This
session did **three** distinct things, in order: (1) a
**tracking-integrity audit + reconciliation** of `requirements.md`
against the actual code; (2) a **partials sweep** closing V5, H3,
and a new V5a; (3) the big one — **T3 compound-PK foreign-key
references**, designed in **ADR-0043**, `/runda`-verified twice,
and implemented end-to-end across both surfaces.
## §1. State at handoff
**Branch:** `main`. **HEAD `4752ba2`.** 8 commits this session
(`28e7596``4752ba2`); push is the user's step.
**Tests: 2193 passing / 0 failing / 1 ignored** (lib 1578, it 423,
typing_surface_matrix 192; the 1 ignored is the long-standing
doc-test). **Clippy clean** (nursery, all targets).
**Requirements markers now:** `[ ]` 19, `[/]` 6, `[x]` 59,
`[~]` 11, `[-]` 3. (The `[/]` "partial / in progress" marker is
**new this session** — see §2.)
This session's commits:
```
4752ba2 feat: compound-PK foreign-key references — grammar + tests (ADR-0043)
b14f019 refactor: relationship model to column lists for compound FK (ADR-0043)
b688592 docs: ADR-0043 implementation-readiness notes from /runda DA pass
274e2b1 docs: ADR-0043 compound-PK foreign-key references (T3); accepted
1d898ad feat: V5a show relationship/index <name> detail views
757711f feat: H3 help <command> per-command detail + general reference
8dec784 feat: V5 show tables / relationships / indexes list commands
28e7596 docs: reconcile requirements tracking with verified code state
```
## §2. The tracking reconciliation (read this — it changes how you plan)
Planning off handoffs/`requirements.md` was **unreliable**: an
audit of all 35 `[ ]` items against the source found **~46 %
mis-marked**, overwhelmingly *under*-claimed (tab completion `I3`
and syntax highlighting `I4` were shipped but marked "not yet
implemented"). Root cause: the **binary legend** — a shipped
feature, a half-built one, and an untouched one all wore the same
`[ ]`.
Fix (commit `28e7596`): added a **`[/]` "partial / in progress"**
marker to the legend, with a dated reconciliation note. Re-marked:
- **7 shipped-but-`[ ]``[x]`:** S1, S4, S5, I1a, I3, I4, C1.
- **9 substantially-built → `[/]`** with explicit gap notes: S3,
A1, V1, V2, V5, T3, H3, DOC1, X1.
- Fixed a **false `CLAUDE.md` claim** ("Tier 4 is wired") — no PTY
tests exist; TT4 is spec-only.
**Discipline going forward:** move items `[ ]``[/]``[x]`;
keep the gap note current. Don't trust a marker without checking
the code — verify empirically.
## §3. The partials sweep (V5, H3, V5a)
Three small/medium, no-ADR partials closed (the others — V1, S3/V2,
A1, DOC1, X1 — need an ADR / output-model redesign / new features,
so were left `[/]`, see §5):
- **V5 `[x]`** (`8dec784`): `show tables` / `show relationships` /
`show indexes` as one `Command::ShowList { kind }`; read-only
worker `show_list`. 10 tests.
- **H3 `[x]`** (`757711f`): `help <command>` per-command detail
(HELP node takes an optional `BarePath` topic; `note_help_topic`
renders every command sharing the entry word) + `help types` +
friendly unknown-topic pointer. 9 tests.
- **V5a `[x]`** (`1d898ad`): singular `show relationship <name>` /
`show index <name>` detail — folded into `ShowList { kind, name:
Option<String> }`. Raised + closed this session (it was the
honest tracked home for V5's `[<name>]` clause).
## §4. T3 — compound-PK foreign-key references (the big one)
**ADR-0043** (`docs/adr/0043-compound-pk-foreign-key-references.md`,
Accepted + implemented). The open leg of T3: a FK that *references*
a parent's compound PK. The audit found single-column FK woven
through ~1520 sites; it earned an ADR, not an inline build.
**Four forks, all user-confirmed at the recommended option:**
- **F-A** reference the parent's **full** PK (subset/UNIQUE-target
OOS), matched **positionally** to an equal-length child list.
- **F-B** **house-style uniform lists, no migration** — the
decisive simplifier: the user confirmed **back-compat is not
required** (pre-release, no installed base), so `project.yaml`
endpoints became `columns: [a, b]` (like `primary_key: [id]`),
metadata `TEXT` cells store the list **comma-joined** (bare for
single), and **no F3 migrator / version bump** was needed.
- **F-C** parenthesized DSL `from P.(a, b) to C.(x, y)` (single
bare form unchanged).
- **F-D** bare table-level SQL `FOREIGN KEY (x,y) REFERENCES P`
auto-expands to the parent's full PK when arities match.
**Implementation (two commits):**
- `b14f019`**the refactor**: relationship model `String →
Vec<String>` through all six layers (`AddRelationship` /
`SqlForeignKey` AST, `RelationshipSchema`, metadata,
`project.yaml`, `ReadForeignKey`, `RelationshipEnd`). **Single-
column behaviour preserved** (one-element vecs); the existing
single-column relationship suite was the regression net. This is
the safe green checkpoint.
- `4752ba2` — **the grammar + tests**: multi-column parsing on both
surfaces (`AR_PARENT_COLS`/`AR_CHILD_COLS` Choice in `ddl.rs`;
`FK_*_COL_LIST` Repeated in `sql_create_table.rs`;
`consume_fk_reference` + the table-level/ALTER parsers collect
lists). Executor: `resolve_fk_parent_columns` (F-A validation +
F-D auto-expand + arity), per-pair `check_fk_type_compat`,
`schema_to_ddl` multi-column emission, `pragma_foreign_key_list`
read **grouped by `id`, ordered by `seq`** (positional pairing),
auto-name joins lists, `--create-fk` per-column, multi-column
teaching echo. 12 tests in `tests/it/compound_fk.rs`.
**`/runda` ran twice** (once on the ADR pre-build, once on the
finished code). The post-build pass **found 3 coverage gaps and
they were closed**: save→rebuild round-trip (the riskiest — proves
comma-encode/decode + yaml + FK reconstruction survive a text→db
rebuild), undo (one step), per-pair type-mismatch.
**Two residuals, confirmed non-blocking (noted in code):**
1. inline `col REFERENCES P(a,b)` gives a *generic* arity error,
not the ADR's tailored "use the table-level form" pointer.
2. a compound-FK *violation*'s friendly error names only the first
column pair (the ADR-0019 facts model is single-column).
Both are messaging-only; FK correctness/enforcement is unaffected.
**Process note (a real scare):** a broad regex script over test
files (to wrap `add_relationship` call args + struct fields in
`vec![]`) **over-reached into lib structs** it shouldn't have
(`RelationshipSelector::Endpoints`, `FailureContext`/
`TranslateContext` — all deliberately single-column), briefly
breaking the lib build. Recovered by reverting those specific
sites. Lesson: scripted edits across `src/` are dangerous when a
field name (`parent_column`) is shared between the struct you're
migrating and ones you're not — prefer compiler-guided manual fixes
or scope the script tightly.
## §5. Remaining open landscape (now trustworthy)
**Still `[/]` partial (need design/ADR or are larger):**
- **V1** relationship line-art visualization — **needs a
visualization ADR** (the most-requested deferred item).
- **S3 / V2** multi-result tabs — output-model redesign (single
buffer → tab abstraction).
- **A1** app-commands — blocked on `seed` (SD1) + `hint` (H2),
both unbuilt features.
- **DOC1** reference docs — writing effort.
- **X1** logging density — broad/mechanical (~25 `tracing` sites
today; `CLAUDE.md` wants "log liberally").
**`[ ]` not started:** H2 `hint`, SD1 `seed`, C4 m:n, B3
query-timeout, I1 multi-line, I1b readline, I5 cancellation, TT5
CI, TT4 PTY (spec-only), D1D3 distribution, NFR-1…7.
## §6. Next job — candidates
No forced next step. By readiness:
1. **V1 relationship visualization** — design-first (its own ADR).
The user has flagged it repeatedly; it's the prominent open
design item.
2. **X1 logging** — mechanical, no ADR; brings the codebase to the
`CLAUDE.md` "log liberally" bar.
3. **TT5 CI** — test infra is solid (2193 green); no workflow yet.
4. The two T3 residuals (§4) — small messaging polish if they bug
you.
## §7. How to take over
1. Read handoffs 57 → 58 → 59, then `CLAUDE.md`,
`docs/requirements.md` (now with the `[/]` marker + the §2
reconciliation note), `docs/adr/README.md`.
2. **For relationships/FK: read ADR-0043.** The model is list-based
(`Vec<String>`); single-column is the one-element case.
3. Codebase on `main` at `4752ba2`, clean, 8 commits unpushed.
4. Process pins that paid off: **verify the tracker against code,
don't trust the marker**; **escalate genuine forks** (the four
ADR-0043 forks were the user's); **`/runda` after planning AND
after implementation** (the post-build pass found 3 real gaps);
**scope scripted edits tightly** (§4 scare). Commits
user-confirmed, append-only, no AI attribution.
@@ -3,7 +3,7 @@
**Date:** 2026-06-04 · **Status:** ready to build
Decisions for this work are recorded in
[ADR-0042](../adr/0042-public-website-and-documentation-site.md): Astro 6 +
[ADR-0044](../adr/0044-public-website-and-documentation-site.md): Astro 6 +
Starlight + Tailwind v4; asciinema demos reusable in docs; the in-page WASM
playground deferred behind a stable demo seam; portable static hosting
(Vercel target); monorepo (`website/`); website is the canonical docs home;
@@ -128,7 +128,7 @@ project lifecycle + undo/history → Tier 3 teaching echo + EXPLAIN + errors +
completion/highlighting → Tier 4 clipboard + hints + editing.
Conventions live in the **living style guide** `website/STYLE.md` (binding
rules from ADR-0042 §7 — no engine name, **no "DSL"**, "planned" callouts —
rules from ADR-0044 §7 — no engine name, **no "DSL"**, "planned" callouts —
plus finer conventions and an open-decisions log for depth/splitting/example
dataset/etc. as they settle). Sources to mine: `src/dsl/command.rs`,
`src/dsl/grammar/*`, the REGISTRY, `en-US.yaml`, `docs/adr/*`,
@@ -144,7 +144,7 @@ static host. `website/README.md` notes the Vercel preset (root dir
`Demo.astro` exposes a stable contract (`{ src, title, height, autoplay }`).
At launch it renders `Cast.astro`; later a `Playground.astro` WASM island
swaps in behind the same props on the landing page and in docs, with zero
call-site changes. Boundary details are in ADR-0042 §3.
call-site changes. Boundary details are in ADR-0044 §3.
## Verification
@@ -155,7 +155,7 @@ call-site changes. Boundary details are in ADR-0042 §3.
- Starlight link-check passes (broken internal links fail the build).
- Docs grep clean of forbidden terms: **no "DSL"**, no engine name.
- A `dist/` static deploy works on Vercel (manual import) — confirms
portability. (No CI gate yet, per ADR-0042 §4.)
portability. (No CI gate yet, per ADR-0044 §4.)
## Notes / recommendations (non-blocking)
+209 -63
View File
@@ -13,17 +13,32 @@ is a process failure.
**Scope.** The list is intentionally coarse — each item is a
unit of "satisfied / not satisfied" judgement. When an item is
taken up for implementation, it is decomposed further in a
backlog (initially in this repo, later in GitHub issues once the
repo is pushed).
backlog (initially in this repo, now tracked as Gitea issues).
## Status legend
- `[ ]` — open, not yet implemented
- `[ ]` — open, not yet started
- `[/]` — partial / in progress: some of it is built and tested,
but named gaps remain. The entry states what works and what is
still missing. (Distinct from `[ ]`, which is genuinely
untouched, and from `[~]`, which is *deliberately* deferred
pending an ADR rather than half-built.)
- `[x]` — satisfied (implemented + tested)
- `[~]` — deferred, awaiting an ADR or further design before any
implementation
- `[-]` — explicitly out of scope (rationale at the bottom)
> **Reconciliation note (2026-06-07).** A full audit of every
> `[ ]` item against the source found ~46 % of them mis-marked —
> overwhelmingly *under*-claimed (e.g. tab completion `I3` and
> syntax highlighting `I4` were shipped but marked "not yet
> implemented"). The binary legend was the root cause: a
> shipped feature, a half-built one, and an untouched one all
> wore the same `[ ]`. The `[/]` marker above was added to fix
> this, and the audited items were re-marked. When you implement
> against an item, move it `[ ]` → `[/]` → `[x]` rather than
> jumping straight to `[x]`, and keep the gap note current.
## Test baseline
After ADR-0022 Amendment 6 (the curated SQL function-name list —
@@ -54,20 +69,34 @@ since ADR-0027.)
## TUI shell
- [ ] **S1** Three-region layout: items list (left), output
- [x] **S1** Three-region layout: items list (left), output
panel (right), input field (bottom).
*(Verified 2026-06-07: `ui.rs:26-58` lays out a horizontal
split — items panel left, right column subdivided into output
panel / input field / hint panel; rendered every frame.)*
- [x] **S2** Items list shows tables and per-table indexes;
designed to extend to additional element kinds (relations,
views, etc.) without restructuring.
*(ADR-0025: the items panel renders a nested list — each
table with its index names indented beneath it. The nested
model is the extension point for future element kinds.)*
- [ ] **S3** Output panel renders a visualization of the
- [/] **S3** Output panel renders a visualization of the
currently selected item and supports multiple tabs.
- [ ] **S4** Hint area below the input field; keyboard-toggleable
*(Partial, verified 2026-06-07: single-element structure
visualisation renders (`output_render.rs:82-180`); **multiple
tabs are not implemented** — the output is one line buffer, no
tab abstraction. Same multi-tab gap as V2.)*
- [x] **S4** Hint area below the input field; keyboard-toggleable
for inspecting hints about the current input or last error.
- [ ] **S5** Mode label and distinct border style on the input
*(Verified 2026-06-07: `ui.rs:1088-1110` `render_hint_panel` /
`resolve_hint_lines` — a dynamic 1`MAX_HINT_ROWS` panel below
the input showing ambient hints, candidates, or the last error.)*
- [x] **S5** Mode label and distinct border style on the input
field communicate the current input mode at all times.
*(Verified 2026-06-07: `ui.rs:896-934` `render_input_panel` —
a coloured, bold mode label plus a mode-distinct border colour
(`border` vs `border_advanced`), tracking the three-way
`EffectiveMode` incl. the one-shot `:` state.)*
- [x] **S6** Input-field validity indicator: a debounced
`[ERR]` / `[WRN]` marker at the right edge of the input row,
summarising — before submit — whether the current command
@@ -89,12 +118,17 @@ since ADR-0027.)
- [ ] **I1** Multi-line entry that auto-expands; Ctrl-Enter (or
equivalent) submits, plain Enter inserts a newline.
- [ ] **I1a** In-line cursor editing in the input field: Left /
- [x] **I1a** In-line cursor editing in the input field: Left /
Right arrows move the cursor by character (UTF-8 boundaries
honoured), Home / End jump to the extremes, Delete removes the
character at the cursor, Backspace removes the character
before. Insertion happens at the cursor position. *(Implemented;
multi-line editing per I1 still pending.)*
before. Insertion happens at the cursor position.
*(Verified 2026-06-07: `app.rs:973-1005` `cursor_left` /
`cursor_right` (with `is_char_boundary` checks), Home/End,
Delete, Backspace, `insert_at_cursor`. This is single-line
cursor editing and is complete on its own terms; the separate
**multi-line** entry goal is tracked under I1, which is
genuinely not started.)*
- [ ] **I1b** Readline-style cursor shortcuts: Ctrl-A / Ctrl-E
as aliases for Home / End for users on keyboards without those
keys (and for ergonomics in command-driven workflows). Likely
@@ -106,15 +140,33 @@ since ADR-0027.)
navigable history is hydrated from the tail of
`history.log` up to the same in-memory cap (Iter 6). Global
rolling history is out of scope per OOS-6 / N4.)*
- [ ] **I3** Tab completion for app commands, DSL keywords, table
- [x] **I3** Tab completion for app commands, DSL keywords, table
names, column names, and SQL keywords.
*(Refinement 2026-05-30, issue #15: SQL expression slots
(`sql_expr_ident`) now also offer a curated set of SQL function
*(Verified 2026-06-07 — this was mis-marked `[ ]` despite being
shipped: `src/completion.rs` is 2852 lines with ~100 tests;
`app.rs:898` binds Tab → `completion_tab_forward` (and
BackTab → `_backward`) with forward/backward cycling through a
candidate memo and a colour-coded candidate line in the hint
panel (`ui.rs:1125` `render_candidate_line`). All five
candidate categories work — app commands (via REGISTRY), DSL
keywords (walker `Expectation`), table names + column names
(`SchemaCache`), and SQL keywords/functions in advanced mode.
Refinement 2026-05-30, issue #15: SQL expression slots
(`sql_expr_ident`) also offer a curated set of SQL function
names — `KNOWN_SQL_FUNCTIONS` in `src/dsl/sql_functions.rs`,
surfaced as a new `CandidateKind::Function` ADR-0022 Amendment 6.
The broad tab-completion goal stays open.)*
- [ ] **I4** Syntax highlighting for both the DSL and SQL.
*(Refinement 2026-05-29, issue #8: column data types now carry a
surfaced as `CandidateKind::Function` (ADR-0022 Amendment 6).
The original "broad tab-completion" goal is met; any further
polish is incremental, not a missing core.)*
- [x] **I4** Syntax highlighting for both the DSL and SQL.
*(Verified 2026-06-07 — mis-marked `[ ]` despite being shipped:
`input_render.rs:64-113` lexes the input to styled byte-range
runs (`lex_to_runs_in_mode`) and renders them per-mode (DSL in
simple, SQL in advanced), with nine token classes in `theme.rs`
(`tok_keyword`, `tok_identifier`, `tok_string`, `tok_punct`,
`tok_flag`, `tok_error`, `tok_function`, `tok_type`) and
diagnostics overlaid (error/warning spans). Both surfaces are
highlighted; the core goal is met.
Refinement 2026-05-29, issue #8: column data types now carry a
dedicated `HighlightClass::Type` / `tok_type` colour, distinct from
identifiers and clause keywords — ADR-0022 Amendment 4; a further
refinement 2026-05-30, issue #15: SQL function-name candidates carry
@@ -172,24 +224,27 @@ since ADR-0027.)
## App-level commands (per ADR-0003)
- [ ] **A1** All canonical app-level commands implemented and
- [/] **A1** All canonical app-level commands implemented and
available in both modes: `save`, `save as`, `load`, `new`,
`rebuild`, `export`, `import`, `seed`, `replay`, `undo`,
`redo`, `mode`, `help`, `hint`, `quit`.
*(Progress: `quit`/`q`, `mode simple|advanced`, `help`,
`save`, `save as`, `load`, `new`, `rebuild`, `export`,
`import`, `replay` all implemented (Iterations 4 + 5;
`replay` via ADR-0024 Phase E — see U4). `seed` in the
seeding iteration; `undo` / `redo` in the U-series; `hint`
with H2.)*
*(Partial, verified 2026-06-07: 13 of 15 implemented and
available in both modes — `quit`/`q`, `mode simple|advanced`,
`help`, `save`, `save as`, `load`, `new`, `rebuild`, `export`,
`import`, `replay`, `undo`, `redo` (REGISTRY in
`grammar/app.rs:249-333`). **Missing: `seed`** (tracked as SD1)
**and `hint`** (tracked as H2) — neither is registered. A1
closes when SD1 + H2 land.)*
## DSL data commands
- [ ] **C1** Table operations: create / drop / rename.
*(Progress: create + drop done; **rename done on the advanced
surface** — `ALTER TABLE … RENAME TO`, ADR-0035 §6 / 4h. A simple-mode
rename-table verb is deliberately not provided — table rename is
advanced-mode only.)*
- [x] **C1** Table operations: create / drop / rename.
*(Verified 2026-06-07: create + drop done; **rename done on the
advanced surface** — `ALTER TABLE … RENAME TO`, ADR-0035 §6 / 4h
(`do_rename_table`, `db.rs:4821`). A simple-mode rename-table
verb is **deliberately** not provided — table rename is
advanced-mode only — so the requirement is satisfied as
designed, not partial.)*
- [x] **C2** Column operations: add / drop / rename / change
type. `drop column` and `rename column` use SQLite native
ALTER TABLE (3.35+ / 3.25+); `change column` routes through
@@ -343,29 +398,54 @@ since ADR-0027.)
*(Implemented per ADR-0014; auto-fills omitted shortid
columns and validates user-supplied values against the same
alphabet and length range.)*
- [ ] **T3** Compound primary keys handled end-to-end (DSL,
- [x] **T3** Compound primary keys handled end-to-end (DSL,
storage, display, FK reference).
*(Progress: DSL grammar (`with pk a(int),b(int)`), storage, and
table-info description are all present; the FK iteration
references single-column PKs only — compound-key FK references
remain pending.)*
*(Done 2026-06-09 via **ADR-0043**: the FK-reference leg now
works on both surfaces — DSL `add 1:n relationship from
P.(a, b) to C.(x, y)` and SQL `FOREIGN KEY (a, b) REFERENCES
P(x, y)` (bare `REFERENCES P` auto-expands to the full PK).
References the parent's full compound PK matched positionally,
per-pair type-compat (ADR-0011); the FK is engine-enforced,
persisted (`columns: [a, b]` in `project.yaml`, comma-joined in
metadata), shown symmetrically by `describe`, and `--create-fk`
creates one child column per parent PK column. The relationship
model went list-based through all six layers, single-column
behaviour preserved (commit `b14f019`); 12 integration tests in
`tests/it/compound_fk.rs` plus the existing single-column suite
as the regression net. The earlier-noted (2026-06-07) breakdown:*
*compound-PK **declaration** (`with pk a(int),b(int)`),
**storage** (`primary_key: Vec<String>`), and **display** were
already present and tested. The FK-reference leg — once an
ADR-first ~1520-site change across the relationship model — is
what ADR-0043 delivered (back-compat dropped by user decision, so
no migration was needed). Subset / non-PK (UNIQUE-target) FK
references stay out of scope.)*
## Visualizations
- [ ] **V1** Single-element views render in the output pane: a
- [/] **V1** Single-element views render in the output pane: a
selected table as its structure (columns, types, keys,
constraints); a selected relationship as two tables joined by
a line.
*(Progress: a basic structure view (column rows with SQLite
type names) is rendered after each successful DDL; pretty
rendering, selection nav, and relationship line-art pending —
see V4 for the broader direction.)*
- [ ] **V2** SQL query results render as a dynamic table view in
*(Partial, verified 2026-06-07: the **table-structure** half is
done — `output_render.rs:82-180` renders columns / types /
constraints / indexes in a box-drawing table, with relationship
metadata as `References:` / `Referenced by:` prose
(`A.col → B.col`). The **relationship-as-line-art** half — two
tables drawn side by side with a connecting line — is **not
implemented** (deferred per `output_render.rs` §5 OOS-1, ADR
pending). This is the relationship-visualisation piece that has
been repeatedly pushed away; it is the substantive open part of
V1. Selection-nav and the broader journal direction live in V4.)*
- [/] **V2** SQL query results render as a dynamic table view in
the output pane, with multiple result tabs supported.
*(Progress: a basic aligned-column data view is rendered for
`show data` and after every write (ADR-0014). Pretty
box-drawing tables with truncation/scroll handling, plus
multi-tab support, remain in V4 territory.)*
*(Partial, verified 2026-06-07: the **table view** is done —
`output_render.rs:38-72` `render_data_table` renders a
box-drawing frame with aligned columns (numeric right, text
left) and NULL/control-char sanitisation, for `show data` and
after every write (ADR-0014). **Missing: multiple result tabs**
— the output is a single `VecDeque<OutputLine>` with no tab
abstraction (same gap as S3). Multi-tab sits in V4 territory.)*
- [~] **V3** Full ER-diagram export (whole-database graph, viewed
outside the TUI) — low priority; design and ADR pending.
- [~] **V4** Output panel as a *scrollable per-session log* with
@@ -381,10 +461,34 @@ since ADR-0027.)
buffer is in, with new output snapping the view to the most
recent. The full V4 scope — smart structure rendering, log
styling, Markdown export, scroll indicator — remains pending.)*
- [ ] **V5** `show <kind> [<name>]` family of commands for
redisplaying schema info on demand. *(Progress: `show table
<name>` and `show data <Table>` implemented;
`show tables`, `show relationships`, etc. pending.)*
- [x] **V5** `show <kind> [<name>]` family of commands for
redisplaying schema info on demand.
*(Done 2026-06-07: `show table <name>` + `show data <Table>`
(single-item) plus the list-all family `show tables` /
`show relationships` / `show indexes` — the latter three landed
as `Command::ShowList { kind }` (one variant, `grammar/data.rs`),
a read-only worker `show_list` formatting count-headed lists from
the same helpers the items panel uses, with help + parse-usage
entries and 10 integration tests (`tests/it/show_list.rs`). The
one remaining member of the `[<name>]` clause — singular
per-item detail for relationships/indexes — is split out as
**V5a** below so it has a tracked home rather than living as a
footnote here.)*
- [x] **V5a** Singular per-item detail views `show relationship
<name>` / `show index <name>` — the `[<name>]` half of V5 for
the relationship and index kinds (the table kind already has
`show table <name>`).
*(Done 2026-06-07: folded into `Command::ShowList { kind, name:
Option<String> }` — `name: Some(_)` is the singular form. Two
grammar branches (`relationship <name>` / `index <name>`,
reusing the `Relationships`/`Indexes` completion sources for the
name slot), a worker `do_show_one` rendering a labelled detail
block (endpoints + ON DELETE/UPDATE for a relationship; table,
columns, uniqueness for an index) or a friendly "No relationship/
index named `X`." line, reusing the V5 `ShowList` render path.
Help + parse-usage entries + two ADR-0042 near-miss matrix rows;
5 added integration tests. V5's `[<name>]` clause is now
complete across all three kinds.)*
- [x] **V6** Copy the output panel to the system clipboard
(issue #11, ADR-0041). `copy` / `copy all` copy the whole
panel; `copy last` copies the most recent command's output.
@@ -557,14 +661,14 @@ since ADR-0027.)
migration sweep of all other user-facing strings, advanced-mode
SQL-error sanitization (§OOS-2), and `messages` persistence
(§OOS-3, awaits the settings ADR).)*
- [ ] **H1a** Strong syntax-help in parse errors. When the user
- [x] **H1a** Strong syntax-help in parse errors. When the user
types something near-correct (e.g. `insert into T ('Oli')` —
forgotten `values`; or `update T set x=1` — missing WHERE),
the error should *name the missing keyword or clause* rather
than just point at the unexpected character. This is a
separate effort from H1 (which targets database errors); it
targets parser errors. Pending — multiple targeted fixes
shipping piecemeal so far (e.g. `values` becoming optional in
targets parser errors. *(Done via the **ADR-0042** systematic
pass, 2026-06-06.)* Built piecemeal first (e.g. `values` becoming optional in
INSERT removes one such case; ADR-0024's typed value slots
give per-column-type rejection wording; `insert into T (col)`
with no `values` clause now flags "looks like Form A — add
@@ -589,19 +693,41 @@ since ADR-0027.)
`col`…"* message at typing time, counted against the user-fillable
columns, with `serial`/`shortid` auto-fill named; new keys
`diagnostic.insert_arity_mismatch_form_b_simple` /
`diagnostic.insert_arity_mismatch_all_auto`). A systematic pass is
still pending.
`diagnostic.insert_arity_mismatch_all_auto`). The **ADR-0042
systematic pass** then closed it: a per-command near-miss matrix
(`tests/it/parse_error_pedagogy.rs`) locks every entry word's
bare / missing-clause / wrong-token cases plus the committed
multi-forms, in both modes; friendlier labels landed (`add` →
`1:n relationship`; bare `select` → "a projection: …"); the
usage block became mode-aware (advanced shows the SQL forms
plus the still-valid DSL fallback forms, SQL-first); `with`
got its own CTE template; and `cross join … on` now teaches
that a CROSS JOIN takes no ON clause. The advanced-SQL
diagnostics that the survey thought missing (INSERT…SELECT
count, RETURNING column scope) were verified already present.
One low-priority residual is deferred by decision: at *submit*
time a non-projection expression position (bare `where `,
`returning `) still shows the raw expression first-set —
typing-time completion already offers the right candidates
there, so the payoff is small.
- [ ] **H2** `hint` provides contextual help for the current
input or the most recent error.
- [ ] **H3** `help` provides general reference and per-command
- [x] **H3** `help` provides general reference and per-command
help.
*(Progress: the `help` command lists currently-supported
commands + DSL grammar reference + types. As of ADR-0024
§help_id it is assembled by iterating the command REGISTRY
and translating each `CommandNode.help_id`, so a new command
appears automatically. A general reference and `help
<command>`-style detailed per-command help are still the
missing pieces.)*
*(Done 2026-06-07: the **general reference** is `help` (no arg) —
intro + the full command list (REGISTRY × `help_id`, so new
commands appear automatically) + the type reference + a footer
pointing at the focused form. **Per-command help** is `help
<command>` (H3's new piece): the HELP node took an optional
single-word topic (`BarePath`), `AppCommand::Help { topic }`,
and `note_help_topic` renders the block(s) of every command
sharing that entry word — so `help create` covers both create
forms — plus `help types` for the type reference and a friendly
"no help for `X`" pointer for an unknown topic. Help/usage
strings catalogued + key-registered; 9 integration tests
(`tests/it/help_command.rs`). A richer *narrative* overview
(modes, the `:` escape, syntax conventions) is reference-docs
scope, tracked under **DOC1** — not part of H3.)*
## CLI
@@ -636,7 +762,7 @@ since ADR-0027.)
## Documentation
- [ ] **DOC1** User- and student-facing reference
- [/] **DOC1** User- and student-facing reference
documentation under `docs/`: the DSL command surface,
the type system, and the boundaries of simple mode.
`docs/simple-mode-limitations.md` is the first piece —
@@ -644,6 +770,11 @@ since ADR-0027.)
reference. Distinct from in-app `help` (`H3`), the
interactive tutorial system (`TU1`), and the sharing
recipes under `E2`.
*(Partial, verified 2026-06-07: `docs/simple-mode-limitations.md`
exists (~55 lines, covers the WHERE-expression and
table-creation boundaries). **Missing:** a DSL
command-surface reference and a standalone type-system
reference under `docs/`.)*
## Testing (per ADR-0008)
@@ -657,14 +788,29 @@ since ADR-0027.)
- [~] **TT4** Tier 4: PTY-based end-to-end for the four critical
flows named in ADR-0008 (cold launch → DDL → quit; save →
reopen; export → import → rebuild; undo after DROP).
*(Verified 2026-06-07: **nothing is wired** — no
`portable-pty` / `expectrl` / `vt100` dependencies, no PTY test
files; ADR-0008 §Tier-4 is a specification only. The Tier-3
`tests/it/*_e2e.rs` files are synthetic event-loop tests, not
PTY. Correcting a stale `CLAUDE.md` line that read "Tier 4 is
wired only for the listed critical flows" — it was not wired at
all. Genuinely deferred.)*
- [ ] **TT5** CI runs all tiers on Linux, macOS, and Windows on
stable Rust.
## Cross-cutting
- [ ] **X1** Comprehensive logging via the project's logging
- [/] **X1** Comprehensive logging via the project's logging
infrastructure per `CLAUDE.md` (decision points, parameter
values, fallback paths).
*(Partial, verified 2026-06-07: the logging **harness** is
wired — `src/logging.rs` sets up file-backed `tracing` with an
env filter — but instrumentation is **sparse**: ~25 `tracing::`
call sites across the tree, concentrated in `runtime.rs` and
`undo.rs` and mostly error/warning on failure paths. The
decision-point / parameter-value / fallback-path coverage the
`CLAUDE.md` "log liberally" standard calls for — especially in
`db.rs`, the parser, and the executors — is largely absent.)*
- [~] **X2** Language: English-only for v1; multi-language is an
open question to revisit later.
- [~] **X3** Accessibility: TUI screen-reader support is