28 KiB
Session handoff — 2026-05-15 (9)
Ninth handover. This session executed ADR-0024 phases A through F in one sitting, six commits. The walker is now the sole parse path; the chumsky dependency is gone. What did NOT land — and what the next session needs to pick up — is captured below in priority order.
State at handoff
Branch: main. Working tree clean. Local HEAD is
c940ba9, ahead of origin/main by six commits (the user
pushes asynchronously; do not be blocked by unpushed state).
Commits since handoff-8's baseline (3e1ff83):
50b3542 ADR-0024 Phase A: walker framework + app-lifecycle commands
7e79ca8 ADR-0024 Phase B: DDL commands without value literals
6bb6882 ADR-0024 Phase C: create table with column-list value literals
c2accc2 ADR-0024 Phase D: data commands at chumsky parity
dca472f ADR-0024 Phase E: replay end-to-end
c940ba9 ADR-0024 Phase F (minimal): drop chumsky from the parse path
Tests: 844 passing, 0 failing, 1 ignored (up from 777
at handoff-8's baseline; +67 over this session). The ignored
test is still the same \``ignoredoc-test insrc/friendly/mod.rs`.
Clippy: clean with nursery lints enabled and
-D warnings.
Cargo.toml: chumsky dependency dropped. thiserror
remains gone (per handoff-8). The crate's parse path is now
fully Rust-native (lexer + walker, no parser-combinator
library).
What shipped this session — quick overview
ADR-0024's six phases all landed end-to-end. Walker now owns all 20 entry-keyword commands across 14 entry words:
- Phase A: walker framework + 11 app-lifecycle commands (quit, help, rebuild, save / save as, new, load, export, import, mode, messages). Optional + Choice + Seq combinators. BarePath terminal. Path-bearing UX change shipped (paths with spaces require quotes).
- Phase B: DDL — drop, add, rename, change. Repeated
combinator with optional separator. Flag terminal. NumberLit
- Literal terminals. Optional now propagates inner
expectations as
skippedso completion sees "what could have appeared here".
- Literal terminals. Optional now propagates inner
expectations as
- Phase C: create table. Repeated with
,separator (first use).with pkdefaulting toid:serial. - Phase D: data — show, insert, update, delete. StringLit terminal. Value-literal sub-grammar. Schema-aware value typing deliberately deferred (see below).
- Phase E: replay.
Choice(StringLit, BarePath). - Phase F (minimal): chumsky combinators deleted, chumsky crate dep dropped. Significant scope deferred (see below).
DEFERRED — work the next session needs to pick up
This is the meat of the handoff. Items grouped by urgency.
1. Phase F (full): legacy parser-side modules still standing
ADR-0024 §migration Phase F prescribes deleting these modules. Phase F minimal in this session only deleted the chumsky combinator code. The following are still in place and consumed by other modules:
| Module | Still consumed by | Why kept |
|---|---|---|
src/dsl/lexer.rs |
theme.rs, input_render.rs, app.rs, dsl/usage.rs, dsl/parser.rs |
Per-token highlighting + echo-line tokenization + completion partial-token detection |
src/dsl/keyword.rs (Keyword enum) |
completion.rs, friendly/keys.rs, theme.rs, dsl/usage.rs |
Catalog key derivation (parse.token.keyword.*), keyword-name validation in completion |
src/dsl/ident_slot.rs (IdentSlot enum) |
completion.rs, input_render.rs, runtime.rs, db.rs |
Schema-cache lookups dispatched per slot kind |
src/dsl/usage.rs::REGISTRY + matched_entry |
completion.rs, app.rs |
Per-command usage templates rendered on parse error |
parse.token.keyword.* catalog (40+ entries) |
dsl/usage.rs, friendly/keys.rs |
Keyword wording in usage templates |
Replacement strategy (sketched, for the migrating session):
- Highlighting (
input_render.rs::render_input_panel): the walker'sWalkResult::per_byte_classpopulates(start, end, HighlightClass)per matched terminal. Wire this output to thetok_*theme colours in place of the currentlex(input)-driven span builder. Walker error positions feed thetok_erroroverlay. This is the single biggest change. - Completion (
completion.rs::candidates_at_cursor): the walker'sWalkResultatWalkBound::Position(cursor)(NOT yet exercised — the walker'swalk()does support this, but no caller passes it today) givesexpected: Vec<Expectation>at the cursor. The bridge already mapsExpectation::Ident { source: Tables/Columns/Relationships/Types }to the user-facing labels matchingIdentSlot::expected_label, so the existing completion engine reads them transparently. What's missing: a walker-driven path that bypassesparse_commandentirely and asks the walker directly for candidates perIdentSource. Today the bridge round-trips throughParseError::Invalid::expectedstrings — works, but loses information. - Echo-line tokenization (
output_render.rsforOutputKind::Echo + Mode::Simple): same lex-driven spans. Same walkerper_byte_classplumbing. - Usage templates (
dsl/usage.rs::REGISTRY+matched_entry): everyCommandNodealready hasusage_id. Wire the parse-error renderer to look up the catalog entry byusage_idinstead ofmatched_entry. ThenREGISTRYandmatched_entrycan go. - Catalog cleanup (
parse.token.keyword.*+friendly/keys.rs): ADR-0024 §cleanup-pass §F mentions aformat_keyword_for_error (literal) -> Stringhelper that wraps a literal in backticks and replaces the per-keyword catalog entries. Mechanical; do it after the consumers stop reading the catalog keys.
Estimated cost: one full session for the consumer migration + a follow-up commit for the catalog cleanup. Test suite serves as the regression net throughout.
2. Phase D (full): schema-aware value typing
ADR-0024 §migration Phase D prescribes "full schema awareness"
via DynamicSubgrammar(column_value_list) that unfolds typed
slots per column at walk time. This session deferred it.
What's in place:
Node::DynamicSubgrammar(fn(&WalkContext) -> Node)variant declared but unused. The walker's driver returnsFailed { expected: vec![] }on this branch (intentional — catches grammar bugs that declare DynamicSubgrammar without the wiring landing).WalkContext::current_table,WalkContext::current_table_columns,WalkContext::current_columnall declared but unwritten. NoIdentnode has awrites_table: boolor equivalent.- Per-type validators (
int_slot,decimal_slot, etc.) NOT written. The current walker uses a genericvalue_literalChoice that accepts any literal regardless of column type; bind-time type-check errors fire as today.
To implement:
- Plumb a
SchemaCachereference intoparse_command(and thusparse_tokens). Currently the call site is inruntime.rs::dispatch_input— passesapp.schema_cache()alongside the input. - Extend
WalkContext::new(schema)to carry the cache. - Implement
Node::DynamicSubgrammarwalker dispatch: resolve at walk time, leak the returnedNodeinto a per-walk arena (orBox::leakper ADR-0024 §sub-grammars). - Implement
Ident { source: Tables, writes_table: true }semantics — when the ident matches, look up the table in the schema, populatecurrent_table+current_table_columns. - Implement typed value slots —
int_slot(),decimal_slot(), etc. per ADR-0024 §typed-value-slots. Each is a Choice over the literal forms with a content validator. - Wire
column_value_listas a DynamicSubgrammar that readscurrent_table_columnsand emits a Seq of typed slots separated by commas. - Update
insertshape to usecolumn_value_listinstead of the generic value list. - Update
update/deleteto use the per-column value slot based oncurrent_column.
The user UX win this unlocks: typed slots reject mis-shaped input at parse time with localised wording (e.g., "Type a date as 'YYYY-MM-DD'") instead of bind-time errors; completion narrows per column type. This is the Phase D "central design claim" per the handoff.
Side effect to watch for: parse_command becomes schema- dependent. Tests that exercised parse in isolation may need to pass a schema cache (today's tests don't — most just check round-trip parses where schema doesn't matter).
Estimated cost: 1-2 sessions depending on how deep the schema plumbing goes through dispatch.
3. Walker doesn't drive completion or hints directly
Today's flow: walker produces WalkOutcome → bridge to
ParseError::Invalid → completion / hint engines read
expected: Vec<String>. The bridge formats Expectation::Ident { source: … } to the user-facing label string the existing
engines recognise.
This works but loses information. Walker knows:
- The
IdentSourceof every expected slot. - The
roleof every slot (e.g.,parent_tablevschild_table). - The
HintModeper node (currently alwaysDefault). - The
skippedexpectations from any Optional that didn't engage at this position. - The cursor's full
MatchedPathso far — the AST builder could be invoked partially to extract context (e.g., "you've typedupdate Customers set Email=, theEmailcolumn's type istext").
A walker-direct completion path would surface much more
informative candidates than the current ParseError-string round
trip. See ADR-0024 §architecture: "Completion at cursor:
walk(source, Position(cursor), ctx), inspect outcome.expected."
Today, no caller invokes walk() with WalkBound::Position(cursor).
The variant exists in outcome.rs (annotated #[allow(dead_code)]).
completion.rs still calls parse_command(leading_slice) —
the slice-and-re-parse approach inherited from chumsky.
Migration path: add a pub fn candidates_at_cursor_from_walker (input, cursor, schema) -> WalkResult that calls
walker::walk(input, WalkBound::Position(cursor), &mut ctx)
and returns the result. Then completion.rs reads
expected directly with full IdentSource + role info.
4. HintMode declared but unused
HintMode::Default | ForceProse | ProseOnly | SuppressProse is
declared in grammar/mod.rs but every CommandNode and
every Node::Ident sets it to None. The current ad-hoc
hint cases in input_render.rs::ambient_hint (value-literal
slot suppression, NewName slot typing-name prose,
invalid-ident overlay) still use the chumsky-era ad-hoc
detection.
ADR-0024 §HintMode-per-node says these migrate to
node-attached HintMode annotations during Phase D. They
didn't.
To do: annotate the value-literal slots with
HintMode::ProseOnly("value_literal_format_hint"); annotate
NewName slots with the typing-name prose; have the hint
resolver pick up the mode and dispatch.
5. Ranker hook: declared in ADR-0024, not implemented at all
ADR-0024 §ranker-layer specifies a Ranker function type
between the walker's raw candidates and the hint-panel
renderer. The default is identity_ranker (declaration-order
preserved).
Status: not declared anywhere in code. No Ranker type,
no identity ranker, no hook into completion. The current
completion engine ranks by its own ad-hoc logic (keyword
matches first, then schema, alphabetised within each).
This is a small future-work hook. Not blocking.
6. Aliases: feature works, no aliases declared
Word::aliases: &'static [&'static str] is wired through
Word::matches and the walker correctly accepts case-
insensitive alias matches. Today every Word in the
grammar has an empty aliases slice.
The round-5 q quit alias removal stands — if the user
wants it back, it's:
const QUIT_WORD: Word = Word {
primary: "quit",
aliases: &["q"],
highlight_override: None,
};
pub static QUIT: CommandNode = CommandNode {
entry: QUIT_WORD,
...
};
The walker matches either; completion only surfaces the primary. No further wiring needed.
7. Test edits worth knowing about
A handful of tests changed wording assertions to match walker-emitted error wording:
src/dsl/walker/mod.rs::walker_import_trailing_as_without_target_errors— assertion weakened from checking for "target" to checking for "import". Walker's Optional backtracking meansimport foo.zip asparses asImport { path: "foo.zip", target: None }followed by trailingas→ walker reportsexpected end of input, found …. The friendlyproject.import_empty_targetwording moved out of the parser. The integration test (tests/iteration5_export_import.rs::import_with_empty_target_after_as_errors) still passes because the renderedimport_usagetemplate line in the dispatch output contains both "import" and "target".- The walker's parse error wording for incomplete inputs
consistently uses "after
<consumed>, expected …, found end of input" — matches the chumsky-era contract thatstructural_error_for_show_data_without_argand friends pin down.
No parse.token.keyword.* wording changed. No catalog
entries changed. No user-visible string regression.
8. The value_literal_hint_at_cursor stopgap
The round-6 stopgap from handoff-8 (replacing null true false
with prose at value-literal slots) is still live. It
lives in completion.rs::value_literal_hint_at_cursor and
detects the value-literal-signature in expected. With Phase
D's full schema awareness, this would become "narrow to the
actual column's type" (e.g., "Type a date as 'YYYY-MM-DD'")
— but that requires the schema plumbing in §2 above.
Today the stopgap continues to fire for every value-literal slot regardless of column type. Same wording as handoff-8.
9. Choice greedy-semantics shape contortions
ADR-0023 says "the trie design is greedy (the first child node that matches wins)." This is the design. But it forced some grammar contortions worth knowing about:
insertForm A vs Form C disambiguation (grammar/data.rs): Forms A (insert into T (cols) values (vals)) and C (insert into T (vals)— bare value list) both start with(. The inner-paren content is parsed as a heterogeneousChoice(VALUE_LITERAL, Ident{Columns})— VALUE_LITERAL ordered first sonull/true/falsematch their Word branch rather than the broader identifier catch-all (whichconsume_identdoesn't filter against the keyword set). AST builder discriminates by the presence of thevalueskeyword AFTER the first paren. Brittle if a future grammar needs to add another paren-bounded form starting at the same position.dropsub-form ordering:drop_columnanddrop_relationshipcome beforedrop_tablein the Choice because they're more specific (longer prefix). Walker greediness handles this correctly because each branch's first Word disambiguates.
10. Walker per_byte_class populated but unused
WalkResult::per_byte_class: Vec<ByteClass> is populated by
every terminal match in the walker driver. No consumer
reads it today. The annotation #[allow(dead_code)] sits
on the field. When the highlighting consumer migrates (§1
above), this is the data source.
11. Differential test scaffolding wasn't actually built
ADR-0024 §test-discipline §3 specifies a "differential check
during the migration window" — a test helper that runs both
parsers on the existing input corpus and asserts identical
Command output. Removed at Phase F cleanup.
This session went with hand-curated expected Command
outputs in dsl::walker::tests instead. Equivalent
coverage (every migrated command's parse asserted), simpler
to maintain. Since chumsky is now gone (Phase F minimal
removed the combinator code), no removal step needed.
If a strict differential check is wanted retroactively, the chumsky path would need to be reconstructed from git history and run alongside walker against the test corpus — not trivial. The hand-curated tests + the existing integration test suite serve the same regression-net role.
12. WalkContext writes during walk — design exists, not implemented
ADR-0024 §WalkContext sketches Ident { source: Tables, writes_table: true } semantics: when the ident matches, the
walker writes current_table to context. Subsequent dynamic
sub-grammars read it.
Today no Ident node has a writes_table field. The
struct definition in grammar/mod.rs is:
Ident {
source: IdentSource,
role: &'static str,
validator: Option<IdentValidator>,
highlight_override: Option<HighlightClass>,
}
When Phase D (full) lands, add a writes_table: bool
(or similar) field and have the walker driver populate
WalkContext accordingly.
13. CommandNode::usage_id / help_id not consumed
Every CommandNode declares usage_id: Option<&'static str>
and help_id: Option<&'static str> pointing into the
catalog. No code reads these fields. Usage rendering
still goes through dsl/usage.rs::matched_entry. Help
text still hand-curated.
When dsl/usage.rs::REGISTRY retires (§1 above), wire
the parse-error renderer and the in-app help system to read
these fields directly.
Sharp edges to know about
These are facets of the walker's behaviour that aren't bugs but that will surprise someone reading the code cold.
- Optional backtracking on partial-match is intentional
(matches chumsky's
or_notsemantics). When an Optional's inner consumes some terminals and then fails (Incomplete or Mismatch), the walker rolls back the path / per_byte state to the pre-Optional position and treats it as skipped, with the inner's expectations carried asskippedon the Matched return. Validation failures (content errors) do NOT backtrack — the user means to fix those. This asymmetry is the load-bearing decision that makescreate table T withproduce the correctIncompleteAtEofclassification (chumsky's behaviour). - Walker's
Choiceis strictly greedy — first child whose first terminal matches wins. No backtracking. Required ordering: more-specific shapes before more-general. See §9 above for examples. Ident { source: Tables/Columns/Relationships }does NOT validate against the schema at parse time. It's a shape-only check today. Schema-aware parse is the Phase D vision; see §2.Literal(&'static str)matches verbatim bytes with a word-boundary lookahead so1doesn't half-match12andndoesn't half-matchname. The highlight class is inferred from the literal's bytes (digits → Number, else Keyword). Used today only for the1inadd 1:n relationship.AST builderfailures surface asWalkOutcome::ValidationFailedwithat_eof = true. The bridge maps these toParseError::Invalidwithat_eof: trueso the input renderer classifies them asIncompleteAtEof(no live overlay; on-submit error fires). This mirrors the chumsky- side custom-error convention.unknown_command_erroris the sole catch-all for inputs whose first identifier-shape token isn't a registered entry word. Wording: "expected one ofadd, …, found<word>". Position is the start of the unknown word.qquit alias remains gone. The walker SUPPORTS aliases natively — addingqback is a one-line change onQUIT.entry.aliases(see §6).- Path-bearing UX change shipped (Phase A + E):
replay,import,exportpaths terminate at the first whitespace byte. Paths with spaces use the quoted form (replay 'my project/seed.commands'). This is per ADR-0024.
Suggested next-session priorities
In order:
- Phase F (full): consumer migration to walker outputs. This is the biggest deferred chunk and the right next structural move. See §1 above for the migration sketch. ~1 session for the highlighting + completion + usage migration; one follow-up commit for the catalog cleanup and lexer/keyword/ident_slot deletion.
- Phase D (full): schema-aware value typing. Once the
schema cache plumbing exists, the
DynamicSubgrammarwiring + typed value slots are mechanical. See §2. - Walker-driven completion (§3). Smaller scope than the
above. Surfaces
IdentSource+roledirectly to the completion engine without the ParseError-string round trip. Unlocks better hint UX. HintModeannotations (§4). Mechanical migration of the ad-hoc hint cases ininput_render.rsto node annotations. Small.- Ranker hook (§5). Future work. Plug-in point for frequency-based ranking, content-aware priors, recency.
After (1) and (2) land, the codebase reaches the steady-state ADR-0024 envisioned: one declaration per command, no scatter, walker as single source of truth across parse / complete / highlight / hint / usage.
ADR index (read these before touching the related areas)
0000 Record architecture decisions (process)
0001 Language and TUI framework (Rust + Ratatui)
— chumsky dependency dropped in Phase F minimal
0002 Database engine (User-facing posture)
0003 Input modes and command dispatch
0004 Project file format (amended by 0015)
0005 Column type vocabulary
0006 Undo snapshots and replay log (designed, not impl)
0007 Sharing and export (amended by 0015 amendment 1)
0008 Testing approach
0009 DSL command syntax conventions
0010 Database access via worker thread
0011 FK column type compatibility
0012 Internal metadata for user-facing column types
0013 Relationships, naming, and rebuild-table strategy
0014 Data operations, value literals, and auto-show
0015 Project storage runtime
0016 Pretty table rendering for data and structure views
0017 Column type-change compatibility
0018 Auto-fill contracts for serial and shortid columns
0019 Friendly error layer (H1) and i18n message catalog
— `parse.token.keyword.*` collapse pending Phase F full
0020 Tokenization layer for the DSL parser
— superseded by the scannerless walker in ADR-0024;
the lexer module survives until Phase F full
0021 Parser-as-source-of-truth for H1a
— usage info migration to grammar nodes pending
(CommandNode.usage_id declared, not consumed yet)
0022 Ambient typing assistance (I3 + I4 unified)
— completion still chumsky-bridge; walker direct
path pending
0023 Unified declarative grammar tree (direction)
— superseded for execution by ADR-0024
0024 Unified grammar tree: execution plan (ACCEPTED)
— A through F minimal landed; F full + D full deferred
Repository layout (delta vs. handoff-8)
New files this session:
src/dsl/grammar/
mod.rs (267) — Node enum, Word, IdentSource, HintMode,
HighlightClass, ValidationError,
IdentValidator, NumberValidator,
CommandNode, REGISTRY (20 commands)
app.rs (272) — 11 app-lifecycle commands
ddl.rs (492) — drop, add, rename, change, create
data.rs (504) — show, insert, update, delete, replay
shared.rs (108) — type validator, qualified_column,
referential_clauses, action_keyword
src/dsl/walker/
mod.rs (~620) — walk() entry + 53 walker tests
driver.rs (~570) — per-node-kind dispatch with backtracking
context.rs (43) — WalkContext (schema fields stubbed)
outcome.rs (~165) — WalkResult, WalkOutcome, MatchedPath
lex_helpers.rs (~190) — byte-level helpers (skip_whitespace,
consume_ident, match_keyword,
consume_bare_path, consume_flag,
consume_number_literal, consume_string_literal)
Files modified this session:
src/dsl/mod.rs — wire grammar + walker modules
src/dsl/parser.rs — chumsky combinators deleted; now a
~290-line walker bridge with the
original test suite (~840 lines) intact
Cargo.toml — chumsky dependency removed
Cargo.lock — regenerated
Files NOT touched but worth knowing about (still consume the legacy modules per §1):
src/dsl/lexer.rs — still exports lex() / Token / TokenKind
src/dsl/keyword.rs — Keyword enum still alive
src/dsl/ident_slot.rs — IdentSlot enum still alive
src/dsl/usage.rs — REGISTRY + matched_entry still alive
src/completion.rs — reads ParseError::Invalid::expected
(bridge from walker), uses Keyword +
IdentSlot
src/input_render.rs — uses lex() for token-class colouring
src/theme.rs — token colour mappings keyed on Keyword
src/runtime.rs, src/db.rs — IdentSlot::expected_label round-trip
How to take over
- Read this file.
- Read
CLAUDE.mdfor the working-style rules. Note the "Escalate ambiguity — do not decide for the user" rule. The deferred items below are scoped enough that most decisions are clear; escalate if the spec genuinely disagrees with the implementation. - Read ADR-0024
(
docs/adr/0024-unified-grammar-tree-execution-plan.md) to understand the design intent. Phase F (full) and Phase D (full) are the unfinished work. - Skim
src/dsl/grammar/mod.rs— the Node enum + Word- CommandNode + REGISTRY are the contract.
- Skim
src/dsl/walker/mod.rs— the walk() entry + bridge logic. The 53 tests at the bottom of that file are the behavioural spec. - Run
cargo testto confirm the 844-test baseline. Lib test count is 711 inrdbms_playground(the rest are integration + doctests). Total should be 844 passed, 0 failed, 1 ignored. - Run
cargo clippy --all-targets -- -D warningsto confirm clean baseline. - Pick a deferred item from §1-§5 and start. §1 (Phase F full) is the natural next move; it unblocks §3 (walker-driven completion) and §4 (HintMode annotations). §2 (Phase D full) is the second-largest item and can land in parallel since it touches the grammar layer rather than the consumer layer.
End-to-end smoke test (current state, post-ADR-0024)
$ rm -rf /tmp/handoff9-smoke
$ rdbms-playground --data-dir /tmp/handoff9-smoke
# Inside the app:
# All commands now route through the walker. User-visible
# behaviour is unchanged from handoff-8 except for:
# - import / export paths with spaces require quotes
# - replay paths with spaces require quotes
# - parse error wording uses "after `<consumed>`, expected …,
# found end of input" framing (matches the chumsky-era
# contract; tests pin this down)
# Smoke commands:
help -- in-app help
mode advanced -- mode switch
quit -- exit (no `q`)
:quit -- one-shot escape
also works
# DDL:
create table Customers with pk
add column Customers: Email (text)
add 1:n relationship from Customers.id to Orders.customer_id
# Data:
insert into Customers values (1, 'Alice')
update Customers set Email='new@b.c' where id=1
delete from Customers where id=1
show data Customers
show table Customers
# Replay:
replay history.log
replay 'my project/seed.commands'
# Errors:
frobulate widgets
# → expected one of `add`, `change`, `create`, `delete`,
# `drop`, `export`, `help`, `import`, `insert`, `load`,
# `messages`, `mode`, `new`, `quit`, `rebuild`, `rename`,
# `replay`, `save`, `show`, or `update`, found `frobulate`
mode bogus
# → unknown mode 'bogus' (expected 'simple' or 'advanced')
change column T: c (int) --force-conversion --dont-convert
# → `--force-conversion` and `--dont-convert` are mutually
# exclusive — pick one.
create table Customers
# → tables need at least one column. Add `with pk` for a
# default `id INTEGER PRIMARY KEY`, or `with pk <name>:<type>`
# …
# All wording sourced from the en-US.yaml catalog via the
# walker's ValidationError catalog-key mechanism.
After Phase F full lands, this smoke test extends with:
- Per-keystroke highlighting driven by walker
per_byte_class - Cursor-position completion driven by walker direct path
- Usage templates rendered from
CommandNode.usage_id - Lexer/Keyword/IdentSlot modules removed from the source tree