# 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 `\`\`\`ignore` doc-test in `src/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 `skipped` so completion sees "what could have appeared here". - **Phase C:** create table. Repeated with `,` separator (first use). `with pk` defaulting to `id: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's `WalkResult::per_byte_class` populates `(start, end, HighlightClass)` per matched terminal. Wire this output to the `tok_*` theme colours in place of the current `lex(input)`-driven span builder. Walker error positions feed the `tok_error` overlay. This is the single biggest change. - **Completion** (`completion.rs::candidates_at_cursor`): the walker's `WalkResult` at `WalkBound::Position(cursor)` (NOT yet exercised — the walker's `walk()` does support this, but no caller passes it today) gives `expected: Vec` at the cursor. The bridge already maps `Expectation::Ident { source: Tables/Columns/Relationships/Types }` to the user-facing labels matching `IdentSlot::expected_label`, so the existing completion engine reads them transparently. What's missing: a walker-driven path that bypasses `parse_command` entirely and asks the walker directly for candidates per `IdentSource`. Today the bridge round-trips through `ParseError::Invalid::expected` strings — works, but loses information. - **Echo-line tokenization** (`output_render.rs` for `OutputKind::Echo + Mode::Simple`): same lex-driven spans. Same walker `per_byte_class` plumbing. - **Usage templates** (`dsl/usage.rs::REGISTRY` + `matched_entry`): every `CommandNode` already has `usage_id`. Wire the parse-error renderer to look up the catalog entry by `usage_id` instead of `matched_entry`. Then `REGISTRY` and `matched_entry` can go. - **Catalog cleanup** (`parse.token.keyword.*` + `friendly/keys.rs`): ADR-0024 §cleanup-pass §F mentions a `format_keyword_for_error (literal) -> String` helper 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 returns `Failed { 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_column` all declared but unwritten. No `Ident` node has a `writes_table: bool` or equivalent. - Per-type validators (`int_slot`, `decimal_slot`, etc.) NOT written. The current walker uses a generic `value_literal` Choice that accepts any literal regardless of column type; bind-time type-check errors fire as today. **To implement:** 1. Plumb a `SchemaCache` reference into `parse_command` (and thus `parse_tokens`). Currently the call site is in `runtime.rs::dispatch_input` — passes `app.schema_cache()` alongside the input. 2. Extend `WalkContext::new(schema)` to carry the cache. 3. Implement `Node::DynamicSubgrammar` walker dispatch: resolve at walk time, leak the returned `Node` into a per-walk arena (or `Box::leak` per ADR-0024 §sub-grammars). 4. Implement `Ident { source: Tables, writes_table: true }` semantics — when the ident matches, look up the table in the schema, populate `current_table` + `current_table_columns`. 5. 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. 6. Wire `column_value_list` as a DynamicSubgrammar that reads `current_table_columns` and emits a Seq of typed slots separated by commas. 7. Update `insert` shape to use `column_value_list` instead of the generic value list. 8. Update `update`/`delete` to use the per-column value slot based on `current_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`. The bridge formats `Expectation::Ident { source: … }` to the user-facing label string the existing engines recognise. This works but loses information. Walker knows: - The `IdentSource` of every expected slot. - The `role` of every slot (e.g., `parent_table` vs `child_table`). - The `HintMode` per node (currently always `Default`). - The `skipped` expectations from any Optional that didn't engage at this position. - The cursor's full `MatchedPath` so far — the AST builder could be invoked partially to extract context (e.g., "you've typed `update Customers set Email=`, the `Email` column's type is `text`"). 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: ```rust 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 means `import foo.zip as ` parses as `Import { path: "foo.zip", target: None }` followed by trailing `as ` → walker reports `expected end of input, found …`. The friendly `project.import_empty_target` wording moved out of the parser. The integration test (`tests/iteration5_export_import.rs::import_with_empty_target_after_as_errors`) still passes because the rendered `import_usage` template line in the dispatch output contains both "import" and "target". - The walker's parse error wording for incomplete inputs consistently uses "after ``, expected …, found end of input" — matches the chumsky-era contract that `structural_error_for_show_data_without_arg` and 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: - **`insert` Form 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 heterogeneous `Choice(VALUE_LITERAL, Ident{Columns})` — VALUE_LITERAL ordered first so `null`/`true`/`false` match their Word branch rather than the broader identifier catch-all (which `consume_ident` doesn't filter against the keyword set). AST builder discriminates by the presence of the `values` keyword AFTER the first paren. **Brittle if a future grammar needs to add another paren-bounded form starting at the same position.** - **`drop` sub-form ordering**: `drop_column` and `drop_relationship` come before `drop_table` in 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` 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: ```rust Ident { source: IdentSource, role: &'static str, validator: Option, highlight_override: Option, } ``` 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_not` semantics). 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 as `skipped` on the Matched return. **Validation failures (content errors) do NOT backtrack** — the user means to fix those. This asymmetry is the load-bearing decision that makes `create table T with` produce the correct `IncompleteAtEof` classification (chumsky's behaviour). - **Walker's `Choice` is 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** so `1` doesn't half-match `12` and `n` doesn't half-match `name`. The highlight class is inferred from the literal's bytes (digits → Number, else Keyword). Used today only for the `1` in `add 1:n relationship`. - **`AST builder` failures surface as `WalkOutcome::ValidationFailed` with `at_eof = true`**. The bridge maps these to `ParseError::Invalid` with `at_eof: true` so the input renderer classifies them as `IncompleteAtEof` (no live overlay; on-submit error fires). This mirrors the chumsky- side custom-error convention. - **`unknown_command_error` is the sole catch-all** for inputs whose first identifier-shape token isn't a registered entry word. Wording: "expected one of `add`, …, found ``". Position is the start of the unknown word. - **`q` quit alias remains gone.** The walker SUPPORTS aliases natively — adding `q` back is a one-line change on `QUIT.entry.aliases` (see §6). - **Path-bearing UX change shipped (Phase A + E):** `replay`, `import`, `export` paths 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: 1. **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. 2. **Phase D (full): schema-aware value typing.** Once the schema cache plumbing exists, the `DynamicSubgrammar` wiring + typed value slots are mechanical. See §2. 3. **Walker-driven completion** (§3). Smaller scope than the above. Surfaces `IdentSource` + `role` directly to the completion engine without the ParseError-string round trip. Unlocks better hint UX. 4. **`HintMode` annotations** (§4). Mechanical migration of the ad-hoc hint cases in `input_render.rs` to node annotations. Small. 5. **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 1. **Read this file.** 2. **Read `CLAUDE.md`** for 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. 3. **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. 4. **Skim `src/dsl/grammar/mod.rs`** — the Node enum + Word + CommandNode + REGISTRY are the contract. 5. **Skim `src/dsl/walker/mod.rs`** — the walk() entry + bridge logic. The 53 tests at the bottom of that file are the behavioural spec. 6. **Run `cargo test`** to confirm the 844-test baseline. Lib test count is 711 in `rdbms_playground` (the rest are integration + doctests). Total should be 844 passed, 0 failed, 1 ignored. 7. **Run `cargo clippy --all-targets -- -D warnings`** to confirm clean baseline. 8. **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 ``, 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 :` # … # 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