Commit Graph

340 Commits

Author SHA1 Message Date
claude@clouddev1 c1c9f6cbc4 runtime: extract the indicator debounce into a tested state machine
The validity-indicator debounce was two locals in the event
loop (indicator_pending + app.input_indicator) with no unit
coverage — ADR-0027's as-built notes flag it as untested async
glue. The decision logic is now an IndicatorDebounce struct:
note_event (a keystroke hides + arms; non-key events leave it
be), settle (the quiet window elapsed → show the verdict +
disarm), is_armed (drives the recv timeout), visible (mirrored
into app.input_indicator for the renderer).

No behaviour change — the tokio timer and terminal stay in the
loop. 7 unit tests cover the debounce contract: the keystroke /
settle cycle, clean verdicts, and that a background event
mid-typing does not cancel the owed recompute. 1125 passing,
clippy clean.
2026-05-19 09:44:28 +00:00
claude@clouddev1 400fb71460 ui: surface diagnostics in the ambient hint panel (ADR-0027 §2)
ambient_hint now reads the walker's schema-aware diagnostics.
input_diagnostics is non-empty only for a command that
structurally parses — so a non-empty result means "complete
and submittable, but wrong or dubious". That is checked early
(right after the Tab-cycle memo), ahead of slot hints and
completions: a command that parses but is flawed no longer
gets the misleading "Submit with Enter" prose, it gets the
diagnostic's why. pick_hint_diagnostic prefers the diagnostic
under the cursor, else the most severe.

The cursor-local invalid-ident hint is kept for genuinely
incomplete commands (no Match → no diagnostics).

5 ambient_hint tests (unknown table, type-mismatch over
submit-prose, LIKE-numeric, clean command still submittable,
cursor-following). The complex_and_or matrix cell referenced a
non-existent column `t`; fixed to a real column so it tests a
valid expression as intended. 1118 passing, clippy clean.
2026-05-19 09:39:58 +00:00
claude@clouddev1 bbfb70c767 ui: overlay diagnostic spans on the input field (ADR-0027 §2)
render_input_runs now overlays the walker's schema-aware
diagnostics: an unknown table/column ERROR is recoloured
tok_error, an expression WARNING (type mismatch, = NULL, LIKE
on a numeric column) recoloured theme.warning. New overlay_span
covers a token's whole byte range (overlay_error only hits the
run at a single byte). New walker::input_diagnostics is the
shared entry point.

The overlay is global — every flagged token is coloured
wherever it sits, not only under the cursor — which is exactly
ADR-0027's motivation. The existing cursor-local invalid-ident
overlay is kept (it covers in-progress idents diagnostics do
not); the two are additive and idempotent.

5 input_render tests (unknown table/column, type-mismatch
literal precise, LIKE-on-numeric, clean command). 1113 passing,
clippy clean.
2026-05-19 09:32:52 +00:00
claude@clouddev1 437b2f2e91 walker: flag LIKE on a numeric column (ADR-0027 Amendment 1)
LIKE is a text-pattern match; against a numeric column (int,
real, decimal, serial) it runs but is almost never intended.
predicate_warnings now emits a WARNING for it, spanned at the
target column. New Type::is_numeric; catalog key
diagnostic.like_numeric; ADR-0027 gains "Amendment 1" and the
adr/README index line is updated per the index-upkeep rule.

bool and the text-/blob-backed types are deliberately not
flagged — see the amendment for the rationale.

3 walker tests (int, decimal NOT LIKE, text-column clean).
1108 passing, clippy clean.
2026-05-19 09:28:43 +00:00
claude@clouddev1 3912fb5a9b walker: precise per-literal spans for expression WARNINGs
Expression WARNING diagnostics (type mismatch, = NULL) carried
a coarse span — the whole WHERE clause, from the `where`
keyword to end of input. They now span exactly the offending
literal operand, read from the Operand source span added in the
previous commit. predicate_warnings derives the span per
warning; pair_type_mismatch returns (message, literal-span);
the dead where_clause_span helper is removed.

5 walker tests assert the spans cover exactly the literal /
identifier (type mismatch, = NULL, BETWEEN bounds, IN item,
unknown-column ERROR). 1105 passing, clippy clean.
2026-05-19 09:24:44 +00:00
claude@clouddev1 426e80185f command: Operand carries a source span
Each WHERE-expression Operand now records the byte span of the
terminal it was built from — the precise per-literal highlight
target for an expression WARNING (finishing ADR-0027 §2's
highlight/hint wiring). parse_operand captures MatchedItem::span;
the RowFilter::eq convenience constructor uses Operand::NO_SPAN.

