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.
This commit is contained in:
claude@clouddev1
2026-05-15 17:26:08 +00:00
parent 044173bd39
commit bbe12524ab
2 changed files with 163 additions and 114 deletions
+43
View File
@@ -30,6 +30,49 @@ use crate::dsl::walker::outcome::{
pub use context::ColumnInfo;
pub use highlight::highlight_runs;
/// What the grammar would accept at the end of `source`
/// (ADR-0024 §architecture, Phase F walker-driven completion).
///
/// Empty / whitespace-only input yields every command-entry word
/// as `Expectation::Word(primary)`. Otherwise the walker is
/// driven to `EndOfInput`; if the input completes a command,
/// the result is empty; if it fails or is incomplete, the
/// walker's expected-set surfaces verbatim — `Ident { source,
/// role }` carries its `IdentSource` (so the completion engine
/// can schema-look-up without a string round-trip), `Word` /
/// `Literal` carry their primary literal, etc.
///
/// Inputs whose first token is not a registered entry word
/// fall back to listing every entry word — matches the
/// synthetic "unknown command" expectation set the parser
/// produces.
#[must_use]
pub fn expected_at_input(source: &str) -> Vec<outcome::Expectation> {
use crate::dsl::grammar::REGISTRY;
if source.trim().is_empty() {
return REGISTRY
.iter()
.map(|c| outcome::Expectation::Word(c.entry.primary))
.collect();
}
let mut ctx = context::WalkContext::new();
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
match result.map(|r| r.outcome) {
Some(outcome::WalkOutcome::Match { .. }) => Vec::new(),
Some(outcome::WalkOutcome::Incomplete { expected, .. }) => expected,
Some(outcome::WalkOutcome::Mismatch { expected, .. }) => expected,
Some(outcome::WalkOutcome::ValidationFailed { .. }) => Vec::new(),
// Walker didn't engage (unknown entry word): the
// completion engine should still surface the available
// entry words so the user can recover.
None => REGISTRY
.iter()
.map(|c| outcome::Expectation::Word(c.entry.primary))
.collect(),
}
}
/// Public walk entry. `bound` is `EndOfInput` for parse;
/// `Position(cursor)` for completion / hint (Phase A: not yet
/// wired).