Commit Graph

15 Commits

Author SHA1 Message Date
claude@clouddev1 90e3f5dbfb Insert grammar: Form C type-awareness via lookahead (ADR-0024 §Phase D)
Form C (`insert into T (vals)`) shared the `(` opener with Form A,
so its paren was an untyped Repeated(Choice(literal, ident)) — values
weren't type- or count-checked at parse time (handoff-12 §2.2).

New Node::Lookahead variant: a factory that peeks the source. The
insert first-paren factory inspects the first token — a value literal
routes the contents through the typed column_value_list (Form B
dispatch contract: per-non-auto-column typed slots); an identifier or
empty paren routes to a Form A column-name list. So Form C now gets
the same per-column typed slots, hints, and parse-time type/count
checking Form B has.

The explicit-Choice-branch split is impossible here (committed-choice
semantics commit after `(` matches); lookahead is the only route, and
DynamicSubgrammar factories couldn't see the source. Node::Lookahead
is not memoized — its output depends on source — but it returns only
a small node (a Repeated, or a thin DynamicSubgrammar wrapper that
delegates to the memoized column_value_list).

`insert into T (` now cleanly shows Form A column candidates instead
of mixed Form-A/C suggestions. Form C matrix tests updated for the
type-aware behaviour.
2026-05-15 22:27:53 +00:00
claude@clouddev1 911a537a83 Walker: node-attached HintMode via Node::Hinted (ADR-0024 §HintMode-per-node)
Replaces the hint resolver's signature-matching (does the expected set
contain all five literal forms? an Ident{NewName}?) with a grammar-
declared annotation. New Node::Hinted { mode, inner } wrapper; the
walker records the mode in WalkContext::pending_hint_mode on entry and
clears it on any successful match (cursor moved past the slot — this
also undoes the leak where a failed Hinted branch of a Choice would
otherwise strand a stale mode). The resolver reads pending_hint_mode
directly.

