fix: H1a G3 advanced usage shows all valid forms; complete near-miss matrix (ADR-0042)

The /runda DA pass found G3 over-corrected: advanced-mode `create`/`drop`
showed SQL forms only, hiding the DSL fallback forms that are valid input
in advanced mode (verified: `create table Foo with pk`, `drop column …`
parse and dispatch). Per the user decision, the advanced usage block now
shows every form valid in the mode, SQL-primary first, then the DSL
fallback forms — a usage hint must never hide working input. Simple mode
unchanged (DSL forms only).

Matrix completion (closing the residual coverage tail):
- arg-less app commands (help/rebuild/new/load/undo/redo/export/import)
  audited + locked — all reject trailing junk with "expected end of
  input" + usage.
- committed multi-forms (add index/constraint/1:n relationship, drop
  index/constraint/relationship, show table, change column, create index,
  alter table add/drop) audited + locked in
  near_miss_matrix_committed_multiforms — each renders its own
  form-specific missing-keyword message + usage.

Also from the DA pass:
- G2 distinct+all detector empirically verified unique to projection
  start (no misfire at count( / union / union all / select distinct).
- stale `chumsky` comment removed (app.rs import handler).
- ADR-0042 Implementation-outcome section records G1–G4, the
  user-confirmed G3 decision, and the now-complete matrix coverage.

Full suite green (lib 1578 / it 387 / typing_surface_matrix 192); clippy clean.
This commit is contained in:
claude@clouddev1
2026-06-05 18:46:57 +00:00
parent 649fdcb38e
commit 1d4923b15b
4 changed files with 178 additions and 39 deletions
+5 -5
View File
@@ -1338,11 +1338,11 @@ impl App {
},
),
AppCommand::Import { path, target } => {
// The path-bearing import goes through the
// pre-chumsky source-slice (parser.rs), which
// already validated non-empty path. Bare
// `import` returns from chumsky with an empty
// path string — surface the usage error.
// A path-bearing import carries a non-empty path
// from the walker. Bare `import` parses with an
// empty path string — surface the usage hint here
// at dispatch (not a parse error; ADR-0024 replaced
// the old chumsky source-slice path).
if path.is_empty() {
self.note_error(crate::t!("project.import_usage"));
return Vec::new();
+35 -19
View File
@@ -566,11 +566,6 @@ pub fn usage_keys_for_input_in_mode(
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 {
@@ -582,23 +577,44 @@ pub fn usage_keys_for_input_in_mode(
}
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);
}
// Advanced mode: every candidate form is reachable — the SQL
// nodes are primary, and the DSL nodes remain valid via fallback
// (verified: `create table … with pk` and `drop column …` both
// run in advanced mode). Show them all, mode-primary (Advanced)
// first, so the usage hint never hides input that works. Simple
// mode: only the DSL forms — the SQL-only forms hit the "this is
// SQL" rail and are not reachable. (ADR-0042 G3.)
let selected: Vec<(usize, &'static CommandNode, CommandCategory)> =
if mode == crate::mode::Mode::Advanced {
let mut v: Vec<_> = candidates
.iter()
.copied()
.filter(|(_, _, c)| *c == CommandCategory::Advanced)
.collect();
v.extend(
candidates
.iter()
.copied()
.filter(|(_, _, c)| *c != CommandCategory::Advanced),
);
v
} else {
candidates
.iter()
.copied()
.filter(|(_, _, c)| *c == CommandCategory::Simple)
.collect()
};
// Degenerate guard: an advanced-only word in simple mode (not
// normally reachable — it hits the SQL rail first) leaves
// `selected` empty; fall back to all candidates so a usage block
// still renders rather than the available-commands fallback.
let pick = if selected.is_empty() { candidates } else { selected };
let keys = union(&pick);
if keys.is_empty() {
return None;
}
let entry = candidates[0].1.entry.primary;
let entry = pick[0].1.entry.primary;
Some((entry, keys))
}