PartialEq is hand-written to ignore the span — it is editor
metadata, so Command equality stays whitespace- and
position-independent, which the Expr test corpus relies on.
No behaviour change; 1100 tests still pass, clippy clean.
2026-05-19 09:20:52 +00:00
claude@clouddev1 39b92a7558 docs: handoff 18 — record the post-ADR-0027 manual-testing pass
The multi-form usage-template fix (151ed08) and the reviewed
`add index` syntax decision (kept as-is), so the next agent
does not re-flag a settled question.
2026-05-19 08:45:45 +00:00
claude@clouddev1 151ed084a3 hint: show the matching usage template for multi-form commands
A parse error in `add index …` showed the `add column` usage:
`add` and `drop` are multi-form commands, and both the
ambient hint and the submit-time usage block picked the
first-listed form unconditionally.

New `grammar::usage_key_for_input` disambiguates by the form
word after the entry keyword — `column` / `index` / `table` /
`relationship`, or the leading digit of `add 1:n …`. The
ambient hint now shows that one form; `render_usage_block`
shows the committed form's usage and falls back to the whole
family only for a bare `add` / `drop` with no form chosen.
2026-05-19 08:37:17 +00:00
claude@clouddev1 5dc0421bd2 chore: handoff 18 — ADR-0027 input-validity indicator implemented 2026-05-19 07:36:13 +00:00
claude@clouddev1 a3268495e2 ADR-0027: existing-cases sweep + docs (step F)
Sweep: input_verdict tests confirm the schema-existence check
fires across the identifier-taking commands — unknown table
on drop / show / add column, unknown column on drop column /
update — and that known references stay clean. The Step B
check is grammar-generic, so this is verification + coverage
rather than new code.

Docs: requirements.md S6 -> [x], baseline 1096; CLAUDE.md
deferred list reconciled (C5a and S6 are done — removed);
ADR-0026's as-built note updated (step 5 shipped via
ADR-0027); ADR-0027 gains an As-built notes section
recording the post-walk diagnostics realization, the
pre-rendered message, the timeout-based debounce, coarse
WARNING spans, and the deferred highlight/hint wiring.
2026-05-19 07:35:06 +00:00
claude@clouddev1 9e10997ffd runtime: debounce the validity indicator (ADR-0027 step E)
The event loop now time-boxes `recv` while an indicator
recompute is owed: every keystroke hides the indicator and
arms an `INDICATOR_DEBOUNCE` (1s) window; once typing pauses
that long the runtime computes `App::input_validity_verdict`
and shows `[ERR]` / `[WRN]`. An idle session (nothing owed)
still blocks plainly on `recv` — no wake-ups.

`update()` stays pure — the debounce timer lives in the
runtime; `App` only holds the resulting `input_indicator`
state, which the runtime clears on a keystroke and sets when
the quiet interval elapses.

`App::input_validity_verdict` is tested directly (a
simple-mode verdict, and silence in advanced mode / the `:`
one-shot); the debounce timing itself is runtime-loop glue,
covered at the integration level.
2026-05-19 07:30:47 +00:00
claude@clouddev1 1a9d950cc2 ui: validity indicator rendering + warning theme colour (ADR-0027 step D)
Adds the `[ERR]` / `[WRN]` validity indicator to the input
row. `App` gains `input_indicator: Option<Severity>` (the
runtime owns its timing — step E) and a pure
`input_validity_verdict()` query that runs `input_verdict`
in simple mode only (advanced mode is raw SQL, ADR-0027 §7).

`render_input_panel` reserves the rightmost six columns of
the input row unconditionally (ADR-0027 §4) — a five-column
label plus a one-column gap — so the typed command never
shifts sideways when the indicator appears or hides. The
label renders only when `input_indicator` is set: `[ERR]` in
`theme.error`, `[WRN]` in the new amber `theme.warning`
(defined for both light and dark themes).

