DSL parser, async DB worker, types, history, metadata, polish

Track 1 implementation plus polish round.

Parser (chumsky):
- Grammar-based DSL producing a typed Command AST.
- create table X with pk [name:type[,name:type...]] supports
  arbitrary names, any user type, compound PKs natively. Bare
  form errors with a friendly hint pointing at `with pk`.
- add column to table X: Name (type); drop table X.
- Required clauses use keyword grammar; -- reserved for opt-in
  flags (ADR-0009). Custom Rich reasons preferred when surfacing
  chumsky errors so unknown-type messages list valid alternatives.

Database (ADR-0010, ADR-0012):
- rusqlite + STRICT tables + foreign_keys=ON.
- Dedicated worker thread; mpsc Request inbox, oneshot replies.
- Typed DbError with friendly_message() hook for H1.
- Internal __rdbms_playground_columns metadata table preserves
  user-facing types across schema reads, atomically maintained
  alongside DDL via Connection transactions. list_tables hides
  it via the new __rdbms_ internal-table convention.

Types (ADR-0005, ADR-0011):
- All ten user-facing types: text, int, real, decimal, bool,
  date, datetime, blob, serial, shortid.
- Type::fk_target_type() for FK-side column-type rule
  (Serial->Int, ShortId->Text, others identity) -- foundation
  for the FK iteration.

App / Runtime / UI:
- update() stays pure-sync; runtime dispatches DSL via spawned
  tasks, results post back as AppEvent::Dsl*.
- Items panel renders live tables list; output panel shows the
  user-facing structure of the current table after each DDL.
- In-memory command history (Up/Down, draft preservation,
  consecutive-duplicate dedup) -- I2 partial.
- Mouse capture removed; terminal native text selection
  restored (toggle approach revisited when scroll/click
  features land).

Docs:
- ADRs 0009 (DSL syntax conventions), 0010 (DB worker),
  0011 (FK type compat), 0012 (internal metadata table).
- requirements.md progress notes; new V4 entry for the
  scrollable session-log + inline rich rendering + Markdown
  export direction.

Tests: 103 passing (91 lib + 12 integration), 0 skipped.
Clippy clean with nursery enabled.
This commit is contained in:
claude@clouddev1
2026-05-07 13:32:19 +00:00
parent 25a0f1260f
commit c1e52920eb
21 changed files with 3186 additions and 120 deletions
@@ -0,0 +1,87 @@
# ADR-0009: DSL command syntax conventions
## Status
Accepted
## Context
As the DSL grows, its commands need consistent surface
conventions. Without an explicit rule, every command would
invent its own way of expressing optional vs. required parts,
and the surface would drift toward an unreadable soup.
The decision is informed by experience from this iteration:
when we initially proposed `create table X --pk` the most
common form (a basic table with a primary key) required a `--`
flag, which is cosmetically wrong — `--` reads as "extra
option," and the most-used form should not look like one.
## Decision
The DSL surface follows three rules.
### 1. Required clauses use keyword grammar
Required parts of a command are written in plain words and
read like English. Examples:
- `create table <Name> with pk <name>:<type>`
- `add column to table <Name>: <Name> (<Type>)`
- `drop table <Name>`
The `with` clause format is the canonical pattern for
attaching required structural information to an entity-creating
command, and is reusable: future iterations may add `with
index`, `with check`, etc. Multiple `with` clauses on the same
command are allowed in principle.
### 2. Optional flags use `--prefix`
Flags signal "I am asking for an extra capability or
non-default behaviour." Examples planned for later iterations:
- `add 1:n relationship on Customers.Id=Orders.CustId --create-fk`
(auto-creates the FK column instead of requiring it to exist)
- *(future)* `--rename-on-clash`, `--no-strict`, etc.
A user reading "with pk id:serial" sees only what's needed; a
user reading "...with pk id:serial --some-flag" sees that they
have asked for something beyond default. The visual distinction
is intentional.
### 3. One sigil only — `:` for the simple-mode advanced escape
Per ADR-0003, prefixing a single line with `:` in simple mode
treats that one submission as if it were entered in advanced
mode. This is the only sigil in the system. App-level commands,
DSL commands, and SQL all use plain words.
### Lexical rules
- **Keywords are case-insensitive.** `CREATE TABLE Customers
WITH PK email:TEXT` is equivalent to `create table Customers
with pk email:text`.
- **Identifiers are case-preserving.** `Customers` and
`customers` are different identifiers if a backend would
treat them as such (we follow SQLite's case-insensitive
identifier rules at the schema level but preserve the user's
written casing in display).
- **Whitespace is liberal.** Any amount of horizontal whitespace
between tokens is accepted, including around punctuation
(`,`, `:`, `(`, `)`).
## Consequences
- The basic, most-common form of any command remains readable
and free of cosmetic punctuation. New users see only words.
- Optional adornments are visually distinct, encouraging
discoverability of advanced features without forcing them on
beginners.
- New commands inherit a uniform shape: keyword-based clauses
for required parts, `--` flags for opt-ins. Drift is bounded
by this rule.
- The grammar implementation (`chumsky`) maps cleanly onto this
structure: a `with_clause` rule can be reused across
commands, and flag parsing has a single representation when
it lands.