Files
rdbms-playground/docs/handoff/20260515-handoff-10.md

443 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session handoff — 2026-05-15 (10)
Tenth handover. This session executed **ADR-0024 Phase F (full)
steps 14** on top of handoff-9: the walker now drives
highlighting, usage rendering, and the schema-list / completion
ident-source vocabulary. `dsl::lexer`, `dsl::keyword`,
`dsl::ident_slot`, and `dsl::usage` are deleted; their
catalog wrappers (`parse.token.keyword.*` and
`parse.token.punct.*`) are deleted too. Four commits in clean
incremental steps.
What did NOT land — and what the next session picks up — is
captured below.
## State at handoff
**Branch:** `main`. Working tree clean. **Local HEAD is
`fa994cf`**, ahead of `origin/main` by five commits (user pushes
asynchronously).
Commits since handoff-9's baseline (`b3d3bdf`):
```
7bdd398 ADR-0024 Phase F (full) step 1: walker-driven highlighting
a41400e ADR-0024 Phase F (full) step 2: usage via CommandNode.usage_ids
266b4c2 ADR-0024 Phase F (full) step 3: delete legacy parser modules
fa994cf ADR-0024 Phase F (full) step 4: catalog token-keyword cleanup
```
**Tests:** **806 passing, 0 failing, 1 ignored** (was 844 at
handoff-9; the net drop reflects the removal of legacy-module
tests — ~45 tests in `dsl::lexer`, `dsl::keyword`,
`dsl::ident_slot`, `dsl::usage` — minus 16 new walker
highlight tests). The ignored test is still the same `\`\`\`ignore`
doc-test in `src/friendly/mod.rs`.
**Clippy:** clean with `nursery` lints + `-D warnings`.
**Cargo.toml:** unchanged (chumsky was already gone in handoff-9).
## What shipped this session
### Step 1 — Walker-driven highlighting (commit 7bdd398)
`input_render::base_runs` no longer calls `lex()`. The new
`walker::highlight::highlight_runs(source) -> Vec<ByteClass>`
combines walker `per_byte_class` output (for matched portions)
with a byte-shape fallback (over `lex_helpers`) for trailing
junk / unknown-command words / unterminated strings. The
fallback handles UTF-8 codepoints byte-exact and produces an
`Error` class for unrecognised bytes — preserves the `$` →
`tok_error` behaviour from the lexer-driven path.
`Theme::highlight_class_color(HighlightClass) -> Color`
replaces `Theme::token_color(&TokenKind)`. The renderer reads
`HighlightClass` directly off `ByteClass`.
`#[allow(dead_code)]` is off `HighlightClass` and
`WalkResult::per_byte_class` — they are part of the production
path now.
16 new walker-highlight tests pin the byte-class output for
walks, fallbacks, multi-byte UTF-8, and the trailing-token case.
### Step 2 — Usage rendering via `CommandNode.usage_ids` (commit a41400e)
`CommandNode.usage_id: Option<&'static str>` becomes
`usage_ids: &'static [&'static str]`. Multi-form families
(`drop`, `add`, `show`) carry every variant — the legacy
`dsl::usage::matched_entry` returned multi-key Vec for those
families and the walker registry now matches that.
App-lifecycle commands had been pointed at non-existent
`parse.usage.app.*` catalog keys (an unnoticed bug introduced
in handoff-9 because the field was unused at the time); they
now point at the real catalog entries.
Two new helpers in `dsl::grammar`:
- `usage_keys_for_input(source) -> Option<(entry_word_text, usage_ids)>`
resolves the input's first identifier-shape token to a
CommandNode and returns its usage_ids list. Used by
`app::render_usage_block` (parse-error rendering) and
`input_render::ambient_hint` (live hint panel).
- `entry_words_alphabetised() -> Vec<&'static str>` replaces
`dsl::usage::entry_keywords_alphabetised`.
`dsl::usage` is deleted entirely. The "available commands:"
fallback in `render_usage_block` formats entry words as
`` `<word>` `` directly (replacing the `parse.token.keyword.*`
catalog lookup; equivalent rendering).
`parse_command` and `parse_tokens` slim down: no pre-lex pass;
the walker scans source bytes directly. `parse_tokens` (which
had been kept `pub` "for future I3/I4 work") is folded into
`parse_command`.
### Step 3 — Legacy module deletion (commit 266b4c2)
Deleted: `src/dsl/lexer.rs`, `src/dsl/keyword.rs`,
`src/dsl/ident_slot.rs`.
`IdentSource` (`dsl::grammar`) absorbs the schema-list /
expected-label / round-trip semantics that previously lived on
`IdentSlot`. The walker's `Expectation::Ident { source }` and
the schema-lookup request on the database worker now share one
enum:
- `IdentSource::Tables`, `Columns`, `Relationships` are the
schema-listable sources (`completes_from_schema() == true`).
- `IdentSource::NewName` is the user-invents kind.
- `IdentSource::Types` is the closed-set source on column-type
slots — does not query the schema; the walker's content
validator handles type-name validity.
- `IdentSource::Free` is the catch-all branch in `mode` /
`messages` value slots.
`SchemaCache::for_slot(IdentSlot)` becomes
`for_source(IdentSource)`; `Database::list_names_for` and
the `Request::ListNamesFor` worker variant take `IdentSource`.
`InvalidIdent.slot: IdentSlot` becomes
`InvalidIdent.source: IdentSource`.
Completion's keyword filter (was `Keyword::from_word`) becomes
"backticked items whose payload is all ASCII alphabetic" —
punct and digit literals (`,`, `1`) 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 (cross-checked enum vs. catalog completeness; both
enums are gone).
### Step 4 — Catalog token-keyword cleanup (commit fa994cf)
Dropped the 47 `parse.token.keyword.*` and 6
`parse.token.punct.*` catalog entries (yaml + keys.rs). Nothing
consumes them: keyword wording is produced verbatim by
`format!("\`{word}\`")`, sourced from grammar-tree Word
literals. Punct wording surfaces the same way via
`Expectation::Punct(ch)`.
Structural-class labels (`parse.token.identifier`, `.number`,
`.string_literal`, `.flag`, `.end_of_input`) and lex-error
wordings (`parse.token.error.{bad_flag,unknown_char,
unterminated_string}`) stay. None of these are derivable from
the grammar tree.
`keys_validate_against_catalog` continues to enforce catalog ↔
`KEYS_AND_PLACEHOLDERS` bidirectional coverage on the trimmed
set.
## DEFERRED — work the next session needs to pick up
Items grouped by priority. Roughly in order of decreasing payoff.
### 1. Walker-driven completion (Phase F full step 5)
The current completion path:
```
input → parse_command → ParseError::Invalid::expected: Vec<String>
→ completion.rs parses each string back to IdentSource
via from_expected_label
```
This works but loses information. The walker knows the full
`IdentSource` of every expected slot, the `role` of every slot
(e.g. `parent_table` vs `child_table`), the `skipped`
expectations from any Optional that didn't engage, the cursor's
full `MatchedPath` so far — and the bridge throws all of that
away to render strings.
**Migration**: add a `expected_walker: Vec<Expectation>` field
on `ParseError::Invalid` (additive, doesn't break consumers
yet). Or a new `walker::candidates_at(input, cursor) ->
WalkResult` API. Update `completion.rs::candidates_at_cursor`
to read `Expectation::Ident { source, role }` directly.
The walker already supports `WalkBound::Position(cursor)` (the
variant exists in `outcome.rs`); no caller passes it today.
Adopting it lets completion see the cursor's full context.
**Estimated cost:** one session. Smaller than the steps that
landed this round.
### 2. Phase D (full): schema-aware value typing
Unchanged from handoff-9 §2 — same scope, same blockers.
Specifically:
- `WalkContext::current_table`, `current_table_columns`,
`current_column` are declared but unwritten.
- `Node::DynamicSubgrammar(fn(&WalkContext) -> Node)` is
declared; the walker driver returns `Failed { expected:
vec![] }` on that branch (deliberate — catches mis-declared
grammar). Walker dispatch needs implementing per ADR-0024
§sub-grammars (`Box::leak` per walk, or per-walk arena).
- `SchemaCache` carries flat `columns: Vec<String>`; per-table
column-with-type info needs adding.
- `Ident { source: Tables }` has no `writes_table: bool` flag;
walker can't populate `current_table` on match.
- Typed value slots (`int_slot`, `decimal_slot`, …) are not
declared.
**The user UX win this unlocks:** typed slots reject mis-shaped
input at parse time with localised wording ("Type a date as
'YYYY-MM-DD'") instead of bind-time errors; completion narrows
per column type. Round-5 "value-literal hint by column type"
becomes type-specific.
**Sequence to implement** (from handoff-9 §2):
1. Plumb a `SchemaCache` reference into `parse_command`; thread
through `WalkContext::new(schema)`.
2. Implement `Node::DynamicSubgrammar` walker dispatch.
3. Add `writes_table: bool` (or analogous) to `Node::Ident`
and have the walker populate `WalkContext::current_table` +
`current_table_columns` from the schema cache when it
matches.
4. Implement typed value slots — content validators for each
`Type`.
5. Wire `column_value_list` as a `DynamicSubgrammar` that reads
`current_table_columns` and emits a `Seq` of typed slots
separated by commas.
6. Update `insert` shape to use `column_value_list`.
7. Update `update` / `delete` to use the per-column value slot
based on `current_column`.
**Side effect to watch:** `parse_command` becomes
schema-dependent. Tests that exercised parse in isolation
without a schema will need to either pass a schema cache or fall
back to the type-unaware path (Option<&SchemaCache>).
**Estimated cost:** 12 sessions, as last session estimated.
### 3. HintMode annotations on grammar nodes
`HintMode::{Default | ForceProse | ProseOnly | SuppressProse}`
is declared in `grammar/mod.rs`. Every `CommandNode` and every
`Node::Ident` sets it to `None`. The current ad-hoc hint cases
in `input_render.rs::ambient_hint` (`value_literal_hint_at_cursor`,
`typing_name_at_cursor`, `invalid_ident_at_cursor`) still drive
the hint panel.
ADR-0024 §HintMode-per-node says these migrate to node-attached
annotations during Phase D. They didn't (handoff-9 confirmed,
this session didn't move them either).
To do once Phase D lands: annotate the value-literal slots with
`HintMode::ProseOnly("value_literal_format_hint")`, NewName
slots with the typing-name prose, and have the hint resolver
dispatch on the walker's per-expected-node mode. The current
stopgap `value_literal_hint_at_cursor` becomes
"narrow to the column's type" instead of generic.
### 4. Ranker hook (declared in ADR-0024, not implemented)
ADR-0024 §ranker-layer specifies a `Ranker` function type
between the walker's raw candidates and the hint-panel
renderer. Default is `identity_ranker` (declaration-order
preserved).
**Status: not declared anywhere in code.** Future plug-in point
for frequency-based ranking, content-aware priors, recency.
Non-blocking.
### 5. Differential-test scaffolding wasn't built (carried from handoff-9)
ADR-0024 §test-discipline §3 specified a "differential check
during the migration window" — a test helper running both
parsers against the input corpus, asserting identical `Command`
output. Since chumsky is gone (removed in Phase F minimal),
running the differential retroactively would require
reconstructing the chumsky path from git history. Not worth it
— the hand-curated `dsl::walker::tests` (53 tests) and the
existing integration test suite serve the same regression-net
role.
### 6. `WalkContext` writes during walk — design exists, not implemented
Carried from handoff-9 §12. `Ident { source: Tables, writes_table:
true }` semantics: when the ident matches, the walker writes
`current_table` to context. Subsequent dynamic sub-grammars read
it. Today no `Ident` node has a `writes_table` field. Adding it
is part of Phase D §3 above.
### 7. `CommandNode.help_id` not consumed
Carried from handoff-9 §13. Every `CommandNode` declares
`help_id: Option<&'static str>` pointing into the catalog.
**No code reads it.** Wiring up in-app help to read this field
(replacing hand-curated `help.in_app_body` lookups) is future
work — not blocking, not user-facing.
### 8. Dead `parse.token.*` catalog entries
The five structural-class entries
(`parse.token.identifier/number/string_literal/flag/end_of_input`)
and three lex-error entries
(`parse.token.error.{bad_flag,unknown_char,unterminated_string}`)
remain in the catalog after step 4. They are unreferenced by
production code today — the walker classifies bytes by shape
for highlighting and emits structural / validation errors with
catalog keys directly (e.g., `mode.unknown`).
Conservative call this session: leave them in. They cost
nothing, and re-introducing them would be cheap if a future
need arises. If you decide they're truly dead, drop the entries
from `friendly/strings/en-US.yaml` and the corresponding
declarations from `friendly/keys.rs`.
## Sharp edges (carried from handoff-9 with updates)
- **Optional backtracking on partial-match** is intentional —
matches chumsky's `or_not` semantics. See handoff-9 for the
asymmetry between content (no rollback) and structural (roll
back). Unchanged.
- **Walker's `Choice` is strictly greedy.** Unchanged.
- **`Ident { source: Tables/Columns/Relationships }` does NOT
validate against the schema at parse time.** Still
shape-only. Phase D §3 unlocks schema-aware parse.
- **`Literal(&'static str)` matches verbatim bytes with a
word-boundary lookahead.** Unchanged.
- **`AST builder` failures surface as
`WalkOutcome::ValidationFailed`** with `at_eof = true`.
Unchanged.
- **`unknown_command_error` is the sole catch-all** for inputs
whose first identifier-shape token isn't a registered entry
word. Now read from `dsl::grammar::entry_words_alphabetised()`
(was `usage::entry_keywords_alphabetised`).
- **`q` quit alias remains gone.** Native walker alias support
still works: adding `q` back is `aliases: &["q"]` on
`QUIT.entry`. Walker matches either; completion surfaces only
the primary.
- **Path-bearing UX (replay / import / export):** unchanged
from handoff-9 — paths with spaces use the quoted form.
- **Highlight fallback semantics (new this session):** for
inputs the walker doesn't engage on (no registered entry
word, e.g., `frobulate widgets`) the byte-shape scanner
classifies each shape as Identifier, Number, String, Flag,
Punct, or Error. The user sees normal token colouring on the
unknown command before the `[error]` line fires on submit.
This matches the pre-walker behaviour.
- **Stopgap `value_literal_hint_at_cursor` continues to fire**
for every value-literal slot regardless of column type. Same
wording as handoff-9. Replaced once Phase D §4 typed slots
land.
## ADR index (delta vs. handoff-9)
```
0019 Friendly error layer and i18n catalog
— parse.token.keyword/punct.* entries collapsed (Phase F)
0020 Tokenization layer for the DSL parser
— superseded; dsl::lexer module deleted (Phase F)
0021 Parser-as-source-of-truth for H1a
— partial: usage info reads from CommandNode.usage_ids;
help_id wiring deferred (handoff-10 §7)
0022 Ambient typing assistance
— completion still reads ParseError-string expected;
walker-direct path pending (handoff-10 §1)
0024 Unified grammar tree: execution plan (ACCEPTED)
— A through F full steps 14 landed.
F step 5 (walker-direct completion) + D full deferred.
```
## Repository layout (delta vs. handoff-9)
Files deleted:
```
src/dsl/lexer.rs (598 lines)
src/dsl/keyword.rs (311 lines)
src/dsl/ident_slot.rs (140 lines)
src/dsl/usage.rs (318 lines)
```
Files added:
```
src/dsl/walker/highlight.rs (319 lines)
docs/handoff/20260515-handoff-10.md (this file)
```
Files significantly modified:
```
src/dsl/grammar/mod.rs — usage_ids, usage_keys_for_input,
entry_words_alphabetised,
IdentSource expanded with helpers
src/dsl/grammar/app.rs — usage_ids tuples (corrected catalog keys)
src/dsl/grammar/ddl.rs — usage_ids tuples (drop/add family)
src/dsl/grammar/data.rs — usage_ids tuples (show family)
src/dsl/parser.rs — slimmed; chumsky-side parse_tokens folded in
src/dsl/walker/mod.rs — re-export highlight_runs
src/dsl/walker/outcome.rs — ByteClass / WalkResult fields no longer
#[allow(dead_code)]
src/dsl/mod.rs — removed legacy module declarations
src/app.rs — render_usage_block via walker registry
src/input_render.rs — base_runs via walker::highlight_runs;
ambient_hint via usage_keys_for_input
src/completion.rs — IdentSource throughout; keyword filter
via ASCII-alphabetic check
src/db.rs — Request::ListNamesFor takes IdentSource
src/runtime.rs — refresh_schema_cache uses IdentSource
src/theme.rs — highlight_class_color; token_color removed
src/friendly/keys.rs — parse.token.keyword/punct.* dropped
src/friendly/strings/en-US.yaml — same drops
```
## How to take over
1. **Read this file.**
2. **Read `CLAUDE.md`** for the working-style rules.
3. **Read handoff-9** (`20260515-handoff-9.md`) for context on the
ADR-0024 phases that landed in the prior session and any
carry-over sharp edges.
4. **Read ADR-0024** for the design intent. F-step-5 (walker-direct
completion) and Phase D full are the unfinished work; their
sketches live in §migration of that ADR.
5. **Skim `src/dsl/grammar/mod.rs`** — the Node / Word /
CommandNode / REGISTRY / IdentSource / HintMode contract.
6. **Skim `src/dsl/walker/mod.rs` + `walker/highlight.rs`** — the
walk() entry, bridge logic, and the new highlight-runs API.
7. **Run `cargo test`** — should report 806 passing, 0 failing,
1 ignored.
8. **Run `cargo clippy --all-targets -- -D warnings`** — clean.
9. **Pick a deferred item from §1–§4** and start. §1 (walker-
driven completion) is the natural next move; it removes the
string round-trip and unlocks richer hints. §2 (Phase D full)
is the largest item and the biggest user-visible payoff.
### End-to-end smoke (current state)
Same as handoff-9's smoke; nothing in user-visible behaviour
changed this session. All four steps were silent refactors of
internal machinery — same parse, same dispatch, same wording
on errors, same colours on tokens. The catalog cleanup removed
unused YAML entries; no string the user sees was sourced from
them.