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:
@@ -60,85 +60,6 @@ fn dump(input: &str, lines: &[String]) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// TEMP baseline-capture (ADR-0042 §1 step 1). Lenient: does not
|
||||
/// assert pass/fail — just dumps every output line so we can read
|
||||
/// the current rendering before writing assertions. Run with:
|
||||
/// cargo test -p ... --test it baseline_dump -- --nocapture --ignored
|
||||
/// Removed once the matrix assertions land.
|
||||
#[test]
|
||||
#[ignore = "baseline capture only; run with --ignored --nocapture"]
|
||||
fn baseline_dump() {
|
||||
// (input, advanced?) — salient near-misses across entry words.
|
||||
let cases: &[(&str, bool)] = &[
|
||||
// --- app-lifecycle (simple) ---
|
||||
("quit now", false),
|
||||
("import", false),
|
||||
("mode sideways", false),
|
||||
("messages louder", false),
|
||||
("copy everything", false),
|
||||
("save sideways", false),
|
||||
// --- DDL bare + missing-slot (simple) ---
|
||||
("create", false),
|
||||
("create table", false),
|
||||
("create table T", false),
|
||||
("drop", false),
|
||||
("drop table", false),
|
||||
("add", false),
|
||||
("add column", false),
|
||||
("rename", false),
|
||||
("rename column", false),
|
||||
("change", false),
|
||||
("change column", false),
|
||||
// --- data bare + missing-clause (simple) ---
|
||||
("show", false),
|
||||
("show data", false),
|
||||
("insert", false),
|
||||
("insert into", false),
|
||||
("insert into T", false),
|
||||
("insert into T ('Oli')", false),
|
||||
("update", false),
|
||||
("update T", false),
|
||||
("update T set x=1", false),
|
||||
("delete", false),
|
||||
("delete from", false),
|
||||
("delete from T", false),
|
||||
("replay", false),
|
||||
("explain", false),
|
||||
// --- advanced-only entry words ---
|
||||
("select", true),
|
||||
("select *", true),
|
||||
("select * from", true),
|
||||
("with", true),
|
||||
("alter", true),
|
||||
("alter table T", true),
|
||||
// advanced-only word typed in SIMPLE mode → "this is SQL" hint
|
||||
("alter table T add column c int", false),
|
||||
("select * from T", false),
|
||||
// --- advanced SQL variants + genuine gaps ---
|
||||
("insert into T", true),
|
||||
("update T", true),
|
||||
("delete from", true),
|
||||
("create", true),
|
||||
("create table", true),
|
||||
("create index", true),
|
||||
("drop", true),
|
||||
("drop index", true),
|
||||
];
|
||||
for (input, advanced) in cases {
|
||||
let mut app = App::new();
|
||||
if *advanced {
|
||||
app.mode = Mode::Advanced;
|
||||
}
|
||||
type_str(&mut app, input);
|
||||
let actions = submit(&mut app);
|
||||
let mode = if *advanced { "ADV" } else { "SIM" };
|
||||
eprintln!("\n=== [{mode}] {input:?} → actions: {actions:?}");
|
||||
for l in &app.output {
|
||||
eprintln!(" [{:?}] {}", l.kind, l.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The simple-mode near-miss matrix (ADR-0042 §1). Each row is a
|
||||
/// near-correct input plus substrings that MUST appear across its
|
||||
/// rendered error lines — the structural "name the missing
|
||||
@@ -203,6 +124,138 @@ fn near_miss_matrix_simple_mode() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: advanced-mode error lines for `input`.
|
||||
fn advanced_error_lines_for(input: &str) -> Vec<String> {
|
||||
let mut app = App::new();
|
||||
app.mode = Mode::Advanced;
|
||||
type_str(&mut app, input);
|
||||
let _ = submit(&mut app);
|
||||
app.output
|
||||
.iter()
|
||||
.filter(|l| l.kind == OutputKind::Error)
|
||||
.map(|l| l.text.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_bare_select_collapses_projection_first_set() {
|
||||
// ADR-0042 G2: bare `select` dumped the full 14-item
|
||||
// expression first-set ("`not`, `-`, …, `case`, column name,
|
||||
// `distinct`, or `all`"). Collapse it to a learner-sized
|
||||
// projection gloss in the error MESSAGE only — completion
|
||||
// still expands the raw set (locked by the typing-surface
|
||||
// matrix).
|
||||
let lines = advanced_error_lines_for("select");
|
||||
let joined = lines.join("\n");
|
||||
let dump_msg = dump("select", &lines);
|
||||
assert!(
|
||||
joined.contains("a projection: `*`, a column, or an expression"),
|
||||
"bare `select` should collapse to the projection gloss\n{dump_msg}",
|
||||
);
|
||||
let err_line = lines
|
||||
.iter()
|
||||
.find(|l| l.starts_with("parse error"))
|
||||
.expect("parse error line");
|
||||
assert!(
|
||||
!err_line.contains("`exists`") && !err_line.contains("`case`"),
|
||||
"projection gloss should replace the raw expression first-set\n{dump_msg}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_mode_usage_block_shows_sql_templates_not_dsl() {
|
||||
// ADR-0042 G3: `render_usage_block` was mode-blind — it
|
||||
// resolved shared entry words to the first-registered (Simple)
|
||||
// node, so advanced-mode `create` showed the DSL `create table
|
||||
// … with pk …` template, which is not valid SQL. Mode-aware
|
||||
// selection now shows the SQL forms.
|
||||
let lines = advanced_error_lines_for("create");
|
||||
let joined = lines.join("\n");
|
||||
let dump_msg = dump("create", &lines);
|
||||
assert!(
|
||||
joined.contains("create table [if not exists]"),
|
||||
"advanced `create` should show the SQL create-table usage\n{dump_msg}",
|
||||
);
|
||||
assert!(
|
||||
joined.contains("create [unique] index"),
|
||||
"advanced `create` should show the SQL create-index usage\n{dump_msg}",
|
||||
);
|
||||
assert!(
|
||||
!joined.contains("with pk"),
|
||||
"advanced `create` must NOT show the DSL `with pk` template\n{dump_msg}",
|
||||
);
|
||||
}
|
||||
|
||||
/// The advanced-mode near-miss matrix (ADR-0042 §1/§3). Mirrors
|
||||
/// the simple-mode matrix for the SQL surface. Every row must show
|
||||
/// a per-command `usage:` block (never the available-commands
|
||||
/// fallback — that is for unconsumed entry words only).
|
||||
#[test]
|
||||
fn near_miss_matrix_advanced_mode() {
|
||||
let matrix: &[(&str, &[&str])] = &[
|
||||
// SQL select / with (G2, G4)
|
||||
("select", &["expected a projection: `*`, a column, or an expression", "select (* |"]),
|
||||
("select * from", &["after `select * from`, expected table name", "select (* |"]),
|
||||
("with", &["after `with`, expected identifier or `recursive`", "with [recursive]", "as ("]),
|
||||
// create / drop / alter — SQL templates (G3)
|
||||
("create", &["after `create`, expected `table`", "create table [if not exists]", "create [unique] index"]),
|
||||
("create table", &["after `create table`, expected identifier or `if`", "create table [if not exists]"]),
|
||||
("create index", &["after `create index`, expected `on`", "create [unique] index"]),
|
||||
("drop", &["after `drop`, expected `table`", "drop table [if exists]"]),
|
||||
("alter", &["after `alter`, expected `table`", "alter table <Table> add column"]),
|
||||
("alter table T", &["expected `add`, `drop`, `rename`, or `alter`", "alter table <Table>"]),
|
||||
// shared insert/update/delete — must show usage, not the
|
||||
// available-commands fallback (regression guard for the
|
||||
// empty-usage_ids SQL nodes).
|
||||
("insert into T", &["after `insert into T`, expected `values`, `with`, `select`, or `(`", "insert into <Table>"]),
|
||||
("update T", &["after `update T`, expected `set`", "update <Table> set"]),
|
||||
("delete from", &["after `delete from`, expected table name", "delete from <Table>"]),
|
||||
];
|
||||
for (input, needles) in matrix {
|
||||
let lines = advanced_error_lines_for(input);
|
||||
let dump_msg = dump(input, &lines);
|
||||
assert!(
|
||||
lines.iter().any(|l| l.starts_with("parse error")),
|
||||
"missing `parse error` line for {input:?}\n{dump_msg}",
|
||||
);
|
||||
// A consumed entry word must yield a usage block, never the
|
||||
// available-commands fallback.
|
||||
assert!(
|
||||
!lines.iter().any(|l| l.starts_with("available commands:")),
|
||||
"advanced {input:?} fell back to available-commands instead of a usage block\n{dump_msg}",
|
||||
);
|
||||
let joined = lines.join("\n");
|
||||
for needle in *needles {
|
||||
assert!(
|
||||
joined.contains(needle),
|
||||
"advanced near-miss {input:?} missing expected substring {needle:?}\n{dump_msg}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_alone_renders_cte_usage_not_select() {
|
||||
// ADR-0042 G4: `with` (advanced-only CTE entry word) borrowed
|
||||
// the `select` usage template, which never mentions the CTE
|
||||
// shape. It now carries its own `parse.usage.with`.
|
||||
let mut app = App::new();
|
||||
app.mode = Mode::Advanced;
|
||||
type_str(&mut app, "with");
|
||||
let _ = submit(&mut app);
|
||||
let lines: Vec<String> = app
|
||||
.output
|
||||
.iter()
|
||||
.filter(|l| l.kind == OutputKind::Error)
|
||||
.map(|l| l.text.clone())
|
||||
.collect();
|
||||
let dump_msg = dump("with", &lines);
|
||||
assert!(
|
||||
lines.iter().any(|l| l.trim_start().starts_with("with ") && l.contains("as (")),
|
||||
"missing CTE-specific `with … as (…)` usage template\n{dump_msg}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_alone_renders_create_table_usage() {
|
||||
let lines = error_lines_for("create");
|
||||
|
||||
Reference in New Issue
Block a user