feat: H1a parse-error gaps G2–G4 + advanced near-miss matrix (ADR-0042)
Close the three remaining ADR-0042 triage gaps, each test-first, and lock the advanced-mode near-miss matrix. G2 — bare `select` dumped the 14-item expression first-set. Collapse it to "a projection: `*`, a column, or an expression" in the error message only (parser::format_walker_error), detected by the joint `distinct`+`all` quantifier signature unique to a projection start. Render-only: completion/hints still expand the full set (typing-surface matrix unchanged). G3 — the usage block was mode-blind: advanced `create table` showed the DSL `create table … with pk …` template. usage_key(s)_for_input gain mode-aware `_in_mode` variants selecting candidates by CommandCategory; render_usage_block and the typing-time ambient usage thread the submission mode. Advanced `create` now shows both SQL forms. A fallback covers shared SQL nodes (insert/update/delete) that declare no usage_ids of their own — without it they regressed to the available-commands fallback (caught by the new advanced matrix). G4 — `with` borrowed `select`'s usage template; give it its own parse.usage.with CTE template. Tests: new near_miss_matrix_advanced_mode (12 SQL-surface cases incl. the available-commands regression guard) + per-gap tests; removed the temporary baseline_dump. Full suite green (lib 1578 / it 386 / typing_surface_matrix 192); clippy clean.
This commit is contained in:
+75
-4
@@ -533,13 +533,73 @@ pub struct CommandNode {
|
||||
/// Returns the canonical (primary-form) entry literal and the
|
||||
/// `usage_ids` list, or `None` if no entry word matches.
|
||||
#[must_use]
|
||||
pub fn usage_keys_for_input(source: &str) -> Option<(&'static str, &'static [&'static str])> {
|
||||
pub fn usage_keys_for_input(source: &str) -> Option<(&'static str, Vec<&'static str>)> {
|
||||
usage_keys_for_input_in_mode(source, crate::mode::Mode::Simple)
|
||||
}
|
||||
|
||||
/// Mode-aware variant of [`usage_keys_for_input`] (ADR-0042 G3).
|
||||
///
|
||||
/// A shared entry word (`create`, `drop`, `insert`, …) registers a
|
||||
/// `Simple` DSL node *and* one or more `Advanced` SQL nodes. The
|
||||
/// usage block must reflect the surface the user is actually typing:
|
||||
/// the SQL forms in `Advanced` mode, the DSL forms in `Simple` mode
|
||||
/// — otherwise advanced-mode `create` shows the DSL `create table …
|
||||
/// with pk …` template, which is not valid SQL.
|
||||
///
|
||||
/// Selection prefers candidates whose [`CommandCategory`] matches
|
||||
/// the mode; if the entry word has none in that category (an
|
||||
/// app-lifecycle command is `Simple`-only yet usable in both modes),
|
||||
/// every candidate is used. The returned keys are the union of the
|
||||
/// selected nodes' `usage_ids`, de-duplicated in registry order — so
|
||||
/// advanced `create` shows both `sql_create_table` and
|
||||
/// `sql_create_index`.
|
||||
#[must_use]
|
||||
pub fn usage_keys_for_input_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Option<(&'static str, Vec<&'static str>)> {
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let start = skip_whitespace(source, 0);
|
||||
let (kw_start, kw_end) = consume_ident(source, start)?;
|
||||
let word = &source[kw_start..kw_end];
|
||||
let (_, node) = command_for_entry_word(word)?;
|
||||
Some((node.entry.primary, node.usage_ids))
|
||||
let candidates = commands_for_entry_word(word);
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let want = if mode == crate::mode::Mode::Advanced {
|
||||
CommandCategory::Advanced
|
||||
} else {
|
||||
CommandCategory::Simple
|
||||
};
|
||||
let union = |nodes: &[(usize, &'static CommandNode, CommandCategory)]| -> Vec<&'static str> {
|
||||
let mut keys: Vec<&'static str> = Vec::new();
|
||||
for (_, node, _) in nodes {
|
||||
for k in node.usage_ids {
|
||||
if !keys.contains(k) {
|
||||
keys.push(*k);
|
||||
}
|
||||
}
|
||||
}
|
||||
keys
|
||||
};
|
||||
let matched: Vec<(usize, &'static CommandNode, CommandCategory)> =
|
||||
candidates.iter().copied().filter(|(_, _, cat)| *cat == want).collect();
|
||||
// Prefer the mode-matching nodes' usage. But a shared SQL node
|
||||
// (`SQL_INSERT` / `SQL_UPDATE` / `SQL_DELETE`) declares no
|
||||
// `usage_ids` of its own — it reuses the DSL template. When the
|
||||
// mode-preferred set yields no usage keys, fall back to every
|
||||
// candidate so the entry word still shows a usage block rather
|
||||
// than the available-commands fallback (regression-locked by
|
||||
// the advanced near-miss matrix).
|
||||
let mut keys = union(&matched);
|
||||
if keys.is_empty() {
|
||||
keys = union(&candidates);
|
||||
}
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let entry = candidates[0].1.entry.primary;
|
||||
Some((entry, keys))
|
||||
}
|
||||
|
||||
/// The single usage template most relevant to `source`, when
|
||||
@@ -555,8 +615,19 @@ pub fn usage_keys_for_input(source: &str) -> Option<(&'static str, &'static [&'s
|
||||
/// show the whole family or nothing.
|
||||
#[must_use]
|
||||
pub fn usage_key_for_input(source: &str) -> Option<&'static str> {
|
||||
usage_key_for_input_in_mode(source, crate::mode::Mode::Simple)
|
||||
}
|
||||
|
||||
/// Mode-aware variant of [`usage_key_for_input`] (ADR-0042 G3) —
|
||||
/// disambiguates the single most-relevant usage key from the
|
||||
/// mode-selected key set.
|
||||
#[must_use]
|
||||
pub fn usage_key_for_input_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Option<&'static str> {
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let (_entry, keys) = usage_keys_for_input(source)?;
|
||||
let (_entry, keys) = usage_keys_for_input_in_mode(source, mode)?;
|
||||
let first = *keys.first()?;
|
||||
if keys.len() == 1 {
|
||||
return Some(first);
|
||||
|
||||
Reference in New Issue
Block a user