Commit Graph

22 Commits

Author SHA1 Message Date
claude@clouddev1 f1ff5970bf Hint: pedagogical Form-A pointer at Form B's first value slot
Handoff-12 §2.2: Form B `insert into T values (…)` silently skips
auto-generated columns from the value list, so a user who wants to
set a serial/shortid column explicitly could only discover Form A by
reading help. Now the hint at the first Form B value slot appends a
note naming the skipped column(s) and pointing at the explicit-column
form.

hint_resolution_at_input derives the skipped columns from the
post-walk WalkContext (Form B = no user_listed_columns + table has
serial/shortid columns) and reports them on HintResolution; the note
fires only at the first slot so it doesn't repeat at every comma.
ambient_hint composes it onto the per-column prose.
2026-05-15 21:30:03 +00:00
claude@clouddev1 50b78253d8 Remove dead parse.token.* catalog entries
The 5 structural-class entries (identifier/number/string_literal/flag/
end_of_input) and 3 lex-error entries (bad_flag/unknown_char/
unterminated_string) were unreachable: ADR-0024 Phase F made the
walker render keyword wording verbatim and the lex errors never
surface through today's walker. Handoff-12 §2.1 kept them as a
conservative call; removed now per user request. Re-adding is cheap
if a future need arises.
2026-05-15 21:29:36 +00:00
claude@clouddev1 0b15ce0306 Walker + parser: surface mid-typing after separators and Form C/A ambiguity
The typing-surface matrix exposed two bugs the existing 859-test suite
missed:

walk_repeated: when the separator consumed but the inner item failed
at EOF, the old path rolled the separator back and reported a definite
error at the rollback position (`insert into T (a, ` flashed red on
the `,` after each comma). Now propagates Incomplete with the inner's
expected set so the input renderer treats it as mid-typing.

build_insert Form C path: `insert into T (col)` walked to a complete
match but produced `values: []` because Form C's value collector drops
ident-shaped items. The user almost certainly meant Form A and just
hasn't typed `values (...)` yet. Reject with a ValidationError naming
the Form-A continuation; classify_input now reports IncompleteAtEof.