The indicator is not yet wired live — `input_indicator`
stays `None` until the debounce lands (step E). Covered by a
render test and the theme contrast test; the input-panel
snapshot is updated for the six-column reservation.
2026-05-19 07:27:54 +00:00
claude@clouddev1 73c74701c2 walker: expression WARNING diagnostics (ADR-0027 step C, folds ADR-0026 §7)
Type-mismatched comparisons and `= NULL` / `!= NULL` in a
WHERE expression now yield WARNING diagnostics — the command
still parses and runs (the ADR-0026 §7 permissive posture is
unchanged), but the validity indicator can flag it before
submission.

Computed post-walk from the built command's `Expr` against
the table's column types: a Compare / Between / In with a
column operand and a non-null literal whose type the column
cannot hold, or a Compare with `=` / `!=` against NULL. New
catalog keys `diagnostic.type_mismatch` / `diagnostic.eq_null`.

This is ADR-0026's deferred step 5, folded into ADR-0027's
diagnostics-severity model as the user requested.
2026-05-19 07:21:30 +00:00
claude@clouddev1 827b47f88f walker: schema-existence ERROR diagnostics (ADR-0027 step B)
`MatchedKind::Ident` now carries its `IdentSource`. A
post-walk pass over a structurally-valid parse flags a
matched `Tables` ident that is absent from the schema, or a
`Columns` ident absent from the table in scope, as an ERROR
diagnostic — the command parses but would fail at execution
(ADR-0027 §2). New behaviour: an unknown table / column used
to parse cleanly and fail only when run.

Column scope is resolved by one left-to-right pass over the
matched path (every command places its table ident before
the columns that belong to it); an unknown table clears the
scope, so its columns are not cascaded into a second
diagnostic. New catalog keys `diagnostic.unknown_table` /
`diagnostic.unknown_column`.
2026-05-19 07:15:58 +00:00
claude@clouddev1 e22f933e02 walker: diagnostics-severity model + input_verdict (ADR-0027 step A)
Adds `Severity` (Error / Warning, ordered so Error > Warning)
and `Diagnostic { severity, span, message }` in
`walker::outcome`, plus a `diagnostics` field on `WalkResult`
— the schema-aware findings layered on a structurally-valid
parse (ADR-0027 §2).

`input_verdict(source, schema)` is the validity-indicator
entry point: `None` when the input would run clean (and for
empty input), `Some(Error)` for a parse failure or unknown
command, `Some(Warning)` for the ADR-0026 expression flags.
The verdict is the highest severity across the parse outcome
and the diagnostics set.

`diagnostics` is empty at this step — the schema-existence
(ERROR) and expression (WARNING) passes that fill it land
next. Covered by `input_verdict` unit tests.
2026-05-19 07:08:13 +00:00
claude@clouddev1 dfd3c51643 chore: handoff 17 — ADR-0026 complex WHERE expressions implemented 2026-05-18 23:21:23 +00:00
claude@clouddev1 a50c6cdf70 WHERE expressions: matrix cells + predicate_tail grammar fix (ADR-0026 step 6)
Adds tests/typing_surface/where_expression.rs — 9 matrix
cells for the complex WHERE / show-data limit typing surface:
operator candidates after an operand, AND / OR after a
predicate, NOT, BETWEEN / IN bounds, and `show data`
where / limit.

Writing the cells surfaced a grammar bug. `predicate_tail`'s
`[NOT] negatable` branch started with `Optional(not)`, and an
Optional-first `Seq` always "commits" — so on an incomplete
input the walker's `Choice` returned that branch's
`Incomplete` early and discarded every sibling branch's
expected set, dropping `is` and the comparison operators from
completion after a column. Fixed by splitting it into
explicit `NOT negatable` and bare `negatable` branches — no
`predicate_tail` branch starts with an `Optional` now. The
matched terminal sequence is unchanged, so `build_expr` is
untouched.

Docs: ADR-0026 gains an "As-built notes" section recording
the option-1 builder realization, its two deviations from the
§3 sketch, and the deferral of §7 diagnostic flagging to
ADR-0027. requirements.md C5a -> [x] (steps 1-4) with the
test baseline refreshed to 1079; CLAUDE.md's deferred list
reconciled (C5a implemented; the QA1/QA2 note now points at
ADR-0028).
2026-05-18 23:19:53 +00:00
claude@clouddev1 f75f71bbe4 WHERE expressions: wire into update/delete/show data + SQL gen (ADR-0026 steps 3-4)
Wires the stratified WHERE-expression fragment into the three
filter commands and compiles the resulting Expr to SQL.

