fix(hint): correct H2 corpus errors + add parse guard (handoff-71)

Semantic verification pass over the tier-3 `hint` corpus (ADR-0053).
Four content errors corrected in src/friendly/strings/en-US.yaml:

- cmd.create_table: the example `with pk id(serial), name(text),
  email(text)` declares a 3-column COMPOUND primary key, not a PK
  plus regular columns (every `with pk` column is a key member,
  ADR-0005). Rewritten to a single-column PK + `add column` for the
  rest; what/concept aligned.
- cmd.save: `save as my-shop` does not parse — `save as` takes no
  inline name, it opens a path-entry prompt. Example -> `save as`;
  what no longer implies inline naming; added a temp-vs-named concept.
- cmd.import: target `shop-copy` does not parse — the `as <target>`
  slot is a NewName ident that rejects hyphens. -> `shop_copy`.
- err.foreign_key.child_side: dropped the bogus `on delete set
  null/cascade` remedy — that governs the parent direction; a
  child-side violation is fixed by inserting the parent first
  (matches the tier-1 hint).

Adds every_cmd_hint_example_parses_in_its_mode — a catalog-driven
guard that parses every hint.cmd.* example in its taught mode,
backstopping syntactic drift (it caught the save and import errors).
Registers the new hint.cmd.save.concept key.