Value-literal fallback slots carry ProseOnly; NewName ident slots carry
ForceProse. hint_mode_at_input_inner now delegates to
hint_resolution_at_input — one resolution path, no duplicated logic.
No behaviour change; the typing-surface matrix guards it.
2026-05-15 21:58:22 +00:00
claude@clouddev1 b3f1a20652 Phase D: insert value list mirrors do_insert's user_cols contract
Bug: hint at \`insert into Customers values (\` for a Customers
table with id:serial PK suggested typing an integer for \`id\`,
but the dispatch path (\`db::do_insert\`) deliberately doesn't
accept user-supplied values for auto-generated columns in
Form B. The grammar prompted for a value the dispatch would
refuse.

The fix aligns Phase D's \`column_value_list\` dynamic sub-grammar
with do_insert's three forms (ADR-0014 + ADR-0018 §3):

- **Form A** \`insert into <T> (col1, col2, …) values (…)\` —
  user explicitly lists columns. Slot list mirrors that
  selection; serial / shortid columns CAN appear if the user
  lists them.
- **Form B** \`insert into <T> values (…)\` — bare values. Slot
  list = non-auto-generated columns of the table in
  declaration order. Serial / shortid get auto-filled by the
  dispatch; the grammar doesn't prompt for them.
- **Form C** \`insert into <T> (v1, v2, …)\` — bare value list.
  Not affected by this change (column_value_list isn't on this
  path; Form C's literals route through the schemaless
  INSERT_PAREN_LIST).

Implementation:

\`WalkContext.user_listed_columns: Option<Vec<String>>\` — when
\`Some\`, signals Form A; \`None\` is Form B. Populated by walking
the first paren's column-list idents.

\`Node::Ident.writes_user_listed_column: bool\` — new field;
\`true\` on the INSERT_PAREN_ITEM's Ident child. When the
walker matches that ident in Form A, it appends the
schema-canonical column name (case-corrected against the
schema) to user_listed_columns.

\`column_value_list\` factory:
- If user_listed_columns is Some → resolve each name from the
  schema; one typed slot per listed column.
- Else → filter current_table_columns to non-auto-generated;
  one typed slot per remaining column.
- Empty result → fall back to the schemaless value-literal
  list (a serial-only table in Form B has nothing for the
  user to type).

Tests:
- New \`phase_d_insert_form_b_skips_serial_column\` confirms the
  bug: \`insert into Customers values (1, 'Alice')\` against a
  Customers with serial id rejects at parse time (Form B
  expects 1 value for Name, not 2).
- New \`phase_d_insert_form_a_accepts_serial_when_listed\`
  confirms \`insert into Customers (id, Name) values (1, 'Alice')\`
  works.
- New \`phase_d_insert_form_a_filters_to_user_listed_columns\`
  confirms partial Form A (\`(Name) values ('Alice')\`).
- Updated \`phase_d_insert_with_schema_accepts_typed_values_per_column\`
  to match the new Form B contract (2 user-typed values, not 3).
- Updated typed-hint test matrix split into form-B (8 types)
  and form-A (serial / shortid).
- New \`typed_hint_form_b_skips_serial_column_to_generic_or_text_neighbor\`
  pins the fallback behavior for a serial-only table.

For the user: \`insert into Customers values (\` for a Customers
with \`(id:serial, Name:text, Email:text)\` now hints
\`for \`Name\`: Type a quoted string …\` (skipping id entirely)
and accepts exactly 2 values. To set the serial explicitly,
use Form A: \`insert into Customers (id, Name, Email) values
(1, 'Alice', 'a@b.c')\`.

Tests: 851 passing, 0 failing, 1 ignored. Clippy clean.
2026-05-15 18:45:47 +00:00
claude@clouddev1 c485189da8 ADR-0024 Phase D: include column name in value-slot hint prose
User-facing improvement: typing into a value slot now surfaces
the column name in the hint. The hint at `insert into Customers
values (` (first column id:int) reads "for `id`: Type an
integer (e.g. 42, -7) or null" instead of the generic
"Type an integer …" prose. After `1, ` the panel updates to
the second column ("for `Name`: Type a quoted string …"). The
same applies to `update T set Email=` and `delete from T where
ts=` — the catalog wrapper threads the column name through.

Implementation:

**`Node::TypedValueSlot.column_name: Option<&'static str>`**
(new field, `src/dsl/grammar/mod.rs`). When `Some`, walker
writes `WalkContext::pending_value_column` on entry; clears
along with `pending_value_type` on inner success.

**Walker driver writes both names** (`src/dsl/walker/driver.rs`):
- `Node::TypedValueSlot` dispatch reads `column_name` and
  populates `pending_value_column`.
- `Ident { writes_column: true }` dispatch also writes
  `pending_value_column` (using the schema-canonical name when
  available, falling back to the user's spelling) so update
  set / where positions surface the column name.

**Shared sub-grammars** (`src/dsl/grammar/shared.rs`):
- New `slot_for_column(ty, name)` builds a `TypedValueSlot`
  with the embedded leaked column name. Used by
  `column_value_list`.
- New `slot_inner_for_type(ty)` returns just the Choice
  (without TypedValueSlot wrapper) for slot_for_column to
  rebuild.
- `column_value_list` factory now constructs per-column slots
  via `slot_for_column(col.user_type, &col.name)`. Each slot
  leaks its column name string with the same per-walk Box::leak
  pattern the rest of dynamic dispatch uses.

**`WalkContext::pending_value_column: Option<String>`** (new
field, `src/dsl/walker/context.rs`). Pairs with
`pending_value_type` to give the hint resolver both pieces.

**Single-walk hint resolver** (`src/dsl/walker/mod.rs`):
- New `HintResolution { mode: HintMode, column: Option<String> }`
  struct.
- New `hint_resolution_at_input(source, schema) -> Option<
  HintResolution>` runs one walk and reports both pieces. The
  ambient_hint dispatch composes per-column prose from the
  result.
- Existing `hint_mode_at_input` / `hint_mode_at_input_with_schema`
  preserved as thinner wrappers for tests / future callers
  that don't need the column name.

**Catalog wrapper** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- New `hint.value_slot_for_column: "for `{column}`: {detail}"`
  prefixes the per-type prose with the actual column name when
  the walker has it bound. Schemaless fallback continues to use
  the generic value-literal prose with no column prefix.

**ambient_hint composes** (`src/input_render.rs`): consults
`hint_resolution_at_input`; when `column` is `Some`, wraps the
type prose through `hint.value_slot_for_column`; otherwise
emits the bare type prose.

Tests (846 total, 0 failing):
- 4 new input_render tests assert column names appear in the
  prose at insert/update/where positions plus the
  second-insert-value position (proves column tracking advances
  with comma).
- All existing tests pass unchanged — the column-name addition
  is layered on top of the type-only prose path.

Clippy clean.
2026-05-15 18:33:52 +00:00
claude@clouddev1 82955679ca ADR-0024 Phase D: per-column-type hint prose at value slots
The Phase D commit landed parse-time validation but not the
user-facing payoff — per-column-type hints. Typing
`insert into Customers values (` rightfully expected a hint
like "Type an integer (e.g. 42, -7) or null" at an int column.
This commit closes that gap.

End-to-end:

**`Node::TypedValueSlot { ty, inner }`** (new variant in
`src/dsl/grammar/mod.rs`):
- Walker walks `inner` to consume the literal but tags
  `WalkContext::pending_value_type = Some(ty)` on entry, then
  clears it on a successful inner match. Positions BETWEEN
  slots (`insert into T values (1` mid-input) thus don't carry
  a stale hint type.

**Typed slot factories wrapped in `TypedValueSlot`**
(`src/dsl/grammar/shared.rs`):
- `INT_SLOT`, `REAL_SLOT`, `DECIMAL_SLOT`, `BOOL_SLOT`,
  `TEXT_SLOT`, `DATE_SLOT`, `DATETIME_SLOT`, `BLOB_SLOT`,
  `SERIAL_SLOT`, `SHORTID_SLOT` — each pairs an inner literal
  Choice with its `Type` so the walker can tag context.
- `slot_for_type(ty)` dispatches to the appropriate constant.
- Bug fix: `ShortId` previously dispatched to `INT_SLOT` (a
  pre-Phase-D holdover from the chumsky-side generic
  fallback). `shortid` columns store base58 text (ADR-0011
  fk_target_type shortid → text); the corrected slot accepts
  `StringLit` or `null`.

**Schema-aware hint resolver** (`src/dsl/walker/mod.rs`):
- `hint_mode_at_input_with_schema(source, &SchemaCache) ->
  Option<HintMode>` is the new public entry point. Reads
  `pending_value_type` from the walker's WalkContext and
  emits `HintMode::ProseOnly("hint.value_slot_<type>")` —
  one per Type.
- The schemaless `hint_mode_at_input(source)` falls back to
  the generic `hint.value_literal_slot` at value-literal slots
  (no per-type narrowing without a schema).
- `catalog_key_for_value_type(ty)` is the type → key
  dispatcher.

**Catalog entries** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- 10 new `hint.value_slot_<type>` keys with per-type prose:
  - int/serial → "Type an integer (e.g. 42, -7) or null"
  - real/decimal → "Type a number (e.g. 3.14, -0.5) or null"
  - bool → "Type true, false, or null"
  - text → "Type a quoted string (e.g. 'Alice') or null"
  - date → "Type a quoted date as 'YYYY-MM-DD' or null"
  - datetime → "Type a quoted datetime as 'YYYY-MM-DD
    HH:MM:SS' or null"
  - blob → "Type a quoted blob literal or null"
  - shortid → "Type a quoted shortid (or omit to auto-generate)
    or null"

**Ambient-hint dispatch** (`src/input_render.rs::ambient_hint`):
- Passes the SchemaCache through to
  `hint_mode_at_input_with_schema`, so the live hint panel
  surfaces per-column-type prose as the user types into a
  value slot.

Tests:
- 8 walker-side tests cover insert / update / where typed-slot
  hint dispatch, mid-value no-stale-hint behaviour, and a
  full-coverage routing matrix for every `Type` variant.
- 4 input_render integration tests cover the end-to-end
  ambient_hint path: insert first/second value, update set
  value, and the schemaless fallback to generic prose.

Tests: 842 passing, 0 failing, 1 ignored. Clippy clean.

For the user: typing `insert into Customers values (` against
a Customers table whose first column is `id:int` now shows
"Type an integer (e.g. 42, -7) or null" in the hint panel,
replacing the previous generic value-literal prose. After
typing `1, `, the panel updates to whatever the second column
requires — "Type a quoted string (e.g. 'Alice') or null"
for text, "Type a quoted date as 'YYYY-MM-DD'" for date, etc.
2026-05-15 18:05:38 +00:00
claude@clouddev1 abebd7944f ADR-0024 Phase D (full): schema-aware value typing
Schema-aware typed value slots — the central design claim of
ADR-0024 §Phase D. Insert / update / delete value slots now
dispatch on the user-facing column type at parse time, rejecting
mis-shaped input with localised wording instead of waiting for
the bind-time error.

What changed:

**SchemaCache extension** (`src/completion.rs`):
- New `TableColumn { name, user_type }` for per-table column
  metadata.
- `SchemaCache.table_columns: HashMap<String, Vec<TableColumn>>`.
- `SchemaCache::columns_for_table(name)` — case-insensitive
  lookup, mirrors the walker's case-insensitive entry-word
  resolution.

**WalkContext schema plumbing** (`src/dsl/walker/context.rs`):
- `WalkContext<'a>` gains a lifetime and a `schema: Option<&'a
  SchemaCache>`. `WalkContext::new()` keeps the schemaless
  default; `with_schema(s)` is the new schema-aware constructor.

**Parser entry point** (`src/dsl/parser.rs`):
- `parse_command_with_schema(input, schema)` is the new public
  schema-aware variant. `parse_command(input)` becomes a thin
  wrapper that delegates with `None` for back-compat.
- Internal `try_walker_route` accepts an `Option<&SchemaCache>`
  and threads it into the WalkContext.

**Node::Ident writes_table/writes_column** (`src/dsl/grammar/mod.rs`):
- Two new fields on `Node::Ident`. When `writes_table: true` and
  `source: Tables`, the walker writes the matched ident's name
  into `current_table` and resolves `current_table_columns`
  against the schema cache. When `writes_column: true` and
  `source: Columns`, the walker writes the resolved
  `TableColumn` into `current_column`.

**Walker driver DynamicSubgrammar dispatch** (`src/dsl/walker/driver.rs`):
- The `Node::DynamicSubgrammar(factory)` branch now resolves the
  factory at walk time and `Box::leak`s the result so its inner
  static-slice fields (Choice/Seq) have the lifetime the walker
  expects (per ADR-0024 §sub-grammars). The leak is bounded by
  command-shape complexity per walk; per-walk arena is a future
  optimisation.
- `walk_ident` extends to perform the schema writes when the
  flags are set.

**Typed value slot factories + dynamic sub-grammars** (`src/dsl/grammar/shared.rs`):
- `int_slot` / `real_slot` / `decimal_slot` / `bool_slot` /
  `text_slot` / `date_slot` / `datetime_slot` / `blob_slot` —
  one per `Type`. Each accepts the appropriate literal kind plus
  `null`; integer-only validator rejects `3.14` at int columns;
  decimal validator pins numeric shape.
- `slot_for_type(ty) -> Node` is the dispatcher.
- `current_column_value(ctx) -> Node` is the dynamic sub-grammar
  for `set col = …` and `where col = …` values; reads
  `current_column` and dispatches via `slot_for_type`.
- `column_value_list(ctx) -> Node` is the dynamic sub-grammar
  for `insert into T values (…)`; reads `current_table_columns`
  and unfolds a Seq of typed slots separated by commas.
- Both fall back to the schemaless `VALUE_LITERAL` choice when
  the context lacks the schema-resolved entries — keeps
  schemaless `parse_command` callers (tests, replay path)
  working.

**Data-command grammar wires the new types** (`src/dsl/grammar/data.rs`):
- `TABLE_NAME_INSERT` / `TABLE_NAME_WRITES` (new): table-name
  slots that set `writes_table: true`. Used by insert / update /
  delete to populate `current_table_columns`.
- `SET_COLUMN` / `FILTER_COLUMN` (new): column-name slots in
  `set col=…` / `where col=…` set `writes_column: true`.
- `INSERT_VALUES_LIST` becomes `DynamicSubgrammar(column_value_list)`.
- `UPDATE_ASSIGNMENT` and `WHERE_CLAUSE` use
  `PER_COLUMN_VALUE = DynamicSubgrammar(current_column_value)`.

**Runtime plumbs schema-with-types** (`src/runtime.rs`):
- `refresh_schema_cache` calls `describe_table` for each table
  and populates `SchemaCache::table_columns` with
  `TableColumn { name, user_type }` entries. Best-effort: a
  `describe_table` miss leaves that table unpopulated and the
  walker falls back to schemaless dispatch.

**App dispatches with schema** (`src/app.rs`):
- `dispatch_dsl` routes through `parse_command_with_schema(&self
  .schema_cache, …)` so live typing/dispatch sees the typed
  slots. The replay path stays schemaless (deferred — replay
  bind-time errors still catch type mismatches).

**Catalog** (`src/friendly/strings/en-US.yaml`, `src/friendly/keys.rs`):
- New `parse.custom.bind_type_mismatch` entry with `{found}` and
  `{expected}` placeholders. Surfaced by the int_slot /
  decimal_slot validators.

Tests:
- 11 new walker-side Phase D tests cover insert / update /
  delete with schemas — typed acceptance per column, decimal
  rejection at int columns, null acceptance at any slot,
  multi-assignment per-column dispatch, schemaless fallback.
- The pre-existing `parse_command(input)` test suite (no
  schema) still passes — the fallback path is behaviour-
  preserving.
- 828 passing total, 0 failing, 1 ignored. Clippy clean.
2026-05-15 17:45:56 +00:00
claude@clouddev1 85817791dc ADR-0024 HintMode dispatch via walker_hint_mode_at_input
Adds the `HintMode` dispatch layer the ADR specified: the
ambient-hint resolver now consults a single
`walker::hint_mode_at_input(source) -> Option<HintMode>` to
decide between the prose / candidates ladder, rather than
discovering each slot kind through three separate post-hoc
helpers (`value_literal_hint_at_cursor`,
`typing_name_at_cursor`, and so on).

Behaviour at slot positions today:

- **Value-literal slot** (`null`/`true`/`false`/number/string
  all in the expected set) → `HintMode::ProseOnly
  ("hint.value_literal_slot")`. The ambient-hint ladder
  emits the catalog prose at empty prefix; once the user types
  a partial (`n`, `tr`, `fa`) the partial check declines and
  normal candidate completion takes over.
- **NewName ident slot** → `HintMode::ForceProse
  ("hint.ambient_typing_name")`. The ladder still consults
  `typing_name_at_cursor` to learn what comes after the name
  (the post-name probe is unchanged); `ForceProse` is the
  declarative tag telling the resolver *that* we're in this
  mode.

`HintMode` itself gains `PartialEq + Eq` for tests, and
its docstring is rewritten to describe the live semantics.

This is the structural shape ADR-0024 §HintMode-per-node
describes: one slot → one hint mode → one dispatch arm. The
detection inside `hint_mode_at_input` is transitional — it
pattern-matches the walker's expected-set today, which is
exactly what the previous ad-hoc detectors did. Phase D will
replace the signature match with node-attached `HintMode`
annotations on the typed value slots (so `date_slot`,
`int_slot`, etc. each carry a type-specific catalog key).

Two helpers move into `input_render.rs`:
- `hint_leading_slice(input, cursor)` mirrors the look-back
  used by `candidates_at_cursor` so the hint resolver sees the
  same token-boundary view of the world.
- `cursor_partial_is_empty(input, cursor)` distinguishes
  empty-prefix from in-progress identifier shapes.

8 new walker tests pin the hint-mode resolver across
value-literal-after-paren, value-literal-after-set-assign,
value-literal-in-where, two NewName-slot cases, the
entry-keyword position, the complete-command position, and
the schema-ident position.

Tests: 817 passing, 0 failing, 1 ignored. Clippy clean.
2026-05-15 17:32:17 +00:00
claude@clouddev1 266b4c2ef4 ADR-0024 Phase F (full) step 3: delete legacy parser modules
Removes the last consumers of `dsl::lexer`, `dsl::keyword`, and
`dsl::ident_slot`, then deletes the modules.

- `Theme::token_color(&TokenKind)` deleted along with its test;
  `Theme::highlight_class_color(HighlightClass)` is the sole
  highlight-colour mapper (the walker's `per_byte_class` feeds
  it directly).
- `IdentSource` (`dsl::grammar`) absorbs the schema-list /
  expected-label / round-trip semantics that previously lived
  on `IdentSlot`. Adds `completes_from_schema`, `expected_label`,
  and `from_expected_label` methods. The walker's
  `Expectation::Ident { source }` and the schema-lookup request
  on the database worker now share one enum.
- `SchemaCache::for_slot(IdentSlot)` → `for_source(IdentSource)`.
- `Database::list_names_for` and the `Request::ListNamesFor`
  worker variant take `IdentSource`. Internal tables and column
  / relationship lookups dispatch on the same enum.
- `InvalidIdent.slot: IdentSlot` → `InvalidIdent.source: IdentSource`.
  The `invalid_ident_at_cursor` rendering branch in
  `input_render.rs::ambient_hint` updates accordingly.
- Completion's keyword filter (`Keyword::from_word`) becomes
  "backticked items whose payload is all ASCII alphabetic" —
  punct and digit literals still surface through their own
  candidate sources (composite-literal, flag, schema-ident);
  the alphabetic filter excludes them from the keyword bucket.
- `friendly::keys::tests::keyword_and_punct_have_complete_token_vocabulary`
  is dropped. It cross-checked `Keyword::ALL` / `Punct::ALL`
  against catalog entries; both enums are gone. The
  `parse.token.keyword.*` / `parse.token.punct.*` catalog
  entries themselves survive for one more commit (catalog
  cleanup, ADR-0024 §cleanup-pass); the
  `keys_validate_against_catalog` test still pins them.
- Modules deleted: `src/dsl/lexer.rs`, `src/dsl/keyword.rs`,
  `src/dsl/ident_slot.rs`.

Tests: 806 passing, 0 failing, 1 ignored. The drop from 852
reflects the removed module-internal tests (~32 lexer, 7
keyword, 4 ident_slot, 1 theme token_color, 1 friendly keys
keyword/punct), and is the expected outcome.

Clippy clean with `nursery` lints + `-D warnings`.
2026-05-15 08:33:59 +00:00
claude@clouddev1 a41400e532 ADR-0024 Phase F (full) step 2: usage via CommandNode.usage_ids
Migrates parse-error usage-block rendering from the legacy
`dsl::usage::matched_entry` (which scanned a `Vec<Token>` for the
first matched Keyword) to walker-side lookup driven by each
`CommandNode`'s `usage_ids` slice.

`CommandNode.usage_id: Option<&'static str>` becomes
`usage_ids: &'static [&'static str]`. Multi-form families
(`drop`, `add`, `show`) carry every variant — `drop` lists
table/column/relationship templates; `add` lists column /
relationship; `show` lists data / table. The single-shape
commands carry their single catalog key.

App-lifecycle CommandNodes had pointed at non-existent
`parse.usage.app.*` keys (never noticed because the field was
unused); they now point at the real catalog entries
(`parse.usage.quit`, `parse.usage.help`, …).

New helpers in `dsl::grammar`:
- `usage_keys_for_input(source) -> Option<(entry_word, usage_ids)>`
  resolves the first identifier-shape token to a CommandNode and
  returns its usage_ids list. Used by `app::render_usage_block`
  and `input_render::ambient_hint`.
- `entry_words_alphabetised() -> Vec<&'static str>` replaces
  `dsl::usage::entry_keywords_alphabetised`.

`dsl::usage` is deleted. The "available commands:" fallback in
`render_usage_block` now formats entry words as `` `<word>` ``
directly (matching the `parse.token.keyword.*` catalog renders);
the per-keyword catalog wrappers will collapse in the next step
(ADR-0024 §cleanup-pass §F).

`parse_command` and `parse_tokens` slim down:
- `parse_command(input)` no longer pre-lexes — the walker scans
  source bytes directly.
- `parse_tokens` (internal-only `pub` for "future I3/I4 work")
  is removed; its body folded into `parse_command`.
- `unknown_command_error` reads the walker registry directly.

Touched modules also drop their `crate::dsl::lexer::lex` and
`crate::dsl::usage` imports: `app.rs`, `input_render.rs`,
`completion.rs`.

Tests: 852 passing, 0 failing, 1 ignored (down from 860 because
the 8 `dsl::usage::tests::*` tests are gone with the module).
2026-05-15 08:27:16 +00:00
claude@clouddev1 7bdd3987e1 ADR-0024 Phase F (full) step 1: walker-driven highlighting
Replaces the lex()-driven `base_runs` span builder in
`input_render.rs` with `walker::highlight_runs`. The new
walker-side `dsl::walker::highlight` module returns per-byte
`HighlightClass` assignments for every token shape in the source:

- For commands the walker engages on, `WalkResult::per_byte_class`
  is the authoritative source (keyword / identifier / number /
  string / punct / flag).
- Trailing junk past a partial match — and inputs the walker
  doesn't engage on at all (no registered entry word) — fall
  through to a byte-shape scanner over `lex_helpers` so unknown
  command words, stray punctuation, and unterminated strings
  still highlight sensibly.

`Theme::highlight_class_color` is the walker-side analogue of
`token_color(&TokenKind)`; the renderer reads `walker::highlight_runs`
output and looks up colours through it. `token_color` and the
`lex()` pre-pass remain in place for now — the lexer module is
still consumed by usage rendering and completion until the
remaining Phase F steps land.

`HighlightClass`'s and `WalkResult::per_byte_class`'s
`#[allow(dead_code)]` annotations come off — they're now part of
the production highlight path.

Tests:
- 16 new tests under `dsl::walker::highlight` cover end-to-end
  walks, byte-shape fallbacks (unknown commands, bare flags,
  numbers, punctuation), UTF-8 codepoint advance, and trailing-
  token handling after partial walks.
- Existing `input_render` tests pass unchanged.
- 860 total tests passing (727 lib + 133 integration), 1 ignored.

Clippy clean with `nursery` lints + `-D warnings`.
2026-05-15 08:19:52 +00:00
claude@clouddev1 dca472f8a5 ADR-0024 Phase E: replay end-to-end
Migrate `replay <path>` to the walker. Shape is
Choice(StringLit, BarePath); the StringLit branch handles the
quoted form (with the existing `''` escape), and BarePath
handles the unquoted form.

Per ADR-0024's path-bearing UX change (already shipped for
import / export in Phase A), bare `replay` paths terminate at
the first whitespace byte. Paths with spaces require the
quoted form. The legacy `try_parse_replay_with_bare_path`
source-slice helper in dsl/parser.rs is removed; the
chumsky-side replay branch in command_parser stays declared
but unreachable until Phase F sweeps the chumsky path.

Tests:
- 7 new walker-specific tests for replay: bare relative path,
  bare absolute path, quoted with whitespace, quoted with
  escaped quote, case-insensitive keyword, missing-path
  error, empty-quoted-path parses to empty (runtime layer
  rejects).
- Total: 844 passed, 0 failed, 1 ignored (was 838 / 1).
- cargo clippy --all-targets -- -D warnings clean.
2026-05-15 07:23:51 +00:00
claude@clouddev1 c2accc2385 ADR-0024 Phase D: data commands at chumsky parity
Migrate the four data commands at four entry words: show
(show data / show table), insert, update, delete. Walker now
owns the entire command set introduced through ADR-0014.

Scope deviation from ADR-0024: full schema-aware value typing
via DynamicSubgrammar(column_value_list) is deferred. The
walker accepts any value at any position — matching the
existing chumsky parser's behaviour, where per-column type
checks happen at bind time. The DynamicSubgrammar Node
variant and WalkContext schema fields stay declared so the
infrastructure is in place when the schema cache plumbs
through parse_command (a future refinement). All existing
tests pass on the new shape.

Walker extensions:
- StringLit terminal — wired to the consume_string_literal
  helper that mirrors the legacy lexer's `''` escape handling.
  MatchedItem text carries the unescaped payload; span covers
  the surrounding quotes.
- Bridge: Incomplete error wording now appends `, found end
  of input` (matching the chumsky-side structural error
  contract that `structural_error_for_show_data_without_arg`
  asserts on).

Grammar:
- src/dsl/grammar/data.rs: SHOW (Choice of show_data /
  show_table), INSERT (three forms folded into a single shape
  via a Choice ordered to disambiguate Form B's `values`
  keyword from Forms A/C's `(`-prefixed content; the inner
  paren list is a Choice(VALUE_LITERAL, Ident{Columns}) with
  VALUE_LITERAL ordered first so `true`/`false`/`null` match
  their Word branch rather than the broader identifier catch-
  all), UPDATE (assignments + filter), DELETE (filter).
- VALUE_LITERAL = Choice(Word("null"), Word("true"),
  Word("false"), NumberLit, StringLit) — matches the chumsky
  `value_literal()`.
- WHERE_CLAUSE / FILTER_CLAUSE shared between update and
  delete.
- AST builders walk MatchedPath items in order, using role
  tags (`update_set_column`, `filter_column`,
  `insert_first_item`) to discriminate column references
  belonging to different shapes within the same command.

Tests:
- 13 new walker-specific tests covering all data forms:
  show data / show table, insert with each of three forms,
  insert with negative numbers, update with single + multiple
  assignments + where, update with --all-rows, delete with
  where, delete with --all-rows, update/delete without filter
  errors, replay still routes via chumsky.
- Total: 838 passed, 0 failed, 1 ignored (was 825 / 1).
- cargo clippy --all-targets -- -D warnings clean.
2026-05-15 07:20:53 +00:00
claude@clouddev1 6bb688251b ADR-0024 Phase C: create table with column-list value literals
Migrate `create table <Name> [with pk [<col>:<type>[, ...]]]`
to the walker. Exercises Repeated{separator: Some(Punct(','))}
for the first time — the with-pk column-spec list.

Walker behaviour changes:
- Optional now backtracks on partial-match failure (Incomplete
  or Failed-Mismatch from a Seq mid-shape). Path / per-byte
  state rolls back to before the partial attempt; the inner's
  expected-set propagates as `skipped` so callers see "what
  would have completed it". Matches chumsky's `or_not`
  semantics. ValidationFailed (content errors) does NOT
  backtrack — the user means to fix those.
- Bridge: ValidationFailed errors now classify as
  `at_eof = true`, mirroring the chumsky-side custom-error
  convention. This is what lets `create table Customers`
  classify as IncompleteAtEof rather than DefiniteErrorAt
  (the user can still continue typing `with pk …`).

Grammar:
- src/dsl/grammar/ddl.rs gains CREATE: shape is
  Seq(Word("table"), Ident{NewName,table_name}, Optional(WITH_PK))
  where WITH_PK = Seq(Word("with"), Word("pk"),
  Optional(Repeated{COL_SPEC, separator: Punct(','), min:1})).
  AST builder enforces `with pk needs at least one column`
  with the existing parse.custom.create_table_needs_pk catalog
  wording; `with pk` alone defaults to id:serial.

Tests:
- 6 new walker-specific tests for create_table: with-pk
  default, named typed PK, compound PK, whitespace tolerance
  around `:` and `,`, bare-create-table-errors-with-with-pk-
  hint, case-insensitive keywords.
- Total: 825 passed, 0 failed, 1 ignored (was 819 / 1).
- cargo clippy --all-targets -- -D warnings clean.
2026-05-15 07:12:22 +00:00
claude@clouddev1 7e79ca865a ADR-0024 Phase B: DDL commands without value literals
Migrate the five DDL commands at four entry words: drop (drop
table / drop column / drop relationship), add (add column /
add 1:n relationship), rename (rename column), change (change
column). The walker route now owns these end-to-end; chumsky
declarations remain unreachable for these inputs but stay
until Phase F.

Walker extensions:
- New node kinds: NumberLit (with optional content validator)
  and Literal(&str) (verbatim byte sequence with word-boundary
  lookahead — used for the `1` in `add 1:n …` so it surfaces
  as `\`1\`` in the expected-set, matching the existing
  parse_error_pedagogy contract).
- Flag (--name) terminal — Phase A stubbed; now wired to the
  walker driver with consume_flag() in lex_helpers.
- Repeated combinator with optional separator and `min` floor.
  Used by referential clauses (0..2 `on <delete|update>` runs)
  and change-column flags (0..N --force-conversion /
  --dont-convert; AST builder enforces mutual exclusion).
- Optional now propagates its inner's expectations as a
  `skipped` field on the Matched result. Seq accumulates these
  across children so the next failure's expected-set surfaces
  the full union — closes the keyword-completion regression
  (`add column ` must offer `to`, `table`, plus the table-name
  identifier slot).
- Expectation::Ident gained a `source: IdentSource` field; the
  parser-side bridge maps Tables/Columns/Relationships/Types
  to the IdentSlot::expected_label strings ("table name",
  "column name", …) so the existing completion engine's
  schema-cache lookup still resolves.
- Walker error wording now includes "after `<consumed>`,
  expected …" framing — matches the chumsky-side test
  contract for structural errors mid-shape.
- AST-builder validation errors now propagate as
  WalkOutcome::ValidationFailed (not the generic "AST builder
  failed" fallback), so `change column … --force-conversion
  --dont-convert` and repeated `on delete` clauses surface
  their friendly catalog wording verbatim.

Grammar additions:
- src/dsl/grammar/shared.rs: type-name validator (TYPE_VALIDATOR
  uses Type::from_str via parse.custom.unknown_type catalog),
  qualified_column sub-grammar, referential action keyword
  (`cascade`/`restrict`/`set null`/`no action`), repeated
  on-clauses.
- src/dsl/grammar/ddl.rs: drop/add/rename/change CommandNodes
  with inline shapes (per-use-site `role` annotations let the
  AST builder discriminate parent vs child columns, etc.).
  The four entry words each have one CommandNode whose `shape`
  is a Choice across sub-forms.

Tests:
- 14 new walker-specific tests covering all DDL forms (bare
  drop table, drop column with optional connectives, drop
  relationship by name and by endpoints, add column with type
  validator, rename column, change column with each flag form
  + mutual-exclusion check, add 1:n relationship minimal /
  full, repeated-clause-twice rejection).
- Total: 819 passed, 0 failed, 1 ignored (was 805 / 1).
- cargo clippy --all-targets -- -D warnings clean.
2026-05-15 06:59:27 +00:00
claude@clouddev1 50b3542050 ADR-0024 Phase A: walker framework + app-lifecycle commands
Stand up the unified-grammar tree walker alongside the existing
chumsky parser and migrate the eleven app-lifecycle commands
(quit, help, rebuild, save / save as, new, load, export, import,
mode, messages) end-to-end. The router in parse_tokens consults
the walker first; non-migrated commands still fall through to
chumsky.

Scope:
- src/dsl/grammar/{mod,app}.rs: Node enum (13 kinds), Word /
  IdentSource / HintMode / HighlightClass / ValidationError /
  CommandNode types, REGISTRY of the eleven app commands.
- src/dsl/walker/{mod,driver,context,outcome,lex_helpers}.rs:
  scannerless byte-level walker, per-node-kind dispatch with
  Choice/Seq/Optional backtracking, WalkContext (Phase B-D
  schema fields stubbed), WalkOutcome with Match/Incomplete/
  Mismatch/ValidationFailed.
- src/dsl/parser.rs: try_walker_route() runs first in
  parse_tokens; bridge converts WalkOutcome to ParseError
  preserving catalog wording (mode.unknown / messages.unknown
  surface verbatim via friendly::translate). Legacy
  try_parse_app_path_command deleted; chumsky's bare-keyword
  app branches remain unreachable until Phase F sweep.

Walker design choices worth noting:
- mode <value> / messages <value> use Choice(Word, Word, Ident)
  so known keywords appear in the expected-set; the trailing
  Ident catch-all funnels unknown values into the friendly
  validator that always errors with the catalog wording.
- save / save as is one CommandNode (Optional(Word("as"))) -
  closes the round-5 "save Tab can't offer as" limitation
  structurally.
- Path-bearing UX shipped per ADR-0024: BarePath terminates at
  whitespace; paths with spaces use the (not-yet-wired) quoted
  form. Existing tests pass on the new shape.

Tests:
- 28 new walker-specific tests in dsl::walker::tests covering
  every app-lifecycle command, friendly-error wording for
  mode/messages unknown values, trailing-garbage detection,
  whitespace tolerance, and routing fall-through.
- Total: 805 passed, 0 failed, 1 ignored (was 777 / 1).
- cargo clippy --all-targets -- -D warnings clean.
2026-05-15 06:39:29 +00:00