Grammar (data.rs): the `update` / `delete` `where` clause is
now the expression fragment (`Subgrammar(&expr::OR_EXPR)`) in
place of the single `col = val` slot; `show data` gains an
optional `where <expr>` and an optional `limit <n>` (a
non-negative integer, validated at parse time). The
expression's right-hand operands are a schema-aware
`DynamicSubgrammar` so the hint panel still narrows to the
left column's type (ADR-0026 §8) — but the inner grammar is
permissive: a type-mismatched literal still parses (§7).

AST: `RowFilter::Where{column,value}` -> `RowFilter::Where(Expr)`;
`ShowData` gains `filter: Option<Expr>` and `limit: Option<u64>`.
A `RowFilter::eq` convenience constructor keeps simple-equality
call sites and tests readable.

SQL (db.rs): `compile_expr` lowers an `Expr` to a
parameterised WHERE — every literal a `?` placeholder,
identifiers `quote_ident`-quoted, `<>` for inequality. A
literal compared against a column binds through that column's
type where compatible and falls back to its syntactic shape on
a mismatch (§7 — permissive). `show data ... limit n` emits
`LIMIT ?` with an implicit primary-key `ORDER BY`, so it is a
stable "first n by primary key".

completion.rs: `invalid_ident_at_cursor` no longer mis-flags a
digit-led literal (`1`) as an unknown column now that the
WHERE operand slot also accepts a column reference; a
`ProseOnly` slot suppresses keyword candidates even when the
expected set also carries a column ident.

11 db integration tests cover AND / OR / NOT, BETWEEN, IN,
LIKE, filtered `show data`, and limit ordering; walker and
expr unit tests cover the parse surface. Type-mismatch /
`= NULL` diagnostic flagging (§7 highlight + hint) is the
remaining ADR-0026 piece.
2026-05-18 23:12:33 +00:00
claude@clouddev1 59e6a541bf grammar: WHERE-expression fragment + Expr AST + build_expr (ADR-0026 step 2)
The stratified WHERE-expression grammar — or / and / not /
bool_primary / predicate tiers as named `static` Node
fragments, recursing through `Subgrammar`. Covers the six
comparison operators (`<>` and `!=` both NotEq), AND / OR /
NOT, parentheses, LIKE / IN / BETWEEN with optional infix NOT,
and IS [NOT] NULL. `predicate_tail` factors the shared operand
prefix and the infix NOT so the Choice branches discriminate
on a cleanly-failing first token.

New recursive Expr / Predicate / Operand / CompareOp AST in
dsl::command. `build_expr` folds the flat matched-terminal
slice into an Expr — a deterministic recursive descent
mirroring the grammar tiers, with single-child tiers
collapsing. Per ADR-0026 §3 option 1: the walker stays a pure
structural matcher; Expr is assembled only in this
submit-time fold.

Fragment + builder are unit-tested standalone (walk against
&OR_EXPR, then build_expr); not yet wired into any command.
2026-05-18 22:40:52 +00:00
claude@clouddev1 f0b2043a39 walker: add Subgrammar node + recursion-depth cap (ADR-0026 step 1)
New `Node::Subgrammar(&'static Node)` variant lets a named
static grammar fragment recurse through a reference — `Seq` /
`Choice` embed children by value and cannot close a cycle, but
a `&'static Node` can point back at an enclosing fragment. This
is the mechanism the stratified WHERE-expression grammar
(ADR-0026 §2) recurses through.

The walker counts active Subgrammar frames in
`WalkContext::subgrammar_depth` and refuses past
`MAX_SUBGRAMMAR_DEPTH` (64), surfacing a friendly
`parse.custom.expression_too_deep` error instead of a stack
overflow. Depth is saved/restored per frame so a
speculatively-walked-then-rolled-back Choice branch leaves no
residue.

No grammar references the node yet; covered by walker unit
tests with a small recursive `( x )` test grammar.
2026-05-18 22:36:19 +00:00
claude@clouddev1 ac41938365 chore: handoff 2026-05-18 22:07:54 +00:00
claude@clouddev1 d9a98bbd49 Grammar: with-pk column specs use name(type), matching add column
`create table … with pk` parsed column types as `name:type`,
while `add column` uses `name(type)`. Unify on the parens
form so column-type syntax is consistent across the DSL:

    create table T with pk id(serial), name(text)

