diff --git a/src/dsl/parser.rs b/src/dsl/parser.rs index b95a699..d9a99ea 100644 --- a/src/dsl/parser.rs +++ b/src/dsl/parser.rs @@ -263,6 +263,16 @@ fn format_expectation(e: &crate::dsl::walker::outcome::Expectation) -> String { use crate::dsl::grammar::IdentSource; use crate::dsl::walker::outcome::Expectation; match e { + // ADR-0042 G1: the bare `1` that opens `add 1:n + // relationship …` is the project's only `Literal("1")` + // (grammar `ddl.rs`); on its own in an expected-set it is + // cryptic — a learner cannot know it begins a + // relationship. Render it as the named construct in error + // wording. This is render-only: completion/hints read the + // raw `Expectation::Literal("1")` directly (offering the + // literal `1` to type), so the candidate surface is + // unchanged. + Expectation::Literal("1") => "`1:n relationship`".to_string(), Expectation::Word(w) | Expectation::Literal(w) => format!("`{w}`"), Expectation::Ident { source, .. } => match source { // Match `IdentSlot::expected_label` outputs so the diff --git a/tests/it/parse_error_pedagogy.rs b/tests/it/parse_error_pedagogy.rs index d93f01e..a3350db 100644 --- a/tests/it/parse_error_pedagogy.rs +++ b/tests/it/parse_error_pedagogy.rs @@ -13,6 +13,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use rdbms_playground::action::Action; use rdbms_playground::app::{App, OutputKind}; use rdbms_playground::event::AppEvent; +use rdbms_playground::mode::Mode; const fn key(code: KeyCode) -> AppEvent { AppEvent::Key(KeyEvent { @@ -59,6 +60,149 @@ 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 +/// keyword/clause" message and the per-command usage template. +/// These are the cases triaged as already-good; the baseline dump +/// (`baseline_dump`, #[ignore]) shows the full rendering. Cases +/// flagged for wording fixes (bare `add` `1`, bare `select` +/// first-set, mode-blind usage) are deliberately NOT locked here +/// until their fixes land. +#[test] +fn near_miss_matrix_simple_mode() { + // (input, required-substrings-across-error-lines) + let matrix: &[(&str, &[&str])] = &[ + // app-lifecycle arg errors + ("quit now", &["after `quit`, expected end of input", " quit"]), + ("save sideways", &["after `save`, expected end of input", "save | save as"]), + ("mode sideways", &["unknown mode 'sideways'", "mode simple | mode advanced"]), + ("messages louder", &["unknown messages mode 'louder'", "messages short"]), + ("copy everything", &["unknown copy target 'everything'", "copy all"]), + // DDL bare + missing-slot + ("create", &["after `create`, expected `table`", "create table with pk"]), + ("create table", &["after `create table`, expected identifier", "create table with pk"]), + ("create table T", &["with pk", "create table with pk"]), + // G1: relationship cardinality reads as the named construct. + ("add", &["after `add`, expected `column`, `1:n relationship`", "add 1:n relationship"]), + ("drop table", &["after `drop table`, expected table name", "drop table "]), + ("add column", &["after `add column`, expected table name", "add column [to] [table]"]), + ("rename", &["after `rename`, expected `column`", "rename column [in] [table]"]), + ("rename column", &["after `rename column`, expected table name", "rename column [in] [table]"]), + ("change", &["after `change`, expected `column`", "change column [in] [table]"]), + ("change column", &["after `change column`, expected table name", "change column [in] [table]"]), + // data bare + missing-clause + ("insert", &["after `insert`, expected `into`", "insert into "]), + ("insert into", &["after `insert into`, expected table name", "insert into
"]), + ("insert into T", &["after `insert into T`, expected `values` or `(`", "insert into
"]), + ("update", &["after `update`, expected table name", "update
set"]), + ("update T", &["after `update T`, expected `set`", "update
set"]), + ("update T set x=1", &["expected `where` or `--all-rows`", "update
set"]), + ("delete", &["after `delete`, expected `from`", "delete from
"]), + ("delete from", &["after `delete from`, expected table name", "delete from
"]), + ("delete from T", &["expected `where` or `--all-rows`", "delete from
"]), + ("replay", &["after `replay`, expected string literal or path", "replay "]), + ("explain", &["after `explain`, expected `show`, `update`, or `delete`", "explain show data"]), + // advanced-only entry word typed in simple mode → "this is SQL" rail + ("select * from T", &["`select` is SQL", "mode advanced"]), + ("alter table T add column c int", &["`alter` is SQL", "mode advanced"]), + ]; + for (input, needles) in matrix { + let lines = 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}", + ); + let joined = lines.join("\n"); + for needle in *needles { + assert!( + joined.contains(needle), + "near-miss {input:?} missing expected substring {needle:?}\n{dump_msg}", + ); + } + } +} + #[test] fn create_alone_renders_create_table_usage() { let lines = error_lines_for("create"); @@ -81,15 +225,18 @@ fn create_alone_renders_create_table_usage() { fn add_alone_renders_both_add_family_usages() { let lines = error_lines_for("add"); let dump_msg = dump("add", &lines); - // Aggregation across `choice` (ADR-0020): the structural - // error line lists both add-family entries. + // Aggregation across the top-level choice: the structural + // error line lists every add-family branch. ADR-0042 G1: the + // relationship branch renders as the friendly `1:n + // relationship` rather than the cryptic bare `1` cardinality + // literal. assert!( lines.iter().any(|l| { l.starts_with("parse error") - && l.contains("`1`") + && l.contains("`1:n relationship`") && l.contains("`column`") }), - "expected aggregated `1` or `column` in structural error\n{dump_msg}", + "expected aggregated `1:n relationship` and `column` in structural error\n{dump_msg}", ); // Usage block (ADR-0021): both add-* templates surface. assert!(