diff --git a/CLAUDE.md b/CLAUDE.md index 483b89d..9895db8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,9 +37,9 @@ Current decisions at a glance (each backed by an ADR): simple to advanced (ADR-0003). No other sigils. - **Project format:** `project.yaml` + `data/.csv` + `history.log`; `playground.db` is a derived artifact (ADR-0004, - amended by ADR-0015). Implemented through Iteration 4 + - cleanup; export/import (Iter 5) and migration framework / - --resume / persistent input history (Iter 6) pending. + amended by ADR-0015). Fully implemented (ADR-0015 Iterations + 1–6): export/import, `--resume`, persistent input history, and + the migration framework scaffold are all done. - **Project storage runtime:** every command persists through to db + yaml + csv + history.log in one execution context, gated 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 not yet implemented: -- **Project storage** (track 2): largely implemented through - Iteration 4 + cleanup pass + safety hardening (Iterations - 1–4 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 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 missing keywords/clauses rather than the unexpected character. *(H1 — the friendly **database**-error layer — is diff --git a/docs/requirements.md b/docs/requirements.md index a30fc57..f578a1c 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -820,7 +820,12 @@ since ADR-0027.) (`what`/`example`/`concept`) covers every command form + the 9 runtime error classes, enforced by a comprehensiveness coverage test. Deferred: 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 help. *(Done 2026-06-07: the **general reference** is `help` (no arg) — diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index 3842073..337b039 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -1012,6 +1012,75 @@ mod hint_key_tests { assert!(cat.get(&key).is_some(), "missing tier-3 error block `{key}`"); } } + + /// Semantic-verification guard (handoff-71): every `hint.cmd.` + /// **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-0030–0039). 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.` 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)] diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index 08a5bc5..341ee54 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -277,6 +277,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("hint.cmd.rebuild.concept", &[]), ("hint.cmd.save.what", &[]), ("hint.cmd.save.example", &[]), + ("hint.cmd.save.concept", &[]), ("hint.cmd.new.what", &[]), ("hint.cmd.new.example", &[]), ("hint.cmd.load.what", &[]), diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 87b820e..50cc352 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -430,8 +430,9 @@ hint: 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." save: - what: "Save the current project under a name; `save as` copies it to a new one." - example: "save as my-shop" + what: "Save the current project; `save as` copies it to a new name or location." + 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: what: "Close the current project and start a fresh temporary one." 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." import: 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: what: "Switch between simple mode (the guided teaching commands) and advanced mode (raw SQL)." example: "mode advanced" @@ -465,9 +466,9 @@ hint: example: "copy last" # DDL — schema-shaping commands (Phase C batch 2). create_table: - what: "Create a new table — its columns, their types, and a primary key." - example: "create table Customers with pk id(serial), name(text), email(text)" - 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." + what: "Create a new table and declare its primary key." + example: "create table Customers with pk id(serial)" + 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: what: "Create a junction table linking two tables many-to-many." example: "create m:n relationship from Students to Courses" @@ -606,7 +607,7 @@ hint: 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." 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: 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)."