docs: drop two stale "deferred" entries from CLAUDE.md — project
storage (export/import, --resume, input history, migration scaffold)
and m:n convenience (C4) are all implemented (ADR-0015/0045); record
the verification pass on requirements.md H2.
This commit is contained in:
claude@clouddev1
2026-06-15 18:59:38 +00:00
parent b4441507e2
commit 5a37437055
5 changed files with 87 additions and 19 deletions
+3 -11
View File
@@ -37,9 +37,9 @@ Current decisions at a glance (each backed by an ADR):
simple to advanced (ADR-0003). No other sigils. simple to advanced (ADR-0003). No other sigils.
- **Project format:** `project.yaml` + `data/<table>.csv` + - **Project format:** `project.yaml` + `data/<table>.csv` +
`history.log`; `playground.db` is a derived artifact (ADR-0004, `history.log`; `playground.db` is a derived artifact (ADR-0004,
amended by ADR-0015). Implemented through Iteration 4 + amended by ADR-0015). Fully implemented (ADR-0015 Iterations
cleanup; export/import (Iter 5) and migration framework / 16): export/import, `--resume`, persistent input history, and
--resume / persistent input history (Iter 6) pending. the migration framework scaffold are all done.
- **Project storage runtime:** every command persists through to - **Project storage runtime:** every command persists through to
db + yaml + csv + history.log in one execution context, gated db + yaml + csv + history.log in one execution context, gated
by the combined db persistence logic; commit-db-last ordering by the combined db persistence logic; commit-db-last ordering
@@ -335,16 +335,8 @@ all of `target/`, forcing a full from-scratch rebuild).
These are explicitly tracked (mostly in `requirements.md`) but These are explicitly tracked (mostly in `requirements.md`) but
not yet implemented: not yet implemented:
- **Project storage** (track 2): largely implemented through
Iteration 4 + cleanup pass + safety hardening (Iterations
14 of ADR-0015). Pending pieces: `export` / `import` (Iter
5), `--resume` + persistent input history hydration +
migration framework scaffold (Iter 6).
- **Modify relationship** (C3a): drop+add covers the use case - **Modify relationship** (C3a): drop+add covers the use case
today. today.
- **m:n convenience** (C4): auto-generates a junction table
with appropriate FKs — depends on relationships being solid
(they are).
- **Strong syntax-help in parse errors** (H1a): point users at - **Strong syntax-help in parse errors** (H1a): point users at
missing keywords/clauses rather than the unexpected missing keywords/clauses rather than the unexpected
character. *(H1 — the friendly **database**-error layer — is character. *(H1 — the friendly **database**-error layer — is
+6 -1
View File
@@ -820,7 +820,12 @@ since ADR-0027.)
(`what`/`example`/`concept`) covers every command form + the 9 runtime (`what`/`example`/`concept`) covers every command form + the 9 runtime
error classes, enforced by a comprehensiveness coverage test. Deferred: error classes, enforced by a comprehensiveness coverage test. Deferred:
the pre-submit-diagnostic route + `diagnostic.*` blocks (#38), the pre-submit-diagnostic route + `diagnostic.*` blocks (#38),
clause-concept hints (#37).)* clause-concept hints (#37). **Content verified 2026-06-15 (handoff-71):**
a semantic pass over every `hint.cmd.*`/`hint.err.*` block fixed four
errors — `create_table` (compound-PK misread), `save` (no inline name),
`import` (hyphen-rejecting target), and `foreign_key.child_side` (wrong
`on delete` remedy) — and added a catalogue-driven guard test that parses
every command example in its taught mode.)*
- [x] **H3** `help` provides general reference and per-command - [x] **H3** `help` provides general reference and per-command
help. help.
*(Done 2026-06-07: the **general reference** is `help` (no arg) — *(Done 2026-06-07: the **general reference** is `help` (no arg) —
+69
View File
@@ -1012,6 +1012,75 @@ mod hint_key_tests {
assert!(cat.get(&key).is_some(), "missing tier-3 error block `{key}`"); assert!(cat.get(&key).is_some(), "missing tier-3 error block `{key}`");
} }
} }
/// Semantic-verification guard (handoff-71): every `hint.cmd.<form>`
/// **example** must parse in the mode the form is taught for. This
/// backstops the bug class found in the H2 corpus pass — an example
/// that drifts out of the real grammar (a typo, a removed clause, or
/// an argument the command never accepted, e.g. an inline name on
/// `save as` which opens a modal instead). It cannot police the
/// *semantics* of an example that happens to parse (that is the
/// manual pass), but it locks the syntactic floor so future edits
/// can't ship an unparseable teaching line.
///
/// The mode per form mirrors `hint_key_for_input_in_mode`: the
/// advanced-SQL forms are taught in advanced mode; everything else
/// (DSL + app commands) in simple mode.
#[test]
fn every_cmd_hint_example_parses_in_its_mode() {
use crate::dsl::parser::parse_command_in_mode;
use crate::mode::Mode;
// Advanced-mode forms — the SQL surface (ADR-00300039). Every
// other form (DSL + app commands) is taught in simple mode. This
// mirrors the mode split `hint_key_for_input_in_mode` resolves.
const ADVANCED: &[&str] = &[
"sql_create_table",
"sql_alter_table",
"sql_create_index",
"sql_drop_index",
"sql_drop_table",
"sql_insert",
"sql_update",
"sql_delete",
"select",
"with",
"explain_sql",
];
// Iterate the *catalog* (the corpus is the source of truth), not the
// REGISTRY: this reaches every `hint.cmd.<id>` block including any
// not owned by a command node, so an orphaned or mis-keyed example
// can't slip past the guard.
let cat = crate::friendly::catalog();
let mut checked = 0usize;
for key in cat.keys() {
let Some(id) = key
.strip_prefix("hint.cmd.")
.and_then(|rest| rest.strip_suffix(".example"))
else {
continue;
};
let example = cat.get(key).expect("key came from the catalog");
let mode = if ADVANCED.contains(&id) {
Mode::Advanced
} else {
Mode::Simple
};
assert!(
parse_command_in_mode(example, mode).is_ok(),
"hint.cmd.{id}.example does not parse in {mode:?} mode: {example:?}",
);
checked += 1;
}
// Floor guard: the corpus had 49 command forms at the time of
// writing (ADR-0053). If this drops, a block (and its example
// coverage) silently vanished.
assert!(
checked >= 49,
"expected at least 49 hint.cmd.* examples, checked {checked}",
);
}
} }
#[cfg(test)] #[cfg(test)]
+1
View File
@@ -277,6 +277,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("hint.cmd.rebuild.concept", &[]), ("hint.cmd.rebuild.concept", &[]),
("hint.cmd.save.what", &[]), ("hint.cmd.save.what", &[]),
("hint.cmd.save.example", &[]), ("hint.cmd.save.example", &[]),
("hint.cmd.save.concept", &[]),
("hint.cmd.new.what", &[]), ("hint.cmd.new.what", &[]),
("hint.cmd.new.example", &[]), ("hint.cmd.new.example", &[]),
("hint.cmd.load.what", &[]), ("hint.cmd.load.what", &[]),
+8 -7
View File
@@ -430,8 +430,9 @@ hint:
example: "rebuild" example: "rebuild"
concept: "The text files (project.yaml + the data folder) are the source of truth; the database is derived and can always be rebuilt from them." concept: "The text files (project.yaml + the data folder) are the source of truth; the database is derived and can always be rebuilt from them."
save: save:
what: "Save the current project under a name; `save as` copies it to a new one." what: "Save the current project; `save as` copies it to a new name or location."
example: "save as my-shop" example: "save as"
concept: "On a temporary project, `save` opens a prompt to give it a permanent name; a named project auto-saves as you work, so `save` on one is already done. `save as` always prompts for a new name or path — use it to copy a project."
new: new:
what: "Close the current project and start a fresh temporary one." what: "Close the current project and start a fresh temporary one."
example: "new" example: "new"
@@ -444,7 +445,7 @@ hint:
concept: "The zip carries the schema and data as text, so anyone can rebuild the very same database from it." concept: "The zip carries the schema and data as text, so anyone can rebuild the very same database from it."
import: import:
what: "Unpack a project zip into a new project and switch to it." what: "Unpack a project zip into a new project and switch to it."
example: "import my-shop.zip as shop-copy" example: "import my-shop.zip as shop_copy"
mode: mode:
what: "Switch between simple mode (the guided teaching commands) and advanced mode (raw SQL)." what: "Switch between simple mode (the guided teaching commands) and advanced mode (raw SQL)."
example: "mode advanced" example: "mode advanced"
@@ -465,9 +466,9 @@ hint:
example: "copy last" example: "copy last"
# DDL — schema-shaping commands (Phase C batch 2). # DDL — schema-shaping commands (Phase C batch 2).
create_table: create_table:
what: "Create a new table — its columns, their types, and a primary key." what: "Create a new table and declare its primary key."
example: "create table Customers with pk id(serial), name(text), email(text)" example: "create table Customers with pk id(serial)"
concept: "A table is a set of rows that share the same columns. The primary key uniquely identifies each row; a `serial` key numbers the rows for you." concept: "A table is a set of rows sharing the same columns. `with pk` declares the primary key — one column, or several for a compound key; add the other columns afterwards with `add column`. A `serial` key numbers the rows for you."
create_m2n: create_m2n:
what: "Create a junction table linking two tables many-to-many." what: "Create a junction table linking two tables many-to-many."
example: "create m:n relationship from Students to Courses" example: "create m:n relationship from Students to Courses"
@@ -606,7 +607,7 @@ hint:
child_side: child_side:
what: "The value you gave for the child column doesn't match any parent row, so the foreign key has nothing to point at." what: "The value you gave for the child column doesn't match any parent row, so the foreign key has nothing to point at."
example: "First insert the parent (insert into Customers …), then the child that references it." example: "First insert the parent (insert into Customers …), then the child that references it."
concept: "A foreign key is a promise that every child points at a real parent, so the parent must exist first. To allow orphans on delete instead, set the relationship's `on delete` to `set null` or `cascade`." concept: "A foreign key is a promise that every child points at a real parent, so the parent must exist before a child can reference it. (`on delete` actions like `cascade` or `set null` govern the other direction — what happens to children when their parent is removed — not this one.)"
parent_side: parent_side:
what: "You're deleting or changing a row that other rows point at, which would orphan those children." what: "You're deleting or changing a row that other rows point at, which would orphan those children."
example: "Delete the child rows first, or set the relationship's `on delete` to `cascade` (remove them too) or `set null` (keep them, unlinked)." example: "Delete the child rows first, or set the relationship's `on delete` to `cascade` (remove them too) or `set null` (keep them, unlinked)."