completion_probe / expected_at_input: ValidationFailed used to return
an empty expected set, leaving Tab with nothing to offer at the new
Form-A flag point. Now surface result.tail_expected (skipped-Optional
expectations captured before validation fired) so `values` is still
offered as a candidate.
2026-05-15 20:06:52 +00:00
claude@clouddev1 b3f1a20652 Phase D: insert value list mirrors do_insert's user_cols contract
Bug: hint at \`insert into Customers values (\` for a Customers
table with id:serial PK suggested typing an integer for \`id\`,
but the dispatch path (\`db::do_insert\`) deliberately doesn't
accept user-supplied values for auto-generated columns in
Form B. The grammar prompted for a value the dispatch would
refuse.

The fix aligns Phase D's \`column_value_list\` dynamic sub-grammar
with do_insert's three forms (ADR-0014 + ADR-0018 §3):

- **Form A** \`insert into <T> (col1, col2, …) values (…)\` —
  user explicitly lists columns. Slot list mirrors that
  selection; serial / shortid columns CAN appear if the user
  lists them.
- **Form B** \`insert into <T> values (…)\` — bare values. Slot
  list = non-auto-generated columns of the table in
  declaration order. Serial / shortid get auto-filled by the
  dispatch; the grammar doesn't prompt for them.
- **Form C** \`insert into <T> (v1, v2, …)\` — bare value list.
  Not affected by this change (column_value_list isn't on this
  path; Form C's literals route through the schemaless
  INSERT_PAREN_LIST).

Implementation:

\`WalkContext.user_listed_columns: Option<Vec<String>>\` — when
\`Some\`, signals Form A; \`None\` is Form B. Populated by walking
the first paren's column-list idents.

\`Node::Ident.writes_user_listed_column: bool\` — new field;
\`true\` on the INSERT_PAREN_ITEM's Ident child. When the
walker matches that ident in Form A, it appends the
schema-canonical column name (case-corrected against the
schema) to user_listed_columns.

\`column_value_list\` factory:
- If user_listed_columns is Some → resolve each name from the
  schema; one typed slot per listed column.
- Else → filter current_table_columns to non-auto-generated;
  one typed slot per remaining column.
- Empty result → fall back to the schemaless value-literal
  list (a serial-only table in Form B has nothing for the
  user to type).

Tests:
- New \`phase_d_insert_form_b_skips_serial_column\` confirms the
  bug: \`insert into Customers values (1, 'Alice')\` against a
  Customers with serial id rejects at parse time (Form B
  expects 1 value for Name, not 2).
- New \`phase_d_insert_form_a_accepts_serial_when_listed\`
  confirms \`insert into Customers (id, Name) values (1, 'Alice')\`
  works.
- New \`phase_d_insert_form_a_filters_to_user_listed_columns\`
  confirms partial Form A (\`(Name) values ('Alice')\`).
- Updated \`phase_d_insert_with_schema_accepts_typed_values_per_column\`
  to match the new Form B contract (2 user-typed values, not 3).
- Updated typed-hint test matrix split into form-B (8 types)
  and form-A (serial / shortid).
- New \`typed_hint_form_b_skips_serial_column_to_generic_or_text_neighbor\`
  pins the fallback behavior for a serial-only table.

For the user: \`insert into Customers values (\` for a Customers
with \`(id:serial, Name:text, Email:text)\` now hints
\`for \`Name\`: Type a quoted string …\` (skipping id entirely)
and accepts exactly 2 values. To set the serial explicitly,
use Form A: \`insert into Customers (id, Name, Email) values
(1, 'Alice', 'a@b.c')\`.

Tests: 851 passing, 0 failing, 1 ignored. Clippy clean.
2026-05-15 18:45:47 +00:00
claude@clouddev1 c485189da8 ADR-0024 Phase D: include column name in value-slot hint prose
User-facing improvement: typing into a value slot now surfaces
the column name in the hint. The hint at `insert into Customers
values (` (first column id:int) reads "for `id`: Type an
integer (e.g. 42, -7) or null" instead of the generic
"Type an integer …" prose. After `1, ` the panel updates to
the second column ("for `Name`: Type a quoted string …"). The
same applies to `update T set Email=` and `delete from T where
ts=` — the catalog wrapper threads the column name through.

Implementation:

**`Node::TypedValueSlot.column_name: Option<&'static str>`**
(new field, `src/dsl/grammar/mod.rs`). When `Some`, walker
writes `WalkContext::pending_value_column` on entry; clears
along with `pending_value_type` on inner success.

**Walker driver writes both names** (`src/dsl/walker/driver.rs`):
- `Node::TypedValueSlot` dispatch reads `column_name` and
  populates `pending_value_column`.
- `Ident { writes_column: true }` dispatch also writes
  `pending_value_column` (using the schema-canonical name when
  available, falling back to the user's spelling) so update
  set / where positions surface the column name.

**Shared sub-grammars** (`src/dsl/grammar/shared.rs`):
- New `slot_for_column(ty, name)` builds a `TypedValueSlot`
  with the embedded leaked column name. Used by
  `column_value_list`.
- New `slot_inner_for_type(ty)` returns just the Choice
  (without TypedValueSlot wrapper) for slot_for_column to
  rebuild.
- `column_value_list` factory now constructs per-column slots
  via `slot_for_column(col.user_type, &col.name)`. Each slot
  leaks its column name string with the same per-walk Box::leak
  pattern the rest of dynamic dispatch uses.

**`WalkContext::pending_value_column: Option<String>`** (new
field, `src/dsl/walker/context.rs`). Pairs with
`pending_value_type` to give the hint resolver both pieces.

**Single-walk hint resolver** (`src/dsl/walker/mod.rs`):
- New `HintResolution { mode: HintMode, column: Option<String> }`
  struct.
- New `hint_resolution_at_input(source, schema) -> Option<
  HintResolution>` runs one walk and reports both pieces. The
  ambient_hint dispatch composes per-column prose from the
  result.
- Existing `hint_mode_at_input` / `hint_mode_at_input_with_schema`
  preserved as thinner wrappers for tests / future callers
  that don't need the column name.

**Catalog wrapper** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- New `hint.value_slot_for_column: "for `{column}`: {detail}"`
  prefixes the per-type prose with the actual column name when
  the walker has it bound. Schemaless fallback continues to use
  the generic value-literal prose with no column prefix.

**ambient_hint composes** (`src/input_render.rs`): consults
`hint_resolution_at_input`; when `column` is `Some`, wraps the
type prose through `hint.value_slot_for_column`; otherwise
emits the bare type prose.

Tests (846 total, 0 failing):
- 4 new input_render tests assert column names appear in the
  prose at insert/update/where positions plus the
  second-insert-value position (proves column tracking advances
  with comma).
- All existing tests pass unchanged — the column-name addition
  is layered on top of the type-only prose path.

Clippy clean.
2026-05-15 18:33:52 +00:00
claude@clouddev1 82955679ca ADR-0024 Phase D: per-column-type hint prose at value slots
The Phase D commit landed parse-time validation but not the
user-facing payoff — per-column-type hints. Typing
`insert into Customers values (` rightfully expected a hint
like "Type an integer (e.g. 42, -7) or null" at an int column.
This commit closes that gap.

End-to-end:

**`Node::TypedValueSlot { ty, inner }`** (new variant in
`src/dsl/grammar/mod.rs`):
- Walker walks `inner` to consume the literal but tags
  `WalkContext::pending_value_type = Some(ty)` on entry, then
  clears it on a successful inner match. Positions BETWEEN
  slots (`insert into T values (1` mid-input) thus don't carry
  a stale hint type.

**Typed slot factories wrapped in `TypedValueSlot`**
(`src/dsl/grammar/shared.rs`):
- `INT_SLOT`, `REAL_SLOT`, `DECIMAL_SLOT`, `BOOL_SLOT`,
  `TEXT_SLOT`, `DATE_SLOT`, `DATETIME_SLOT`, `BLOB_SLOT`,
  `SERIAL_SLOT`, `SHORTID_SLOT` — each pairs an inner literal
  Choice with its `Type` so the walker can tag context.
- `slot_for_type(ty)` dispatches to the appropriate constant.
- Bug fix: `ShortId` previously dispatched to `INT_SLOT` (a
  pre-Phase-D holdover from the chumsky-side generic
  fallback). `shortid` columns store base58 text (ADR-0011
  fk_target_type shortid → text); the corrected slot accepts
  `StringLit` or `null`.

**Schema-aware hint resolver** (`src/dsl/walker/mod.rs`):
- `hint_mode_at_input_with_schema(source, &SchemaCache) ->
  Option<HintMode>` is the new public entry point. Reads
  `pending_value_type` from the walker's WalkContext and
  emits `HintMode::ProseOnly("hint.value_slot_<type>")` —
  one per Type.
- The schemaless `hint_mode_at_input(source)` falls back to
  the generic `hint.value_literal_slot` at value-literal slots
  (no per-type narrowing without a schema).
- `catalog_key_for_value_type(ty)` is the type → key
  dispatcher.

**Catalog entries** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- 10 new `hint.value_slot_<type>` keys with per-type prose:
  - int/serial → "Type an integer (e.g. 42, -7) or null"
  - real/decimal → "Type a number (e.g. 3.14, -0.5) or null"
  - bool → "Type true, false, or null"
  - text → "Type a quoted string (e.g. 'Alice') or null"
  - date → "Type a quoted date as 'YYYY-MM-DD' or null"
  - datetime → "Type a quoted datetime as 'YYYY-MM-DD
    HH:MM:SS' or null"
  - blob → "Type a quoted blob literal or null"
  - shortid → "Type a quoted shortid (or omit to auto-generate)
    or null"

**Ambient-hint dispatch** (`src/input_render.rs::ambient_hint`):
- Passes the SchemaCache through to
  `hint_mode_at_input_with_schema`, so the live hint panel
  surfaces per-column-type prose as the user types into a
  value slot.

Tests:
- 8 walker-side tests cover insert / update / where typed-slot
  hint dispatch, mid-value no-stale-hint behaviour, and a
  full-coverage routing matrix for every `Type` variant.
- 4 input_render integration tests cover the end-to-end
  ambient_hint path: insert first/second value, update set
  value, and the schemaless fallback to generic prose.

Tests: 842 passing, 0 failing, 1 ignored. Clippy clean.

For the user: typing `insert into Customers values (` against
a Customers table whose first column is `id:int` now shows
"Type an integer (e.g. 42, -7) or null" in the hint panel,
replacing the previous generic value-literal prose. After
typing `1, `, the panel updates to whatever the second column
requires — "Type a quoted string (e.g. 'Alice') or null"
for text, "Type a quoted date as 'YYYY-MM-DD'" for date, etc.
2026-05-15 18:05:38 +00:00
claude@clouddev1 abebd7944f ADR-0024 Phase D (full): schema-aware value typing
Schema-aware typed value slots — the central design claim of
ADR-0024 §Phase D. Insert / update / delete value slots now
dispatch on the user-facing column type at parse time, rejecting
mis-shaped input with localised wording instead of waiting for
the bind-time error.

What changed:

**SchemaCache extension** (`src/completion.rs`):
- New `TableColumn { name, user_type }` for per-table column
  metadata.
- `SchemaCache.table_columns: HashMap<String, Vec<TableColumn>>`.
- `SchemaCache::columns_for_table(name)` — case-insensitive
  lookup, mirrors the walker's case-insensitive entry-word
  resolution.

**WalkContext schema plumbing** (`src/dsl/walker/context.rs`):
- `WalkContext<'a>` gains a lifetime and a `schema: Option<&'a
  SchemaCache>`. `WalkContext::new()` keeps the schemaless
  default; `with_schema(s)` is the new schema-aware constructor.

**Parser entry point** (`src/dsl/parser.rs`):
- `parse_command_with_schema(input, schema)` is the new public
  schema-aware variant. `parse_command(input)` becomes a thin
  wrapper that delegates with `None` for back-compat.
- Internal `try_walker_route` accepts an `Option<&SchemaCache>`
  and threads it into the WalkContext.

**Node::Ident writes_table/writes_column** (`src/dsl/grammar/mod.rs`):
- Two new fields on `Node::Ident`. When `writes_table: true` and
  `source: Tables`, the walker writes the matched ident's name
  into `current_table` and resolves `current_table_columns`
  against the schema cache. When `writes_column: true` and
  `source: Columns`, the walker writes the resolved
  `TableColumn` into `current_column`.

**Walker driver DynamicSubgrammar dispatch** (`src/dsl/walker/driver.rs`):
- The `Node::DynamicSubgrammar(factory)` branch now resolves the
  factory at walk time and `Box::leak`s the result so its inner
  static-slice fields (Choice/Seq) have the lifetime the walker
  expects (per ADR-0024 §sub-grammars). The leak is bounded by
  command-shape complexity per walk; per-walk arena is a future
  optimisation.
- `walk_ident` extends to perform the schema writes when the
  flags are set.

**Typed value slot factories + dynamic sub-grammars** (`src/dsl/grammar/shared.rs`):
- `int_slot` / `real_slot` / `decimal_slot` / `bool_slot` /
  `text_slot` / `date_slot` / `datetime_slot` / `blob_slot` —
  one per `Type`. Each accepts the appropriate literal kind plus
  `null`; integer-only validator rejects `3.14` at int columns;
  decimal validator pins numeric shape.
- `slot_for_type(ty) -> Node` is the dispatcher.
- `current_column_value(ctx) -> Node` is the dynamic sub-grammar
  for `set col = …` and `where col = …` values; reads
  `current_column` and dispatches via `slot_for_type`.
- `column_value_list(ctx) -> Node` is the dynamic sub-grammar
  for `insert into T values (…)`; reads `current_table_columns`
  and unfolds a Seq of typed slots separated by commas.
- Both fall back to the schemaless `VALUE_LITERAL` choice when
  the context lacks the schema-resolved entries — keeps
  schemaless `parse_command` callers (tests, replay path)
  working.

**Data-command grammar wires the new types** (`src/dsl/grammar/data.rs`):
- `TABLE_NAME_INSERT` / `TABLE_NAME_WRITES` (new): table-name
  slots that set `writes_table: true`. Used by insert / update /
  delete to populate `current_table_columns`.
- `SET_COLUMN` / `FILTER_COLUMN` (new): column-name slots in
  `set col=…` / `where col=…` set `writes_column: true`.
- `INSERT_VALUES_LIST` becomes `DynamicSubgrammar(column_value_list)`.
- `UPDATE_ASSIGNMENT` and `WHERE_CLAUSE` use
  `PER_COLUMN_VALUE = DynamicSubgrammar(current_column_value)`.

**Runtime plumbs schema-with-types** (`src/runtime.rs`):
- `refresh_schema_cache` calls `describe_table` for each table
  and populates `SchemaCache::table_columns` with
  `TableColumn { name, user_type }` entries. Best-effort: a
  `describe_table` miss leaves that table unpopulated and the
  walker falls back to schemaless dispatch.

**App dispatches with schema** (`src/app.rs`):
- `dispatch_dsl` routes through `parse_command_with_schema(&self
  .schema_cache, …)` so live typing/dispatch sees the typed
  slots. The replay path stays schemaless (deferred — replay
  bind-time errors still catch type mismatches).

**Catalog** (`src/friendly/strings/en-US.yaml`, `src/friendly/keys.rs`):
- New `parse.custom.bind_type_mismatch` entry with `{found}` and
  `{expected}` placeholders. Surfaced by the int_slot /
  decimal_slot validators.

Tests:
- 11 new walker-side Phase D tests cover insert / update /
  delete with schemas — typed acceptance per column, decimal
  rejection at int columns, null acceptance at any slot,
  multi-assignment per-column dispatch, schemaless fallback.
- The pre-existing `parse_command(input)` test suite (no
  schema) still passes — the fallback path is behaviour-
  preserving.
- 828 passing total, 0 failing, 1 ignored. Clippy clean.
2026-05-15 17:45:56 +00:00
claude@clouddev1 fa994cfb66 ADR-0024 Phase F (full) step 4: catalog token-keyword cleanup
Drops the 47 `parse.token.keyword.*` and 6 `parse.token.punct.*`
catalog entries (and their `KEYS_AND_PLACEHOLDERS` declarations).
Nothing consumes them: the walker renders keyword wording in
`format!(\"`{word}`\")` directly, sourced from grammar-tree Word
literals; punct wording surfaces the same way via
`Expectation::Punct(ch)`.

Structural-class labels (`parse.token.identifier`,
`parse.token.number`, `parse.token.string_literal`,
`parse.token.flag`, `parse.token.end_of_input`) and the lex-error
wordings (`parse.token.error.{bad_flag,unknown_char,
unterminated_string}`) stay. These are not derivable from the
grammar tree and the walker's expected-set / validator paths still
read them.

`friendly::keys::tests::keys_validate_against_catalog` continues to
assert catalog ↔ `KEYS_AND_PLACEHOLDERS` bidirectional coverage,
so the trimmed declaration is pinned against the trimmed catalog.

Tests: 806 passing, 0 failing, 1 ignored. Clippy clean.
2026-05-15 08:35:59 +00:00
claude@clouddev1 266b4c2ef4 ADR-0024 Phase F (full) step 3: delete legacy parser modules
Removes the last consumers of `dsl::lexer`, `dsl::keyword`, and
`dsl::ident_slot`, then deletes the modules.

- `Theme::token_color(&TokenKind)` deleted along with its test;
  `Theme::highlight_class_color(HighlightClass)` is the sole
  highlight-colour mapper (the walker's `per_byte_class` feeds
  it directly).
- `IdentSource` (`dsl::grammar`) absorbs the schema-list /
  expected-label / round-trip semantics that previously lived
  on `IdentSlot`. Adds `completes_from_schema`, `expected_label`,
  and `from_expected_label` methods. The walker's
  `Expectation::Ident { source }` and the schema-lookup request
  on the database worker now share one enum.
- `SchemaCache::for_slot(IdentSlot)` → `for_source(IdentSource)`.
- `Database::list_names_for` and the `Request::ListNamesFor`
  worker variant take `IdentSource`. Internal tables and column
  / relationship lookups dispatch on the same enum.
- `InvalidIdent.slot: IdentSlot` → `InvalidIdent.source: IdentSource`.
  The `invalid_ident_at_cursor` rendering branch in
  `input_render.rs::ambient_hint` updates accordingly.
- Completion's keyword filter (`Keyword::from_word`) becomes
  "backticked items whose payload is all ASCII alphabetic" —
  punct and digit literals still surface through their own
  candidate sources (composite-literal, flag, schema-ident);
  the alphabetic filter excludes them from the keyword bucket.
- `friendly::keys::tests::keyword_and_punct_have_complete_token_vocabulary`
  is dropped. It cross-checked `Keyword::ALL` / `Punct::ALL`
  against catalog entries; both enums are gone. The
  `parse.token.keyword.*` / `parse.token.punct.*` catalog
  entries themselves survive for one more commit (catalog
  cleanup, ADR-0024 §cleanup-pass); the
  `keys_validate_against_catalog` test still pins them.
- Modules deleted: `src/dsl/lexer.rs`, `src/dsl/keyword.rs`,
  `src/dsl/ident_slot.rs`.

Tests: 806 passing, 0 failing, 1 ignored. The drop from 852
reflects the removed module-internal tests (~32 lexer, 7
keyword, 4 ident_slot, 1 theme token_color, 1 friendly keys
keyword/punct), and is the expected outcome.

Clippy clean with `nursery` lints + `-D warnings`.
2026-05-15 08:33:59 +00:00
claude@clouddev1 3b36bbb4d6 hint: replace misleading "null true false" suggestions at value slots
At value-literal slots (`insert into T values (`, `update T set
col=`, `where col=`, comma positions) the expected-token set
contains null/true/false/number/string-literal. The completion
engine was surfacing the three keyword candidates as Tab options
— actively misleading because the user is usually about to enter
a number, quoted text, or date, and seeing "null true false"
implies those are *the* options. User report (round-6 testing):
"especially not when I'm trying to insert a datetime value and
don't know the correct format for the literal".

Fix: detect the value-literal slot by its expected-set
fingerprint. Suppress Tab candidates at empty prefix. Surface a
prose hint listing all literal forms with format examples
('YYYY-MM-DD' for dates, 'YYYY-MM-DDTHH:MM:SS' for datetimes).
Once the user starts typing a prefix (n / tr / fa), normal
keyword completion still applies.

Schema-aware narrowing (show ONLY the datetime format at a
datetime column) waits on ADR-0023.

Tests: 769 -> 777 passing (+8). Clippy clean.
2026-05-14 20:40:19 +00:00
claude@clouddev1 a55b6a7a05 remove q quit alias
`q` was introduced in round-5 as a peer Keyword variant alongside
`quit`. Per ADR-0023's "alias miss" critique, that was the wrong
shape — it surfaced `q` as a standalone command in completion
(only one of its kind), and required parallel parser + usage +
catalog + test entries. Drops the Keyword variant entirely; if
this ever needs to come back, it should arrive as an alias
annotation per ADR-0023, not as a peer keyword.

Tests still 769 passing.
2026-05-13 22:36:42 +00:00
claude@clouddev1 6ca297579e round-5 follow-up r2: migrate all thiserror Display attributes to catalog
Completes the i18n sweep started in the previous commit. All
remaining hand-rolled user-facing English strings inside
thiserror #[error(...)] attributes have been moved into the
catalog. Drops the thiserror dependency entirely.

Twelve error types migrated:

- dsl::action::UnknownAction         → parse.custom.unknown_action
- dsl::parser::ParseError            → parse.error_wrapper + parse.empty
- dsl::value::ValueError             → value.{type_mismatch,format}
- persistence::csv_io::CsvError      → persistence.csv.*
- persistence::mod::PersistenceError → persistence.{io,encode}
- persistence::yaml::YamlError       → persistence.yaml.*
- persistence::migrations::MigrateError → persistence.migrate.*
- project::lock::LockError           → project.lock.*
- project::naming::NamingError       → project.naming.*
- project::naming::UserNameError     → project.user_name.*
- project::mod::ProjectError         → project.{path_not_found,...}
- project::mod::SafeDeleteError      → project.safe_delete.*
- archive::ArchiveError              → archive.*
- cli::ArgsError                     → cli.*
- db::DbError                        → db.error.*

Pattern per type: drop thiserror::Error derive, write manual
Display calling crate::t!(), keep #[from] semantics via
explicit From impls, override Error::source() where applicable
so #[source]-style chaining is preserved.

Why this matters (user rationale): "fine to have fallbacks for
errors that are purely technical, but lift the output to a
place where it can be localized later and where an adjustment
with friendly text is easily possible if any of them become
part of the happy path." All surface strings now live in
en-US.yaml and can be reworded or localized without touching
Rust source.

Tests: 769 passing, 0 failed, 1 ignored. Clippy clean with
-D warnings. Cargo.toml: drop thiserror = "2.0.18".
2026-05-13 21:24:51 +00:00
claude@clouddev1 1e06490572 round-5 follow-up: completion + i18n sweep
Four user-reported gaps from the round-4 testing pass:

1. Empty-prompt hint reworded from "(no active hint)" to
   "Type a command — press Tab for options, `help` for a
   list" (6 snapshots updated to reflect 80-col truncation).

2. App-lifecycle commands (quit/q, help, rebuild, save/save as,
   new, load, export, import, mode, messages) now flow through
   the DSL parser:
   - 15 new keywords + catalog token entries
   - new Command::App(AppCommand) AST with 11 variants
   - parse-first dispatch in submit() (app commands work in
     both simple and advanced modes)
   - pre-chumsky source-slice for `export <path>` /
     `import <zip> [as <target>]` mirrors the replay precedent
   - UsageEntry registry entries so parse errors surface
     relevant usage templates
   - `mode <bad>` / `messages <bad>` use try_map for the
     friendly "unknown mode/messages" wording

3. DSL completion gaps:
   - `1:n` surfaces as a composite candidate at `add `
   - --all-rows / --create-fk / --force-conversion /
     --dont-convert surface as new CandidateKind::Flag
     candidates (coloured with tok_flag in hint panel)
   - filter_clause .labelled() wrap removed so chumsky's
     expected-set surfaces the constituent options

4. Hardcoded user-facing strings migrated to catalog:
   - 4 parser custom errors (incl. the known "tables need at
     least one column" wart)
   - UnknownType Display now via parse.custom.unknown_type
   - UI panel titles + mode labels (Output / Hint / SIMPLE /
     ADVANCED / Advanced:)
   - app.rs cascade rendering (action labels + summary)
   - runtime --resume CLI stderr
   - db.rs change-column diagnostic tables (7 headers + 3
     wrapper summaries + force-conversion hint)

Tests: 765 → 769 passing, 0 failed, 1 ignored (same doctest
as before). Clippy clean with -D warnings.

Deferred:
- ~25 thiserror #[error] attributes still hand-rolled
  (DbError, ArgsError, ArchiveError, PersistenceError,
  LockError). Tracked separately.
- DSL/SQL relationship in advanced mode — clarified
  implicitly via parse-first dispatch; broader ADR
  amendment to follow.
- Post-complete-parse completion gap (e.g. `save ` Tab
  can't offer `as` because `save` parses bare; same shape
  as `--create-fk` after a complete `add relationship`).
2026-05-13 15:58:29 +00:00
claude@clouddev1 22119d6a4e ADR-0022 follow-up r3: identifier colour, NewName hint, "Next:" wording, "type" label
Three fixes from a third round of real testing.

1. **tok_identifier vivid (round-3 #1).** The cool grey-blue
   from r2 was still too close to theme.fg to register as
   distinct. Bumped to cyan-teal (#56B6C2 dark / #0F6B76
   light) — identifiers are the user's most "special" content
   and now read that way against keywords (purple), numbers
   (orange), strings (green), and flags (amber).

2. **"Type a name" hint at NewName slots (round-3 #2).**
   New `completion::typing_name_at_cursor(input, cursor)`
   returns `Some(TypingName)` when the cursor sits at — or
   inside — an `IdentSlot::NewName` position. It probes by
   substituting a single-letter placeholder identifier and
   re-parsing to discover what the parser would expect AFTER
   the name; the hint then reads "Type a name, then `(`"
   instead of the technical "next: `(`" that surfaces once
   the partial identifier has been consumed by the live
   parser. When the probe yields nothing useful (custom
   errors with empty expected, or a complete-on-substitute
   case), falls back to "Type a name".

   New catalog keys hint.ambient_typing_name and
   hint.ambient_typing_name_then. Wired into ambient_hint
   between the candidate-list and invalid-ident checks.

3. **"Next:" instead of "expected:" wording.** "Expected"
   read as a leaked diagnostic; "Next:" is shorter,
   conversational, and consistent with the action-oriented
   voice of "Submit with Enter" and "Type a name". Hint
   sentences now also start capitalised
   (Submit/Next/Type/No-such), per the user's Capital-T-on-
   "type a name" preference.

4. **type_keyword labelled "type".** Without a label, the
   `select_ref!` over an Identifier token produced
   `RichPattern::SomethingElse`, which rendered as the
   meaningless "something else" in the hint after `(`.
   Labelled now: error reads "Next: type" — terse but
   honest. The label is applied BEFORE try_map (not after,
   not via as_context) so the existing custom-error wording
   for unknown types ("unknown type 'varchar' (expected one
   of: …)") still surfaces unchanged.

Tests: 755 passing, 0 failing, 1 ignored (no net change —
+5 typing_name cases, -0 net since one test was reworded
for capitalisation rather than added). Clippy clean.

Smoke probe verifies: "add column to table T: " → "Type a
name, then `(`"; "add column to table T: Name (" → "Next:
type"; "show data Custp" → "No such table: `Custp`"; valid
input → "Submit with Enter".

Note for next testing round: parser-side custom errors
(e.g. the "tables need at least one column" message that
fires for `create table Customers `) still read in
lowercase — they're hand-written in parser.rs source rather
than via the catalog. If the lowercase "tables need…"
intrusion bothers you, easy follow-up.
2026-05-11 22:41:23 +00:00
claude@clouddev1 8214e4136a ADR-0022 stage 8e: invalid-identifier detection + hint variant
Per the user's #5: "if our candidate selection works
correctly, then entering a character that removes all matches
is the same as entering an invalid token." Closes the loop
between schema cache (8c/8d) and live error feedback (4).

New `completion::invalid_ident_at_cursor(input, cursor, cache)`
returns `Some(InvalidIdent { range, found, slot })` when:
  - the cursor is on a partial identifier-shaped token;
  - the parser's expected-set at the start of that token
    contains a known-set IdentSlot (TableName / Column /
    RelationshipName);
  - no schema entry across those slots prefix-matches the
    typed text.

`render_input_runs` extended to take a `&SchemaCache` and
overlay the invalid-identifier range with `tok_error` —
same visual treatment as the parse-error overlay (4),
unified red signal regardless of which detector fires.

`ambient_hint` extended to surface `hint.ambient_invalid_ident`
when invalid_ident_at_cursor returns Some — wording
"no such {kind}: `{found}`" mirrors ADR-0019's engine-error
voice for consistency. Catalog + KEYS_AND_PLACEHOLDERS
declaration added; validator passes.

Render priority: candidates win over invalid-ident
(if any schema match exists for the partial prefix, the
state is "in-progress completion" not "invalid"). Falls
through to the existing parse-error/incomplete/Valid
framings otherwise.

NewName slots are filtered out at the source — typing
into a "user invents this name" position is never invalid
(per `IdentSlot::completes_from_schema`).

Tests: 744 passing, 0 failing, 1 ignored (738 baseline →
+6: 5 invalid_ident_at_cursor cases covering
unknown-prefix-fires, prefix-match-doesn't-fire,
NewName-immune, no-cursor-token, keyword-slot-immune;
plus 1 ambient_hint integration test). Clippy clean.

This closes ADR-0022. Stages 1-8e together deliver the
ambient-typing-assistance feature: token highlighting,
error overlay, hint panel ambient, hint panel multi-
candidate display with scroll markers, Tab/Shift-Tab
cycling with one-keystroke Esc/Backspace undo, schema-aware
identifier completion, and invalid-identifier live
feedback. Total stage-8 footprint: 5 commits, ~1600 lines.
2026-05-11 21:01:44 +00:00
claude@clouddev1 9c4857eb50 ADR-0022 stage 5/8: hint panel ambient typing assistance
ParseError::Invalid gains an `expected: Vec<String>` field —
the human-rendered names of the patterns chumsky was looking
for at the failure point (`\`create\``, `identifier`, etc.).
Empty for custom errors, which have no expected-set framing.
Populated by a new `describe_expected()` helper in parser.rs
that humanise() also delegates to (eliminates duplication).

`input_render::ambient_hint(input) -> Option<String>` returns
the hint-panel content per ADR-0022 §6:
  - empty input → None (caller falls back to panel.hint_empty);
  - Valid → t!("hint.ambient_complete") ("submit with Enter");
  - IncompleteAtEof → t!("hint.ambient_expected", expected = …)
    listing the parser's expected next tokens, oxford-joined;
  - DefiniteErrorAt → t!("hint.ambient_error_with_usage", …)
    composing the parse-error message with the matching
    parse.usage.* template if a known entry keyword was
    consumed, else the bare message.

Catalog gains the three hint.ambient_* keys + validator
declarations.

ui::render_hint_panel resolution order:
  1. explicit app.hint (modal contexts) wins;
  2. simple-mode + non-empty input → ambient_hint;
  3. fallback to panel.hint_empty.
Advanced mode (persistent + one-shot `:`) bypasses ambient
hinting per ADR-0022 §12.

Snapshot: highlighted_input_all_token_classes rebaselined
because the hint panel now displays an ambient hint instead
of the empty placeholder when input is non-empty.

Tests: 698 passing, 0 failing, 1 ignored (693 baseline →
+5 ambient_hint cases). Clippy clean.

Stage 6 introduces the IdentSlot taxonomy + parser audit so
identifier-typed slots can yield schema-aware completion
candidates in stage 8.
2026-05-10 17:42:13 +00:00
claude@clouddev1 11071ae164 ADR-0021 implementation: per-command usage templates in parse errors
New `dsl::usage` module: registry pairing each command's
entry-keyword with a `parse.usage.*` catalog key.
`matched_entry()` resolves the entry keyword from the
consumed token prefix; multi-entry families (add, drop,
show) return all matching keys.

Catalog: new `parse.usage.<command>` keys (one per command),
`parse.token.{keyword,punct,...}` vocabulary (one per
Keyword/Punct variant + token-class labels + LexError
kinds), and `parse.available_commands` for the no-prefix
fallback. Catalog grows ~60 entries.

Validator: extended KEYS_AND_PLACEHOLDERS; new completeness
test asserts every Keyword and Punct variant has its
`parse.token.*` entry.

`app::dispatch_dsl` rewritten to compose three blocks per
ADR-0021 §2: caret + structural/custom error + usage block
(or available-commands fallback per §5). Caret math fixed
to use original-input byte position rather than
trimmed-input position (the lexer no longer trims before
lexing). Three pre-existing app tests adjusted to look
across all error lines instead of `output.back()` (the
usage block is now the last line).

`dsl::usage::matched_entry` uses `<=` rather than `<` for
position comparison so custom errors raised by `try_map`
(whose span starts at the first consumed token) still
resolve to the entry keyword.

Tests: 668 passing, 0 failing, 1 ignored (650 baseline →
+18: 8 usage + 1 token-vocab completeness + 9 new
integration tests in tests/parse_error_pedagogy.rs
covering create/add/drop/show/frobulate/update/insert
cases). Clippy clean.
2026-05-10 14:41:32 +00:00
claude@clouddev1 a6fd26d15a ADR-0019 §9 sweep (3/3): ui.rs prose strings (caught in manual sanity)
Surprise gap from the post-sweep sanity check — `ui.rs` had a
substantial set of TUI-rendered strings that the previous two
sweep passes didn't cover. Caught by grepping for capitalised
literals in `ui.rs` after running the binary smoke check.

## Migrated

- **modal.*** — load picker title / empty state / path
  prompt; rebuild confirm title / "Continue?" prompt.
  (modal.path_entry's title comes from `save.*` since it's
  the save / save-as dialog.)
- **save.*** — `save` no-op hint, modal titles for
  Save / Save as, modal prompt body.
- **status.*** — status bar `Project:` label and the
  `(no project)` placeholder.
- **panel.*** — `Tables` panel title, `(none yet)`
  placeholder for empty tables, `(no active hint)`
  placeholder for the hint panel.
- **shortcut.*** — the bottom-bar keyboard hint labels
  (submit, confirm, cancel, yes, no, load, select,
  browse_path, back_to_list, switch, advanced_once,
  cancel_one_shot, quit). Each is a translatable label
  paired with a key name (Enter / Esc / Ctrl-C / etc.) at
  the call site. Keystroke names are deliberately left as
  literals — translating them would mean retraining users
  away from what their keyboard says.

The `push_shortcut` closure's parameter type changed from
`&'static str` to `&str` so it accepts the catalog-returned
String.

## Deliberately left

- **Echo prefix tags**: `[simple] `, `[advanced] `,
  `[system] `, `[error]  `. Their column widths are
  hardcoded into the wrap-width calculation in
  `render_output_panel`; translating them would silently
  break alignment. Worth a follow-up pass if a future locale
  needs different prefixes (would need `mode.label()` and
  the echo-tag widths to live behind a single locale-aware
  function).
- **Mode labels**: `SIMPLE` / `ADVANCED` / `Advanced:`
  rendered in the input panel border. Same alignment
  reasoning as the echo tags — also they're keywords (the
  user types `mode simple` to switch), so translating the
  display label without translating the command word would
  be confusing. Left as is.
- **Visual decoration**: `[Y]`, `[N]`, `[TEMP] `, `>`
  cursor markers, `█` cursor block, `↑↓` arrow glyph,
  `›` selection marker. Universal symbols / labels rather
  than translatable prose.

## Catalog totals

The catalog now has ~170 entries across 16 categories.
`tests/engine_vocabulary_audit` passes — no engine
vocabulary leaks anywhere user-reachable.

## Tally

610 tests passing (no change — pure refactor with
identical-output catalog substitutions). Clippy clean
with nursery lints. Release builds at 7.8 MB.

ADR-0019 §9 is now genuinely complete.
2026-05-09 22:41:06 +00:00
claude@clouddev1 720511ef29 ADR-0019 §9 sweep (2/2): help blocks + modals + system notes
Final pass of the i18n migration sweep. Every user-visible
string in `src/` now flows through the catalog via `t!()`.

## Categories migrated in this commit

- **help.cli_banner** — the entire `cli::HELP_TEXT` const,
  formerly a 40-line `&'static str`, is now a YAML block in
  the catalog. The const is replaced by a thin
  `cli::help_text() -> String` wrapper that performs the
  catalog lookup. `main.rs` calls `help_text()` for both
  `--help` output and the args-parse error path. The two
  integration tests that referenced `HELP_TEXT` directly are
  updated.
- **help.in_app_body** — the in-app `help` command's body is
  one YAML block; `note_help` becomes 5 lines that iterate
  the lines and emit each as its own output row (preserving
  the renderer's "one logical line = one display row"
  invariant for accurate scroll math).
- **modal.*** — load picker, rebuild confirm, and save-as
  path-entry strings: rebuild_cancelled, load_cancelled,
  generic_cancelled, load_picker_nothing,
  path_entry_empty_name, path_entry_empty_path.
- **dsl.failed** — the `"<verb> <subject>" failed: <rendered>`
  wrapper around the friendly-error layer's translated
  message.
- **dsl.running** — the `running: <input>` echo line shown
  above each command's response. (Note: the en-US prefix
  "running: " is hardcoded in the parse-error caret-padding
  calculation. Translators changing the prefix must keep the
  width consistent — documented inline.)
- **advanced_mode.not_implemented** — the placeholder echo
  shown when SQL hits the unimplemented advanced-mode path
  (Q1 territory).
- **fatal.persistence** — the FATAL banner for
  PersistenceFatal events (ADR-0015 §8).
- **project.{load_path_missing,saveas_target_exists,**
  **import_zip_missing}** — runtime-side project-switch
  validation errors that surface via ProjectSwitchFailed.

## Catalog start-up ordering

`main.rs` now calls `friendly::catalog()` at the very top
(before args parsing) so `help_text()` works in both the
success path and the args-error path. A corrupted build
artefact still fails loudly with a useful panic; the
practical risk is essentially zero since the catalog is
`include_str!`'d at compile time and validated by the unit
test before shipping.

## Remaining literals

The only `note_*` calls in `src/` that still pass plain
strings are inside `#[cfg(test)]` modules — synthetic test
fixtures, not user-visible. The codebase passes the "every
user-visible string flows through the catalog" bar.

## Tally

610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints.

## What this closes

ADR-0019 §9 (migration sweep) — done.

ADR-0019 itself is now fully implemented:
- §1-§5: catalog + translator + voice + verbosity ✓ (`eac7e5b`)
- §6: row pinpointing + schema enrichment ✓ (`431645a`)
- §9: migration sweep ✓ (this + `aff528a`)
- §10: anchor phrases preserved throughout ✓
- The five "Out of scope" items remain explicitly bounded
  to future ADRs (advanced-mode SQL, settings persistence,
  pluralisation, runtime locale, value formatting,
  constraint management).
2026-05-09 22:29:28 +00:00
claude@clouddev1 aff528aa3f ADR-0019 §9 sweep (1/2): replay/client_side/ok/mode/messages/project/parse
First half of the catalog migration sweep. Six categories of
user-visible literals moved from inline `format!` calls to the
i18n catalog via `t!()`:

- **replay.*** — `[ok] replay … N command(s) run`,
  `replay … failed at line N: …`, the `> command` echo, and
  the inner `could not open` / `parse error` / `nested replay`
  wordings the runtime constructs inside `ReplayFailed.error`.
- **client_side.*** — the four [client-side] pedagogical notes
  from ADR-0017 §6 / ADR-0018 §9 (transformed,
  transformed_lossy, auto_fill_transition,
  auto_fill_add_serial, auto_fill_add_shortid). The
  `format_auto_fill_add_note` helper in db.rs now routes via
  the catalog too.
- **ok.*** — the `[ok] {verb} {subject}` summary header
  (consolidated through a new `App::note_ok_summary` helper)
  plus the per-operation row-count footers
  (`{count} row(s) inserted/updated/deleted`).
- **mode.*** — `mode: simple/advanced` set/show banners +
  `usage: mode …` + `unknown mode '{value}' …` errors.
- **messages.*** — `messages: short/verbose` set/show + the
  `unknown messages mode` error.
- **project.*** — `[ok] rebuild — {summary}`, `[ok] now
  editing: {display_name}`, `[ok] export — wrote {path}`, plus
  matching failure variants and the `usage: export/import`
  + `import: empty target after as` argument-parsing errors.
- **parse.*** — the `parse error: {detail}` wrapper around
  chumsky's structural output, the `{padding}^` caret pointer,
  and the `empty input` fallback for `ParseError::Empty`.

Catalog total: 99 lines of YAML across the new categories,
44 new entries declared in `keys.rs::KEYS_AND_PLACEHOLDERS`.
The validator (`keys_validate_against_catalog`) walks the
expanded list and confirms placeholder coverage / no format
specifiers / no engine vocabulary across every entry.

Anchor phrases (ADR-0019 §10) preserved verbatim; existing
substring assertions in the test suite hold.

## Tally

610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints. Release builds.

## Still ahead in the sweep

- Sweep 7: HELP_TEXT (CLI banner) + in-app `note_help` —
  large multi-line blocks.
- Sweep 8: modal labels (load picker, rebuild confirm,
  save-as path entry) + any remaining strays. Final pass.

Both shipping in a follow-up commit so this checkpoint
stays reviewable.
2026-05-09 22:20:34 +00:00
claude@clouddev1 431645ae60 ADR-0019 §6: runtime enrichment + row pinpointing
Closes the placeholder-substitution gap reported during manual
testing: FK violations were rendering `<value>` and `<column>`
literally because the App had no schema awareness. With this
change the runtime resolves the schema-dependent facts before
the App ever sees the failure.

## Architecture

- **Database** gains two public methods backed by new worker
  Request variants:
  - `read_relationships(table)` → (outbound, inbound) FK list
    (lifts the previously-private `read_relationships_*` pair
    into the public surface, behind a `RelationshipsReply`
    type alias).
  - `find_rows_matching(table, column, value, limit)` →
    `DataResult` for row pinpoint queries.

- **friendly module** gets:
  - New `FailureContext` struct: schema-resolved facts the
    runtime builds (table, column, value, parent_table,
    parent_column, child_table, optional diagnostic_table).
  - `TranslateContext` loses its lifetime parameter and gains
    `parent_table` / `parent_column` fields. All string fields
    are now `Option<String>` for ownership simplicity.
  - `TranslateContext::from_facts(operation, verbosity, facts)`
    helper.
  - Translator's FK paths now use `ctx.parent_table` /
    `ctx.parent_column` for child-side wording; FK Update gets
    a dedicated `fk_child_side_update` arm.
  - FK dispatch is enrichment-driven first
    (`parent_table` set → child-side; `child_table` set →
    parent-side), with operation as the tiebreaker.
  - The translator forwards `ctx.diagnostic_table` onto the
    `FriendlyError` so pinpointed rows render through the
    existing ADR-0017 §7 bordered renderer.

- **Event** `DslFailed` carries `(command, error, facts)`.
  The runtime populates `facts` via `enrich_dsl_failure`
  before posting the event.

- **Runtime** `enrich_dsl_failure(database, command, error)`
  classifies and resolves:
  - UNIQUE INSERT/UPDATE: parses `T.col` from engine message,
    finds the user's attempted value (with schema fallback
    for natural-order multi-value INSERT — including the
    serial/shortid auto-skip rule from `do_insert`), pinpoints
    the existing conflicting row(s) via `find_rows_matching`
    and renders as a `DiagnosticTable`.
  - NOT NULL INSERT/UPDATE: parses `T.col`; no value
    (definitionally null) and no pinpoint (engine doesn't
    identify the row).
  - FK INSERT/UPDATE: outbound relationship lookup picks the
    FK column the user is touching; resolves
    `parent_table`/`parent_column`/`value`. UPDATE falls back
    to inbound (parent-side) when no outbound match.
  - FK DELETE: inbound relationship lookup picks a child_table
    that references this row.

- **App** drops its old `attempted_value_for` /
  `column_from_qualified_target` helpers (their work moved to
  runtime where the Database is in scope).
  `build_translate_context` combines the runtime-supplied
  facts with the operation derived from the Command and the
  App's verbosity.

## Manual-test fixes folded in

Two issues surfaced during manual testing of the initial
implementation, both fixed:

1. Natural-order multi-value INSERT
   (`insert into Orders values (4, 11.99)`) skipped FK
   enrichment because `user_value_for_column` only knew the
   single-value short form. The schema-aware lookup
   (`user_value_for_column_with_schema`) now mirrors
   `do_insert`'s position-mapping rule (auto-generated
   columns skipped), so positional INSERTs onto tables with
   serial/shortid PKs resolve correctly. Regression test:
   `enrich_fk_insert_natural_order_multi_value_resolves_via_schema`.

2. The arity error on INSERT now lists the columns it
   expected — `expected 3 value(s) for (id, Name, Email), got 2`
   instead of the bare count. Surfaces what the user needs
   to fix without making them go check the schema.

## Tests

`tests/friendly_enrichment.rs` (+8 integration tests):
- UNIQUE INSERT with explicit columns: facts.{table, column,
  value, diagnostic_table} all resolved; pinpoint shows
  conflicting row.
- UNIQUE INSERT natural-order short form: schema fallback
  resolves the value.
- UNIQUE UPDATE: value pulled from assignments.
- NOT NULL INSERT: table+column resolved, value None
  (correct), no pinpoint.
- FK INSERT: parent_table, parent_column, value all resolved
  via outbound relationship lookup.
- FK INSERT natural-order multi-value: schema-aware lookup
  with auto-skip resolves correctly (regression for the
  manual-test bug).
- FK DELETE: child_table resolved via inbound relationship
  lookup.
- DbError::Unsupported: enrichment returns default
  FailureContext (no false positives).

App-level tests updated to populate `FailureContext` directly
(simulating runtime enrichment) for the verbosity / threading
checks.

## Tally

610 tests passing (was 603: +8 enrichment integration tests
minus 1 obsolete App-side helper test that the runtime
absorbed). Clippy clean with nursery lints. Release builds.
2026-05-09 22:10:05 +00:00
claude@clouddev1 eac7e5b81d ADR-0019 implementation: friendly error layer + i18n catalog
All eight implementation steps from ADR-0019's §"Order of
operations":

Step 1 — `src/friendly/` module skeleton; `t!()` macro; YAML
  catalog loader (`include_str!` + `serde_yml`); `{name}`
  substitution helper that rejects format specifiers per §8.4.

Step 2 — `error.*` catalog populated for UNIQUE / FK /
  NOT NULL / CHECK / type-mismatch / not_found / already_exists /
  generic / invalid_value, with verbose hints per
  pedagogical-voice rule (§5). Anchor phrases (§10) preserved
  verbatim.

Step 3 — `FriendlyError { headline, hint, diagnostic_table }`
  + renderer composing the three blocks per §7.

Step 4 — `translate(&DbError, &TranslateContext) → FriendlyError`.
  Classifies by `SqliteErrorKind` first, then by message text
  for the constraint family. `change column` failures route to
  the type-mismatch headline, subsuming the previous
  `friendly_change_column_engine_error` helper.

Step 5 — `DbError::friendly_message()` delegates to the
  translator with default context. Removed
  `friendly_change_column_engine_error` (absorbed) and
  `enrich_fk_message` (FK list moves to the deferred re-query
  step). One test rewritten to assert on the engine-classified
  payload rather than the removed enrichment text.

Step 6 — `messages (short|verbose)` app-level command parallel
  to `mode`. `App::messages_verbosity` (default verbose)
  threaded into `TranslateContext` via
  `App::build_translate_context`. `AppEvent::DslFailed` now
  carries the structured `DbError`, plus the App extracts the
  user's attempted value from `Command::Insert` / `Update`
  to fill the `{value}` placeholder for UNIQUE / NOT NULL.

Step 7 — Catalog validator (§8.6) checks for missing keys,
  unused/undeclared placeholders, format specifiers, and
  forbidden engine vocabulary. `main.rs` parses the embedded
  catalog at startup so a corrupted build artefact fails
  loudly there rather than at the first `t!()` call.

Step 8 — Anchor phrases (§10) held: existing tests asserting
  on "no such table", "already exists", "cannot be converted",
  etc. all pass without rewording.

## Tally

603 tests passing (was 561: +42 net). Clippy clean with
nursery lints. Release binary 7.7 MB.

## Deliberately deferred

- Schema-aware enrichment for FK violations (parent_table /
  parent_column / child_table) and the multi-value
  natural-order INSERT case for UNIQUE. Both need the
  Database handle in scope at translation time, so they
  bundle naturally with the row-pinpoint re-query work
  (ADR-0019 §6) — that follow-on adds runtime-side
  enrichment via a `Database` lookup and a structured
  failure-context carried on `DslFailed`. Until then,
  unfilled placeholders render as their `{name}` form for
  visual consistency with the catalog.
- Migration sweep (§9). Only `error.*` is catalog-driven so
  far; `help.*`, `ok.*`, `client_side.*`, `replay.*`,
  `parse.*`, modal labels, etc. migrate per-PR.
- Settings persistence for `messages`. In-session state for
  now; waits on the future settings ADR.
2026-05-09 12:43:37 +00:00