Only `COL_SPEC` changes (`:` → `( … )`); `build_create_table`
reads columns by role, so it is unaffected. The `:` that
separates table from column in `add column` / `drop column`
is unchanged. Sweeps the test suite, the typing-surface
matrix (two `after_colon` cells renamed to `after_paren`,
4 snapshots regenerated), the friendly catalog's usage
templates, ADR-0009's example, and requirements.md.

1039 passing / 0 failing / 1 ignored; clippy clean.
2026-05-18 21:51:52 +00:00
claude@clouddev1 9aa7e2ede0 docs: add ADR-0028 — query plans (EXPLAIN QUERY PLAN)
The QA1/QA2 design: an `explain` prefix command over
`show data` / `update` / `delete` that runs
EXPLAIN QUERY PLAN (without executing the statement) and
renders the result as an annotated tree. Plan steps keep
the engine's own wording; an annotation taxonomy marks
full scans, index use, and the automatic-index "you
should add an index here" case. Introduces a general
styled-output-line mechanism — an OutputLine may carry
per-span styling — realising the per-span theming
ADR-0016 deferred; the plan renderer is its first
consumer. The explained SQL is shown above the tree as
standard, copy-pasteable SQL.

- docs/adr/0028-query-plans.md — the ADR.
- docs/adr/README.md — index entry.
- docs/requirements.md — QA2 [~] -> [ ]; QA1 note
  reconciled (designed in ADR-0028).
2026-05-18 21:27:52 +00:00
claude@clouddev1 032a050f7b docs: add ADR-0027 — input-field validity indicator
A debounced `[ERR]` / `[WRN]` marker at the right edge of
the input row, summarising — before submit — whether the
current command would run. Backed by a small
diagnostics-severity model: the walker emits severity-
tagged diagnostics (parse outcome, schema-existence of
table / column names) that the indicator summarises and
the existing highlighting / hint layers detail. Advisory
only — submission is never blocked.

- docs/adr/0027-input-validity-indicator.md — the ADR.
- docs/adr/README.md — index entry.
- docs/requirements.md — new S6 (TUI shell).
2026-05-18 20:46:06 +00:00
claude@clouddev1 6e42a118a3 docs: add ADR-0026 — complex WHERE expressions
The C5a design: a stratified, recursive WHERE-expression
grammar (AND/OR/NOT, comparisons, LIKE, IS NULL, IN,
BETWEEN) for update / delete / show-data filters; show
data gains optional `where` and `limit`. Adds the
`Subgrammar` reference-following grammar node and a
recursive `Expr` AST, built selectively for the
expression fragment.

- docs/adr/0026-complex-where-expressions.md — the ADR.
- docs/adr/README.md — index entry.
- docs/simple-mode-limitations.md — new running list of
  simple-mode query boundaries vs. advanced SQL, seeded
  from ADR-0026.
- docs/requirements.md — C5a [~] -> [ ] (designed, not
  yet implemented); new Documentation section with DOC1.
2026-05-18 10:34:12 +00:00
claude@clouddev1 f4eedf3dd2 chore: handoff 2026-05-17 09:22:49 +00:00
claude@clouddev1 4e4dbbffe3 Input history: reset the nav cursor on every submit
`push_history` skipped its `history_cursor` / `history_draft`
reset on the consecutive-duplicate early-return path. Recalling
a command with Up and re-submitting it unchanged left the cursor
stranded at that entry, so the next Up stepped backwards from
there instead of restarting at the newest entry.

Move the reset ahead of the early-return guards. Adds a Tier-1
regression test driving the recall/resubmit keystroke sequence.
2026-05-17 09:17:20 +00:00
claude@clouddev1 0dc159fd7e Indexes: add index / drop index, persistence, display (ADR-0025)
Implement ADR-0025 — indexes as a DSL DDL feature.

- Grammar: `add index [as <name>] on <T> (<cols>)`, `drop index
  <name>` / `drop index on <T> (<cols>)`, plus a `--cascade`
  flag on `drop column`.
- db.rs: index operations over the engine's native index
  catalog (no metadata table). The rebuild-table primitive now
  captures and recreates indexes, so `change column` and the
  relationship operations no longer silently drop them.
- `drop column` refuses an indexed column unless `--cascade`,
  which drops the covering indexes and reports each.
