fbf449f9e04fd67075db952c856cfecbfcf2a004
356 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9bbb96e735 |
Walker: memoize DynamicSubgrammar resolution to bound the Box::leak
Node::DynamicSubgrammar factories build a Node from the WalkContext and must Box::leak it (the Node enum's combinator children are &'static). Leaking per walk grew unbounded under per-keystroke completion (handoff-12 §2.1). resolve_dynamic now memoizes on the schema state a factory reads (table columns, current column, user-listed columns) keyed by factory fn-pointer. Each distinct value-list shape leaks exactly once — total leak bounded by distinct (schema × form) combinations, not keystroke count. TableColumn gains Hash for the cache key. The handoff's original arena sketch needed a lifetime-generic Node (major refactor); memoization gets the same bound without it. |
||
|
|
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.
|
||
|
|
f1ff5970bf |
Hint: pedagogical Form-A pointer at Form B's first value slot
Handoff-12 §2.2: Form B `insert into T values (…)` silently skips auto-generated columns from the value list, so a user who wants to set a serial/shortid column explicitly could only discover Form A by reading help. Now the hint at the first Form B value slot appends a note naming the skipped column(s) and pointing at the explicit-column form. hint_resolution_at_input derives the skipped columns from the post-walk WalkContext (Form B = no user_listed_columns + table has serial/shortid columns) and reports them on HintResolution; the note fires only at the first slot so it doesn't repeat at every comma. ambient_hint composes it onto the per-column prose. |
||
|
|
bcc5ad2f20 |
Matrix: pin natural candidate ordering
8 tests covering completion-candidate order: connective keywords in reading order (`to`/`from`/`in` before `table`), and command-part keywords before schema identifiers. The ordering already held via declaration-order preservation + keywords-first sectioning in candidates_at_cursor; nothing pinned it until now, so a future grammar or sort change could silently break the hint panel's left-to-right reading. |
||
|
|
50b78253d8 |
Remove dead parse.token.* catalog entries
The 5 structural-class entries (identifier/number/string_literal/flag/ end_of_input) and 3 lex-error entries (bad_flag/unknown_char/ unterminated_string) were unreachable: ADR-0024 Phase F made the walker render keyword wording verbatim and the lex errors never surface through today's walker. Handoff-12 §2.1 kept them as a conservative call; removed now per user request. Re-adding is cheap if a future need arises. |
||
|
|
42cf851f7e | add handoff-13: typing-surface matrix complete + 5 matrix-found bug fixes | ||
|
|
c7ecc64757 |
Matrix: create table, DDL, and app-command coverage
55 tests covering create table, drop column, drop relationship (endpoints + by-name), add relationship, rename/change column, and all app-lifecycle commands. The drop-column and relationship tests drove the §2.2 writes_table fix in the previous commit. Documents one UX wrinkle as a flagged finding: partial entry words (`qu`) classify as DefiniteErrorAt — same as unknown commands — because the walker only engages on a complete entry word. 859 baseline -> 989 passing; 1 ignored (pre-existing doc-test). |
||
|
|
216e7ba61b |
DDL grammar: writes_table on table-name slots for column narrowing
Handoff-12 §2.2: the DDL TABLE_NAME_EXISTING slot and the relationship-endpoint table idents didn't set writes_table, so column-name slots downstream (drop/rename/change column; relationship qualified columns) couldn't narrow to the active table — candidates leaked from every table. Set writes_table: true on TABLE_NAME_EXISTING and on DR_PARENT/DR_CHILD/AR_PARENT/AR_CHILD table idents. The deliberately-documenting completion test now asserts per-table narrowing. |
||
|
|
37db2f5dd2 |
Matrix: insert Form C + update + delete coverage
34 new tests covering: - Form C bare-value-list (happy path + Form-A-recovery + type-unaware grammar limitation per handoff §2.2) - update with WHERE (column-narrowing invariant per handoff §1 bug E1; typed-slot prose for assignments and where filters) - update --all-rows (filter-clause requirement per ADR-0014) - delete with WHERE (column-narrowing; typed-slot prose for where filters) - delete --all-rows 859 baseline -> 931 passing. No bugs surfaced — the data-mutation command family was already well-shaped post-Phase-D. |
||
|
|
a9a04cff97 |
Matrix: insert Form B coverage
12 tests across schema_serial_pk / text_pk / multi_table / every_type. Pins (a) Form B skips auto-gen columns from the slot list (regression for handoff-12 §B fix); (b) wrong-count value lists are now flagged at typing time, not only at submit (the previous commit's fix); and (c) per-type slot prose advances correctly through every Type variant. |
||
|
|
fffb44ff4f |
input_render: schema-aware classify_input for wrong-count value lists
classify_input was schemaless; wrong-count Form B value lists
(`insert into Customers values ('Alice')` against a 3-column table)
showed as Valid until submit. Add classify_input_with_schema that
threads the SchemaCache through parse_command_with_schema and wire
render_input_runs to use it. Schemaless classify_input is kept public
for handoff-11/12 regression tests that exercise schema-independent
positions.
|
||
|
|
24e641bc21 |
Matrix: typing-surface infrastructure + insert Form A coverage
Per docs/handoff/20260515-handoff-12.md §1. Systematic per-position coverage of (state, hint, completion, parse_result) across canonical schema shapes; submodule per command family. Insert Form A covers 23 cursor positions across serial-PK, text-PK, multi-table, and every-Type schemas. Both bugs fixed in the previous commit were surfaced by these tests. Shared helpers under tests/typing_surface/mod.rs: 5 canonical schema shapes, assess() helper, property-assertion shortcuts, and a snap! macro that wraps insta with a stable per-cell suffix. 859 -> 885 tests passing; 1 ignored (pre-existing doc-test). |
||
|
|
0b15ce0306 |
Walker + parser: surface mid-typing after separators and Form C/A ambiguity
The typing-surface matrix exposed two bugs the existing 859-test suite missed: walk_repeated: when the separator consumed but the inner item failed at EOF, the old path rolled the separator back and reported a definite error at the rollback position (`insert into T (a, ` flashed red on the `,` after each comma). Now propagates Incomplete with the inner's expected set so the input renderer treats it as mid-typing. build_insert Form C path: `insert into T (col)` walked to a complete match but produced `values: []` because Form C's value collector drops ident-shaped items. The user almost certainly meant Form A and just hasn't typed `values (...)` yet. Reject with a ValidationError naming the Form-A continuation; classify_input now reports IncompleteAtEof. completion_probe / expected_at_input: ValidationFailed used to return an empty expected set, leaving Tab with nothing to offer at the new Form-A flag point. Now surface result.tail_expected (skipped-Optional expectations captured before validation fired) so `values` is still offered as a candidate. |
||
|
|
3b1955c6bf | add handoff-12: post-Phase-D UX fixes + test-coverage agenda for next session | ||
|
|
619a8bd707 |
Completion: narrow column candidates to the active table
Two related fixes:
1. \`update MyTable set \` was offering columns from every
table in the project — completion fetched
\`cache.for_source(IdentSource::Columns)\` which returns the
flat \`cache.columns\` (union of every table's columns).
The walker's WalkContext had \`current_table_columns\`
populated (because the update-table-name slot is
\`writes_table: true\`) but the completion engine never
consulted it.
2. \`insert into MyTable (\` was offering nothing — the
value-literal suppression fired because the expected set at
this position contains both Form A column-list candidates
(\`Ident{Columns}\`) and Form C bare-value-list literals
(null/true/false/NumberLit/StringLit). \`is_value_literal_signature\`
matched and the engine returned \`None\` before the column
candidates were considered.
The fix threads the walker's \`current_table_columns\` through
to the completion engine and narrows the suppression rule:
**Walker:**
- New \`walker::CompletionProbe { expected, current_table_columns }\`
struct.
- New \`walker::completion_probe(source, schema) -> CompletionProbe\`
runs one schema-aware walk and reports both the expected
set (or tail_expected on Match) and the resolved table-column
snapshot.
**Completion engine:**
- \`candidates_at_cursor_with\` calls \`completion_probe\` and
reads \`current_table_columns\` for the \`Columns\` ident
source. Schemaless or unknown-table falls back to the flat
\`cache.columns\` (preserves pre-fix behavior).
- Value-literal suppression now gated on
\`!has_schema_ident\` — if the expected set also offers a
schema-listable Ident, the user has actionable candidates
beyond the misleading null/true/false trio and we shouldn't
hide them.
Tests:
- \`update_set_offers_only_current_table_columns\` confirms
Customers' columns appear while Orders' columns don't.
- \`update_where_offers_only_current_table_columns\` covers
the where path.
- \`insert_into_open_paren_offers_current_table_columns\` and
\`insert_into_open_paren_does_not_offer_unrelated_columns\`
cover the Form A column-list position.
- \`drop_column_from_offers_only_current_table_columns\`
documents the DDL fallback (drop-column's table-name slot
doesn't currently \`writes_table\` — falls back to the flat
list).
For the user: \`update MyTable set \` now offers only
MyTable's columns. \`insert into MyTable (\` offers all of
MyTable's columns so Form A is fully discoverable.
Tests: 859 passing, 0 failing, 1 ignored. Clippy clean.
|
||
|
|
5815918efb |
Hint: surface ( as a branching candidate; stop red-flagging in-progress Form A values
Two related fixes from a user-reported snag:
1. After typing \`insert into Orders \`, the hint suggested only
\`values\` even though the user could also choose \`(\` to
open Form A (the explicit-column-list variant). The walker
reports both \`Expectation::Word("values")\` and
\`Expectation::Punct('(')\` at that position, but
\`candidates_at_cursor\` had a blanket "no punctuation as Tab
candidate" policy.
Loosened the policy to surface branching punct
(specifically \`(\` opening a sub-shape). Closing punct
(\`)\`), separators (\`,\`), and content-trailing punct (\`:\`,
\`=\`, \`.\`) stay out — the user types those naturally and
advertising them in the Tab menu is noise. New
\`CandidateKind::Punct\` so the renderer colors it as punct
rather than mis-classifying as a keyword.
2. While typing \`insert into Orders (id, CustId, Total) values
(42, 89, 17.59\` (no closing paren yet), the word \`values\`
was rendered in \`tok_error\` red. The walker's
\`Optional(Seq[values, '(', list, ')'])\` was rolling back on
the partial inner match — treating \`(id, CustId, Total)\` as
Form C (bare value list) followed by trailing junk starting
at \`values\`. The classify_input call thus returned
\`DefiniteErrorAt(<values byte>)\` and the renderer overlaid.
Tightened \`walk_optional\`: roll back only when the inner
reports NoMatch (or Incomplete / Mismatch without consuming
anything). Once the inner has committed to at least one
terminal (e.g. matched the \`values\` keyword), propagate
Incomplete / Mismatch up — the user is mid-typing the
optional's content and rolling back would lose their
intent.
The pre-existing chumsky-or_not-style aggressive rollback
covered cases like \`save Customers\` (Optional(\`as\`)
inner is a single Word that returns NoMatch without
consuming, so rollback still fires). Those keep working.
3. Side effect: with \`Optional\` no longer hiding the
in-progress Form A from the leading slice, the walker on
\`create table T with \` correctly reports the next-expected
keyword as \`pk\` — so cursor at the end of the complete
command \`create table T with pk\` would now re-offer \`pk\`
as a Tab candidate against the partial \"pk\". Added a final
filter: when the full input is a valid parse AND the
partial prefix is non-empty, drop candidates that equal the
partial exactly. Preserves schema narrowing
(\`show data Cu\` → \`Customers\` is not an exact match).
Tests:
- New \`in_progress_form_a_values_list_classifies_as_incomplete\`
asserts the input-state for the user's exact scenario.
- New \`open_paren_branching_punct_surfaces_after_insert_into_table\`
and \`open_paren_candidate_is_classified_as_punct_kind\` cover
the punct-as-candidate surface.
- Renamed and rewrote \`punctuation_expected_does_not_produce_candidates\`
to \`non_branching_punctuation_is_not_surfaced_as_candidate\`
to document the new finer-grained policy.
- Existing tests for \`save Tab → as\` and the schema-
narrowing case continue to pass.
Tests: 854 passing, 0 failing, 1 ignored. Clippy clean.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
124c1d33e9 | add handoff-11: ADR-0024 Phase F/D fully landed; HintMode/Ranker/round-5 closed | ||
|
|
8188fa5ee1 |
ADR-0024 round-5 follow-up: surface tail-Optional expectations
Pre-Phase-D, `save ` parsed as a complete `save` command, so the completion engine had nothing to mine: `as` never surfaced as a Tab candidate. This is the round-5 gap the handoffs have been tracking. `WalkResult` gains a `tail_expected: Vec<Expectation>` field. The walker's top-level `Matched` branch copies the outer shape's skipped-Optional expectations into it. `expected_at_input` returns `tail_expected` on `Match` so the completion engine sees the optional-suffix continuations and offers them as Tab candidates. `hint_mode_at_input` deliberately does NOT consume `tail_expected` — surfacing prose like "Type a name" at the end of a valid command would be misleading. A new private `expected_for_hint` returns empty on `Match` to preserve this. The split distinguishes "valid + could continue" (completion helps) from "invalid + must continue" (hint resolver helps). Tests: - `save ` Tab → `as` (new test, the original round-5 gap). - `messages ` Tab → `short` and `verbose` (same shape). - Existing `hint_mode_none_for_complete_command` stays green because hint resolver ignores tail_expected. - 830 total passing, 0 failing, 1 ignored. Clippy clean. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
7ae1a0fde1 |
ADR-0024 ranker hook scaffolding
Adds the `Ranker` plug-in point ADR-0024 §ranker-layer specified. The grammar tree declares *what's valid*; the ranker decides *what's likely useful first*. Default `identity_ranker` preserves declaration order from the grammar. API: - `pub type Ranker = fn(Vec<Candidate>) -> Vec<Candidate>;` - `pub const fn identity_ranker(c) -> Vec<Candidate>` returns its input unchanged. - `candidates_at_cursor_with(input, cursor, cache, ranker)` applies a custom ranker; the default `candidates_at_cursor` delegates with `identity_ranker`. Three new tests cover identity preservation, custom reordering, and the empty-list-collapses-to-None edge. This is a future-work hook — no production caller passes a non-identity ranker yet. Hooks for frequency-based ranking, content-aware priors, or recency plug in here without touching the grammar declarations. Tests: 809 passing, 0 failing, 1 ignored. Clippy clean. |
||
|
|
bbe12524ab |
ADR-0024 Phase F (full) step 5: walker-driven completion
Replaces the `ParseError::Invalid::expected: Vec<String>`
round-trip with structured `Expectation`s direct from the walker
(ADR-0024 §architecture). The completion engine no longer parses
formatted strings back into types — `Expectation::Ident { source,
role }`, `Expectation::Word`, `Expectation::Literal`,
`Expectation::Flag`, `Expectation::NumberLit`, and
`Expectation::StringLit` are consumed as enum variants.
New helper:
- `walker::expected_at_input(source) -> Vec<Expectation>`
consolidates the empty-input case (returns every CommandNode
entry word), unknown-command-word case (also entry words), and
walker-engaged case (Incomplete / Mismatch expectations) in one
place. ValidationFailed and Match resolve to empty.
`completion.rs` refactor:
- `expected_at(leading)` wraps the walker helper; replaces the
legacy string-based `expected_set`.
- Keyword candidates: filter `Expectation::Word(w)` /
`Expectation::Literal(s)` to alphabetic-only literals (no
more string-parsing / `strip_backticks`).
- Type names: detect `Expectation::Ident { source:
IdentSource::Types }` directly (replaces the `TYPE_SLOT_LABEL`
magic string).
- Flag candidates: read `Expectation::Flag(body)` and format
as `--{body}` (replaces backticked-string matching).
- Composite-literal candidates: match against
`Expectation::Literal("1")` (replaces the backticked-string
`` `1` ``).
- Schema identifiers: `Expectation::Ident { source, .. }`
filtered by `source.completes_from_schema()`.
- `is_value_literal_signature` checks for `Expectation::Word`
values "null"/"true"/"false" and `Expectation::NumberLit` +
`Expectation::StringLit` variants (replaces backticked-string
matching).
- `invalid_ident_at_cursor` and `typing_name_at_cursor` adopt
the same path.
The `typing_name_at_cursor` probe (substitute placeholder and
re-parse) still goes through `parse_command` because the probe
specifically wants the *post-name* expected set — `parse_command`
+ the string `expected` field carries that today. A future
follow-up could thread the structured probe through `walker`,
but the value-add is marginal.
`COMPOSITE_CANDIDATES` opener key changes from `` `1` `` (the
backticked-string the chumsky parser produced) to bare `"1"`
(the Expectation::Literal payload).
Touched modules: `dsl/walker/mod.rs` (new export),
`src/completion.rs` (refactor).
Tests: 806 passing, 0 failing, 1 ignored — every existing
completion test passes unchanged, proving the structured path
is behaviour-preserving. Clippy clean.
|
||
|
|
044173bd39 | add handoff-10: ADR-0024 Phase F (full) steps 1-4 landed; remaining work catalogue | ||
|
|
fa994cfb66 |
ADR-0024 Phase F (full) step 4: catalog token-keyword cleanup
Drops the 47 `parse.token.keyword.*` and 6 `parse.token.punct.*`
catalog entries (and their `KEYS_AND_PLACEHOLDERS` declarations).
Nothing consumes them: the walker renders keyword wording in
`format!(\"`{word}`\")` directly, sourced from grammar-tree Word
literals; punct wording surfaces the same way via
`Expectation::Punct(ch)`.
Structural-class labels (`parse.token.identifier`,
`parse.token.number`, `parse.token.string_literal`,
`parse.token.flag`, `parse.token.end_of_input`) and the lex-error
wordings (`parse.token.error.{bad_flag,unknown_char,
unterminated_string}`) stay. These are not derivable from the
grammar tree and the walker's expected-set / validator paths still
read them.
`friendly::keys::tests::keys_validate_against_catalog` continues to
assert catalog ↔ `KEYS_AND_PLACEHOLDERS` bidirectional coverage,
so the trimmed declaration is pinned against the trimmed catalog.
Tests: 806 passing, 0 failing, 1 ignored. Clippy clean.
|
||
|
|
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`.
|
||
|
|
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). |
||
|
|
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`. |
||
|
|
b3d3bdfe5b | add handoff-9: ADR-0024 phases A-F minimal landed; deferred work catalogue | ||
|
|
c940ba9cf2 |
ADR-0024 Phase F (minimal): drop chumsky from the parse path
Delete the chumsky-side command_parser and its per-command sub-parsers, error humanisation helpers, and keyword/punct/ ident/value-literal combinators. The unified-grammar walker in `crate::dsl::walker` is now the sole parse path. parse_tokens flow (post-Phase F): 1. lex(input) — still produces the Token stream that completion / highlighting / echo-line consumers depend on. 2. try_walker_route(source) — walker handles every entry keyword in REGISTRY (20 commands across 14 entry words). 3. unknown_command_error(source) — synthetic ParseError for inputs whose first identifier-shape token isn't a registered entry word. Wording mirrors the chumsky-side "expected one of `add`, `change`, …, found `<word>`" structural error the legacy top-level Choice produced. Cargo.toml: chumsky dependency dropped (no remaining uses). Cargo.lock regenerated; ~58 lines net reduction in the dependency graph. Scope intentionally deferred (separate follow-up): - dsl/lexer.rs, dsl/keyword.rs, dsl/ident_slot.rs, dsl/usage.rs::REGISTRY: still consumed by completion.rs, input_render.rs, app.rs, theme.rs, db.rs, runtime.rs, friendly/keys.rs. Removing these requires migrating each consumer to the walker's per-byte-class output / grammar REGISTRY / IdentSource enum. Substantial blast radius; worth a dedicated session. - parse.token.keyword.* catalog entries (40+): used by usage.rs and parse-error rendering for the unmigrated consumers above. Collapse follows after the consumer migration. Tests: - All existing parser tests (`dsl::parser::tests`) ported in place; they call `parse_command` which now flows through the walker. 844 passed, 0 failed, 1 ignored — same count as Phase E (no test additions, no regressions). - cargo clippy --all-targets -- -D warnings clean. - cargo build (release-like dev profile) succeeds. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
3e1ff83f26 |
add handoff-8: round 5/6 summary + ADR-0024 execution handoff
Round 5 + 6 testing + i18n sweep + thiserror migration +
ADR-0023 (direction) + ADR-0024 (accepted execution plan).
Frames the next session's primary task: execute ADR-0024
Phases A-F non-interactively. Spells out escalation criteria
("ADR doesn't cover this" not "I'm unsure which choice is
better"), per-phase commit checkpoints as the user
touchpoint, and the autonomous-by-default disposition the
user has explicitly requested.
Smoke test refreshed (empty hint wording, value-literal slot
hint, q removed, catalog-driven CLI error). Sharp-edges
section covers the design's traps (scannerless walker,
schema-aware parse, WalkContext writes, DynamicSubgrammar
expansion, Optional continuations closing the round-5 'save
Tab can't offer as' gap, IdentSource::Types replacing the
TYPE_SLOT_LABEL magic string, path-bearing UX change
requiring quoting for paths-with-spaces, hand-curated help
text staying out of grammar-derivation scope).
|
||
|
|
74c3ec1edf |
add ADR-0024: unified grammar tree execution plan (accepted)
Concrete specification for the direction in ADR-0023, landed during the round-6 design pass. Resolves all four rounds of open design questions: walker as single source of truth, scannerless terminal vocabulary (~8 building blocks), typed value slots with content validators, WalkContext for schema- aware narrowing from day one, WalkOutcome multi-purpose return, HintMode per-node, ranker as separate layer, static + dynamic sub-grammars, aliases as Word annotations, IdentSource taxonomy, six-phase per-command migration with chumsky and walker side-by-side during the transition. Key shifts from ADR-0023's sketch: - Lexer dissolves entirely. Walker operates on bytes directly. dsl/lexer.rs, dsl/keyword.rs go away in Phase F. - Schema-aware parse from day one (not phased). Typed value slots reject mis-shaped input at parse time with localised wording. Completion narrows per column type. - Sub-grammars: static (fn() -> Node) for composition; dynamic (fn(&WalkContext) -> Node) for schema-dependent expansion. No global named registry. - Path-bearing commands: BarePath becomes a routine non-whitespace terminal. Paths with spaces require quoting via StringLit (UX simplification, aligns with standard CLI convention). - 13-node taxonomy: Word, Punct, Ident, NumberLit, StringLit, BlobLit, Flag, BarePath, Choice, Seq, Optional, Repeated, DynamicSubgrammar. Migration plan: Phase A (walker scaffolding + app-lifecycle commands), Phase B (DDL without value literals), Phase C (create table), Phase D (data commands with full schema awareness -- the design's central claim landing), Phase E (replay), Phase F (delete chumsky + lexer + legacy parser modules, simplify catalog). Estimated ~4 sessions total. Also: rename ADR-0023 from 0023-proposed-unified-grammar-tree.md to 0023-unified-grammar-tree.md (git mv preserves history) and update its status to reflect the direction-accepted-but- superseded-for-execution-detail relationship with ADR-0024. Index updated. |
||
|
|
3b36bbb4d6 |
hint: replace misleading "null true false" suggestions at value slots
At value-literal slots (`insert into T values (`, `update T set
col=`, `where col=`, comma positions) the expected-token set
contains null/true/false/number/string-literal. The completion
engine was surfacing the three keyword candidates as Tab options
— actively misleading because the user is usually about to enter
a number, quoted text, or date, and seeing "null true false"
implies those are *the* options. User report (round-6 testing):
"especially not when I'm trying to insert a datetime value and
don't know the correct format for the literal".
Fix: detect the value-literal slot by its expected-set
fingerprint. Suppress Tab candidates at empty prefix. Surface a
prose hint listing all literal forms with format examples
('YYYY-MM-DD' for dates, 'YYYY-MM-DDTHH:MM:SS' for datetimes).
Once the user starts typing a prefix (n / tr / fa), normal
keyword completion still applies.
Schema-aware narrowing (show ONLY the datetime format at a
datetime column) waits on ADR-0023.
Tests: 769 -> 777 passing (+8). Clippy clean.
|
||
|
|
d6e138169f |
add ADR-0023 (proposed): unified declarative grammar tree
Captures the architectural critique surfaced during round-5 manual testing — that adding a keyword or command currently requires edits in 7-10 files across parser, completion, usage registry, catalog, and tests — and the proposed direction: a single declarative trie registry that drives parse, completion, highlight, and usage rendering from one source. Status: Proposed. Not yet accepted. Filename carries the `-proposed-` segment so status is visible at directory-listing time; rename to `0023-unified-grammar-tree.md` on acceptance. Estimated cost: ~4 sessions, per-command migration. Why not now: feature backlog and bearable scatter cost. Right moment to execute when backlog quiets or scatter cost becomes visibly painful. |
||
|
|
a55b6a7a05 |
remove q quit alias
`q` was introduced in round-5 as a peer Keyword variant alongside `quit`. Per ADR-0023's "alias miss" critique, that was the wrong shape — it surfaced `q` as a standalone command in completion (only one of its kind), and required parallel parser + usage + catalog + test entries. Drops the Keyword variant entirely; if this ever needs to come back, it should arrive as an alias annotation per ADR-0023, not as a peer keyword. Tests still 769 passing. |
||
|
|
6ca297579e |
round-5 follow-up r2: migrate all thiserror Display attributes to catalog
Completes the i18n sweep started in the previous commit. All
remaining hand-rolled user-facing English strings inside
thiserror #[error(...)] attributes have been moved into the
catalog. Drops the thiserror dependency entirely.
Twelve error types migrated:
- dsl::action::UnknownAction → parse.custom.unknown_action
- dsl::parser::ParseError → parse.error_wrapper + parse.empty
- dsl::value::ValueError → value.{type_mismatch,format}
- persistence::csv_io::CsvError → persistence.csv.*
- persistence::mod::PersistenceError → persistence.{io,encode}
- persistence::yaml::YamlError → persistence.yaml.*
- persistence::migrations::MigrateError → persistence.migrate.*
- project::lock::LockError → project.lock.*
- project::naming::NamingError → project.naming.*
- project::naming::UserNameError → project.user_name.*
- project::mod::ProjectError → project.{path_not_found,...}
- project::mod::SafeDeleteError → project.safe_delete.*
- archive::ArchiveError → archive.*
- cli::ArgsError → cli.*
- db::DbError → db.error.*
Pattern per type: drop thiserror::Error derive, write manual
Display calling crate::t!(), keep #[from] semantics via
explicit From impls, override Error::source() where applicable
so #[source]-style chaining is preserved.
Why this matters (user rationale): "fine to have fallbacks for
errors that are purely technical, but lift the output to a
place where it can be localized later and where an adjustment
with friendly text is easily possible if any of them become
part of the happy path." All surface strings now live in
en-US.yaml and can be reworded or localized without touching
Rust source.
Tests: 769 passing, 0 failed, 1 ignored. Clippy clean with
-D warnings. Cargo.toml: drop thiserror = "2.0.18".
|
||
|
|
1e06490572 |
round-5 follow-up: completion + i18n sweep
Four user-reported gaps from the round-4 testing pass:
1. Empty-prompt hint reworded from "(no active hint)" to
"Type a command — press Tab for options, `help` for a
list" (6 snapshots updated to reflect 80-col truncation).
2. App-lifecycle commands (quit/q, help, rebuild, save/save as,
new, load, export, import, mode, messages) now flow through
the DSL parser:
- 15 new keywords + catalog token entries
- new Command::App(AppCommand) AST with 11 variants
- parse-first dispatch in submit() (app commands work in
both simple and advanced modes)
- pre-chumsky source-slice for `export <path>` /
`import <zip> [as <target>]` mirrors the replay precedent
- UsageEntry registry entries so parse errors surface
relevant usage templates
- `mode <bad>` / `messages <bad>` use try_map for the
friendly "unknown mode/messages" wording
3. DSL completion gaps:
- `1:n` surfaces as a composite candidate at `add `
- --all-rows / --create-fk / --force-conversion /
--dont-convert surface as new CandidateKind::Flag
candidates (coloured with tok_flag in hint panel)
- filter_clause .labelled() wrap removed so chumsky's
expected-set surfaces the constituent options
4. Hardcoded user-facing strings migrated to catalog:
- 4 parser custom errors (incl. the known "tables need at
least one column" wart)
- UnknownType Display now via parse.custom.unknown_type
- UI panel titles + mode labels (Output / Hint / SIMPLE /
ADVANCED / Advanced:)
- app.rs cascade rendering (action labels + summary)
- runtime --resume CLI stderr
- db.rs change-column diagnostic tables (7 headers + 3
wrapper summaries + force-conversion hint)
Tests: 765 → 769 passing, 0 failed, 1 ignored (same doctest
as before). Clippy clean with -D warnings.
Deferred:
- ~25 thiserror #[error] attributes still hand-rolled
(DbError, ArgsError, ArchiveError, PersistenceError,
LockError). Tracked separately.
- DSL/SQL relationship in advanced mode — clarified
implicitly via parse-first dispatch; broader ADR
amendment to follow.
- Post-complete-parse completion gap (e.g. `save ` Tab
can't offer `as` because `save` parses bare; same shape
as `--create-fk` after a complete `add relationship`).
|
||
|
|
1eb2e0d01f | handoff | ||
|
|
c247f55094 |
ADR-0022 follow-up r4: column-type completion
Round-4 user finding: typing `(de` at a column-type slot
showed the parser's "unknown type 'de'" error and Tab did
nothing — completion was blind to the type vocabulary
entirely.
Root cause: type names are NOT in the Keyword enum (ADR-0020
§2 — they remain identifiers, validated by Type::from_str),
so the keyword-iter path in candidates_at_cursor missed
them. The schema-identifier path also missed them (they're
not in the schema cache).
Fix: when the parser's expected-set contains the `"type"`
label (from `ident_inner().labelled("type")` inside
`type_keyword`), produce candidates from `Type::all()`
filtered by the partial prefix. Centralised as
`TYPE_SLOT_LABEL` constant so the parser and the completion
engine agree on the magic string.
Candidates appear in `Type::all()` declaration order
(text/int/real/decimal/bool/date/datetime/blob/serial/
shortid) — matching ADR-0005's pedagogical grouping. Coloured
as Keyword (purple) since type names are closed-set
grammar, not user content.
Verified end-to-end:
- `(de` → ["decimal"] (single match → Tab inserts with space)
- `(da` → ["date", "datetime"] (multi → cycles)
- `(sh` → ["shortid"]
- `(` → all 10 types in declaration order
- `(var` → [] (no Tab candidates; parser custom error fires on submit)
Tests: 760 passing, 0 failing, 1 ignored (755 baseline +5
new type-slot cases). Clippy clean.
|
||
|
|
22119d6a4e |
ADR-0022 follow-up r3: identifier colour, NewName hint, "Next:" wording, "type" label
Three fixes from a third round of real testing. 1. **tok_identifier vivid (round-3 #1).** The cool grey-blue from r2 was still too close to theme.fg to register as distinct. Bumped to cyan-teal (#56B6C2 dark / #0F6B76 light) — identifiers are the user's most "special" content and now read that way against keywords (purple), numbers (orange), strings (green), and flags (amber). 2. **"Type a name" hint at NewName slots (round-3 #2).** New `completion::typing_name_at_cursor(input, cursor)` returns `Some(TypingName)` when the cursor sits at — or inside — an `IdentSlot::NewName` position. It probes by substituting a single-letter placeholder identifier and re-parsing to discover what the parser would expect AFTER the name; the hint then reads "Type a name, then `(`" instead of the technical "next: `(`" that surfaces once the partial identifier has been consumed by the live parser. When the probe yields nothing useful (custom errors with empty expected, or a complete-on-substitute case), falls back to "Type a name". New catalog keys hint.ambient_typing_name and hint.ambient_typing_name_then. Wired into ambient_hint between the candidate-list and invalid-ident checks. 3. **"Next:" instead of "expected:" wording.** "Expected" read as a leaked diagnostic; "Next:" is shorter, conversational, and consistent with the action-oriented voice of "Submit with Enter" and "Type a name". Hint sentences now also start capitalised (Submit/Next/Type/No-such), per the user's Capital-T-on- "type a name" preference. 4. **type_keyword labelled "type".** Without a label, the `select_ref!` over an Identifier token produced `RichPattern::SomethingElse`, which rendered as the meaningless "something else" in the hint after `(`. Labelled now: error reads "Next: type" — terse but honest. The label is applied BEFORE try_map (not after, not via as_context) so the existing custom-error wording for unknown types ("unknown type 'varchar' (expected one of: …)") still surfaces unchanged. Tests: 755 passing, 0 failing, 1 ignored (no net change — +5 typing_name cases, -0 net since one test was reworded for capitalisation rather than added). Clippy clean. Smoke probe verifies: "add column to table T: " → "Type a name, then `(`"; "add column to table T: Name (" → "Next: type"; "show data Custp" → "No such table: `Custp`"; valid input → "Submit with Enter". Note for next testing round: parser-side custom errors (e.g. the "tables need at least one column" message that fires for `create table Customers `) still read in lowercase — they're hand-written in parser.rs source rather than via the catalog. If the lowercase "tables need…" intrusion bothers you, easy follow-up. |
||
|
|
f94a999e66 |
ADR-0022 stage 8 follow-up r2: completion UX fixes from real testing
Two concrete behaviour changes from the user's second testing
round:
1. **Single vs multi commit paths.** Previously every Tab,
even single-candidate, created a memo so Esc/Backspace could
undo. The downside: with one candidate, repeated Tab "cycled"
through the same item invisibly — looked stuck. Now:
- Single candidate → insert with trailing space, no memo.
The user can keep typing or hit Tab again to fresh-complete
at the new cursor. (Trade-off: Esc/Backspace no longer
whole-span undo for unique completions; the user accepted
this for the chained-Tab fluency.)
- Multi candidate → insert WITHOUT trailing space, create
memo for cycling. The natural commit gesture is space —
pressing it clears the memo and inserts the space normally,
producing "<chosen> " ready for the next position.
The "stuck on unique" symptom goes away, and the missing
trailing space on multi-Tab signals "you're picking; press
space when you're done" without needing modal affordances.
2. **Keyword candidates in grammar order.** Dropped the
alphabetical sort in `describe_expected` in favour of
chumsky's native source-order traversal of `or_not`/`choice`
chains — empirically this matches the canonical command
shape. Result: `add column ` now offers `to` before
`table` (as `add column [to] [table] <Table>:…` reads),
not `table` before `to` which previously suggested the
nonsensical `add column table to ...`. Identifiers still
alphabetised within their group; entry-keyword fallback
for the no-prefix case stays alphabetical (no source order
when 10 separate command branches).
Tests: 750 passing, 0 failing, 1 ignored (747 baseline →
+3 net: replaced single-candidate Esc/Backspace tests with
new multi-candidate variants; added the unique-Tab-chains-
naturally case that drove the round-2 fix; kept the
keywords-in-grammar-order test updated to assert
`to`/`table`/identifiers ordering).
|
||
|
|
bd1cce672d |
ADR-0022 stage 8 follow-up: fixes from real-app testing
Three fixes from the user's testing run, plus an investigation note on a fourth. #4 Sticky hint during cycling. The previous code recomputed candidates_at_cursor at the post-Tab cursor position, which made the panel whiplash through "what comes next at the new cursor" between cycles. ambient_hint now short-circuits to the memo's stored candidate list while the memo is alive — so Tab Tab Tab keeps showing the same list with the selection moving, then snaps to the post-Tab ambient state once any non-Tab key clears the memo. #2 Candidate ordering and kind-coloured rendering. New `Candidate { text, kind: Keyword|Identifier }` carries the classification through completion, last-completion memo, and ambient-hint payload. candidates_at_cursor now sorts keywords first (alphabetical), identifiers second (alphabetical), and the hint-panel renderer colours keywords in `tok_keyword` and identifiers in `tok_identifier`. Keyword-vs-identifier name collisions resolve in favour of the keyword (rare; the user can still address their table via different syntax). #3 tok_identifier no longer matches theme.fg. Identifiers in the input pane now render in a distinct cool grey-blue (dark) / dark steel-blue (light), so they stand out from prose-like default text without competing with keyword purple. Same colour drives the identifier candidates in the hint panel for visual consistency input ↔ hint. Limitation worth knowing: "keywords first, alphabetical" is not the same as grammatical order. For "add column " the hint shows `table to` not `to table` — chumsky's expected-set doesn't preserve combinator-source order, and encoding it in the registry adds maintenance overhead the fix doesn't cleanly justify. Marked for future revisit if it bites. #1 (Tab does nothing on "add column ") — not reproduced through App::update. The internal logic works correctly: "add column " + Tab inserts "Customers ", second Tab cycles to "Orders ", third to "Thing ". The most likely explanation is a stale binary or a terminal-level event intercept (tmux focus, kitty-keyboard protocol differences, etc.) — needs user verification with a fresh build. Tests: 747 passing, 0 failing, 1 ignored (744 baseline → +3: 2 new completion-ordering cases including the keyword-wins-on-name-collision edge, plus 1 hint-mid-cycle sticky test). Clippy clean. |
||
|
|
8214e4136a |
ADR-0022 stage 8e: invalid-identifier detection + hint variant
Per the user's #5: "if our candidate selection works correctly, then entering a character that removes all matches is the same as entering an invalid token." Closes the loop between schema cache (8c/8d) and live error feedback (4). New `completion::invalid_ident_at_cursor(input, cursor, cache)` returns `Some(InvalidIdent { range, found, slot })` when: - the cursor is on a partial identifier-shaped token; - the parser's expected-set at the start of that token contains a known-set IdentSlot (TableName / Column / RelationshipName); - no schema entry across those slots prefix-matches the typed text. `render_input_runs` extended to take a `&SchemaCache` and overlay the invalid-identifier range with `tok_error` — same visual treatment as the parse-error overlay (4), unified red signal regardless of which detector fires. `ambient_hint` extended to surface `hint.ambient_invalid_ident` when invalid_ident_at_cursor returns Some — wording "no such {kind}: `{found}`" mirrors ADR-0019's engine-error voice for consistency. Catalog + KEYS_AND_PLACEHOLDERS declaration added; validator passes. Render priority: candidates win over invalid-ident (if any schema match exists for the partial prefix, the state is "in-progress completion" not "invalid"). Falls through to the existing parse-error/incomplete/Valid framings otherwise. NewName slots are filtered out at the source — typing into a "user invents this name" position is never invalid (per `IdentSlot::completes_from_schema`). Tests: 744 passing, 0 failing, 1 ignored (738 baseline → +6: 5 invalid_ident_at_cursor cases covering unknown-prefix-fires, prefix-match-doesn't-fire, NewName-immune, no-cursor-token, keyword-slot-immune; plus 1 ambient_hint integration test). Clippy clean. This closes ADR-0022. Stages 1-8e together deliver the ambient-typing-assistance feature: token highlighting, error overlay, hint panel ambient, hint panel multi- candidate display with scroll markers, Tab/Shift-Tab cycling with one-keystroke Esc/Backspace undo, schema-aware identifier completion, and invalid-identifier live feedback. Total stage-8 footprint: 5 commits, ~1600 lines. |