- Persistence: additive `indexes:` list in `project.yaml`
  (version unchanged); round-trips through rebuild/export/import.
- Display: an `Indexes:` section in the structure view and a
  nested tables/indexes items panel (S2).

Reconciles requirements.md (C3 index portion, S2 satisfied)
and CLAUDE.md. 1038 tests passing (+31), clippy clean.
2026-05-16 00:15:55 +00:00
claude@clouddev1 41043d686b docs: record ADR-0024 completion, reconcile requirements.md + handoff-14
ADR-0024 audited as fully implemented. Amend the ADR with a "Phase F
minimal" implementation note (parser.rs retained as the router +
ParseError home) and update the README index line to match.

Reconcile docs/requirements.md against handoffs 10-14: refresh the
test baseline (449 -> 1006), mark U4 (replay) satisfied, correct the
A1 / H1a / H3 progress notes.

Amend handoff-14: §3 flagged items both resolved (ranker kept,
CommandNode.hint_mode removed); §4 rewritten as a concrete next-work
pointer at the reconciled requirements.md.
2026-05-15 23:03:18 +00:00
claude@clouddev1 6d2b92996d Grammar: remove the dead CommandNode.hint_mode field
HintMode became per-node (Node::Hinted) in the node-attached refactor;
the per-command hint_mode field was never the mechanism and is now
read by nothing. Removed the field and its 20 `None` initialisers.
2026-05-15 22:54:24 +00:00
claude@clouddev1 5fa3460ff6 add handoff-14: handoff-12 §2 backlog cleared (8 items) 2026-05-15 22:48:09 +00:00
claude@clouddev1 03dd9003df Help: consume CommandNode.help_id — REGISTRY-driven in-app help
Every CommandNode declared a help_id that nothing read; the in-app
`help` body was a single hand-kept catalog block that drifted from
the command set (handoff-12 §2.1).

note_help now iterates the command REGISTRY and translates each
CommandNode's help_id (`help.<id>`), framed by help.intro /
help.dsl_section / help.types_reference. A newly-registered command
appears in `help` automatically — no edit to note_help or a hand-kept
list. Added 20 per-command help entries plus the 3 framing entries;
removed help.in_app_body.

Per-command entries use block scalars: a libyml 0.0.5 scanner bug
panics on long internal space runs in double-quoted scalars, and the
entries are space-aligned.
2026-05-15 22:45:18 +00:00
claude@clouddev1 f46606b12e Runtime: schema-aware replay parsing
run_replay parsed each line with the schemaless parse_command, so
Phase D typed-slot rejections (wrong-count value lists, wrong-type
column values) fired only at bind time during replay — inconsistent
with the interactive path (handoff-12 §2.1).

run_replay now re-snapshots the schema per line (the schema mutates
as replayed create-table / add-column commands run) and parses with
parse_command_with_schema. Extracted build_schema_cache, shared with
the interactive refresh_schema_cache.

Added a replay integration test asserting a typed-slot violation is
caught at parse time (through the replay.error_parse wrapper).
2026-05-15 22:31:19 +00:00
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 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.
2026-05-15 22:06:33 +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 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.
2026-05-15 21:30:03 +00:00
claude@clouddev1 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.
2026-05-15 21:29:54 +00:00
claude@clouddev1 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.
2026-05-15 21:29:36 +00:00
claude@clouddev1 42cf851f7e add handoff-13: typing-surface matrix complete + 5 matrix-found bug fixes 2026-05-15 20:53:25 +00:00
claude@clouddev1 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).
2026-05-15 20:50:56 +00:00
claude@clouddev1 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.
2026-05-15 20:50:56 +00:00
claude@clouddev1 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.
2026-05-15 20:34:01 +00:00
claude@clouddev1 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.
2026-05-15 20:31:01 +00:00
claude@clouddev1 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.
2026-05-15 20:31:01 +00:00
claude@clouddev1 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).
2026-05-15 20:06:58 +00:00
claude@clouddev1 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.
2026-05-15 20:06:52 +00:00
claude@clouddev1 3b1955c6bf add handoff-12: post-Phase-D UX fixes + test-coverage agenda for next session 2026-05-15 19:15:27 +00:00
claude@clouddev1 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.
2026-05-15 19:07:46 +00:00
claude@clouddev1 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.
2026-05-15 18:58:28 +00:00