# Session handoff — 2026-05-09 (6) Sixth handover. The previous session (handoff-5) shipped ADR-0017, ADR-0018, the parser tiny-win, and the cleanup queue. This session worked through every item on handoff-5's "Independent work" list (B2, B1, A2, A3 — A1 deferred at user request), then designed and **fully implemented ADR-0019** (friendly error layer + i18n catalog), including the schema-aware row-pinpoint enrichment and the catalog migration sweep. The next agent picks up a clean baseline with only one substantial recommended next move. ## State at handoff **Branch:** `main`. Working tree clean. **1 commit ahead of `origin/main`** (just the latest §9 sweep — earlier commits were pushed between turns). Push remains the user's call. Commits since handoff-5: ``` a6fd26d ADR-0019 §9 sweep (3/3): ui.rs prose strings (caught in manual sanity) 720511e ADR-0019 §9 sweep (2/2): help blocks + modals + system notes aff528a ADR-0019 §9 sweep (1/2): replay/client_side/ok/mode/ messages/project/parse 431645a ADR-0019 §6: runtime enrichment + row pinpointing eac7e5b ADR-0019 implementation: friendly error layer + i18n catalog d4801ea ADR-0019: pluralisation is a translator concern, not deferred work 2a8618c ADR-0019: friendly error layer (H1) and i18n message catalog c4ee264 replay: new `replay ` command (A3, U4) b8102dc tests: ADR-0002 engine-vocabulary audit (A2) 3dbaedc help: surface ADR-0017/0018 auto-fill semantics (B1) 0d7a7bc db: end-to-end tests for change_column int -> bool (B2) ``` **Tests:** **610 passing, 0 failing, 1 ignored** (up from 534 at handoff-5's baseline; +76 over this session). The ignored test is unchanged from handoff-5 — not new debt. Per-phase counts: - B2 (int→bool tests): +2 (534 → 536) - B1 (help text test): +1 (536 → 537) - A2 (engine-vocabulary audit): +4 (537 → 541) - A3 (replay command): +20 (541 → 561) - ADR-0019 H1 implementation: +39 (561 → 600) - §6 runtime enrichment: +8 integration tests + 2 fixups (600 → 610) - §9 migration sweep: 0 net (pure refactor) **Clippy:** clean with `nursery` lints enabled. **Release build:** ~7.8 MB single binary (up ~600 KB from handoff-5's 7.2 MB; the increase is the friendly module + serde_yml + the embedded en-US catalog). ## What's implemented (delta vs. handoff-5) ### Independent work from handoff-5 §"Independent work" All four non-CI items shipped: - **B2** — End-to-end tests for `change column int → bool` (the `(Int, Bool)` matrix entry at the db.rs level, not just the per-cell unit tests). - **B1** — In-app `help` updated to surface ADR-0017's flag semantics and ADR-0018's auto-fill behaviour. New regression test pins the wording so future help-text edits can't silently drop the pedagogical lines. - **A2** — ADR-0002 engine-vocabulary audit confirmed the codebase is already clean (no `SQLite` / `STRICT` / `PRAGMA` / `rusqlite` in user-reachable strings). `tests/engine_vocabulary_audit.rs` pins this so a regression fails loudly. - **A3** — New `replay ` DSL command. Parser grammar, `Action::Replay`, runtime `run_replay`, per-line failure reporting, file-relative-to-project resolution, nested-replay refusal, history.log invariant (sub-commands persisted but the replay invocation itself is not). 9 integration tests. **A1 (CI workflow) remains open** — explicitly postponed at the start of this session. ### ADR-0019 (friendly error layer + i18n) — **fully implemented** The session's biggest piece. Started as a deferred handoff-5 "pending work" item; now the entire ADR is shipped, including the originally-deferred §6 (row pinpointing) and §9 (migration sweep). What the ADR provides: - **Single chokepoint for user-visible message wording.** Every literal that reaches the user goes through the i18n catalog (`src/friendly/strings/en-US.yaml`) via the `t!()` macro. ~170 entries across 16 categories (`error.*`, `client_side.*`, `replay.*`, `ok.*`, `mode.*`, `messages.*`, `project.*`, `parse.*`, `help.*`, `dsl.*`, `advanced_mode.*`, `fatal.*`, `modal.*`, `save.*`, `status.*`, `panel.*`, `shortcut.*`). - **`friendly` module** owns the structured translator: - `format.rs` — catalog loader (YAML embedded via `include_str!` + `serde_yml`), `{name}` substitution rejecting format specifiers per ADR-0019 §8.4. - `keys.rs` — the canonical `KEYS_AND_PLACEHOLDERS` list every translation site references; a unit test validates every key exists, placeholders match, no specifiers, no engine vocabulary, no orphan YAML entries. - `error.rs` — `FriendlyError { headline, hint, diagnostic_table }` payload + renderer composing the three blocks per ADR-0019 §7. - `translate.rs` — `translate(&DbError, &TranslateContext) → FriendlyError` classifies UNIQUE / FK / NOT NULL / CHECK / type-mismatch / not_found / already_exists / generic / invalid_value with operation-tailored wording per §4. Verbose vs short via the `Verbosity` enum. - **Runtime-side row pinpoint + schema enrichment** (ADR-0019 §6). When an INSERT/UPDATE/DELETE fails, the runtime calls `enrich_dsl_failure(database, command, error)` which: - Parses the engine message to identify the table/column. - For UNIQUE: looks up the user's attempted value from the Command (with schema-aware fallback for natural-order multi-value INSERT — including the serial/shortid auto-skip rule), pinpoints the existing conflicting row(s) via `Database::find_rows_matching` and renders as a `DiagnosticTable`. - For FK INSERT/UPDATE: outbound relationship lookup resolves `parent_table`, `parent_column`, and the attempted `value`. - For FK DELETE: inbound relationship lookup resolves `child_table`. - For NOT NULL: table+column resolution; no value or pinpoint (the value is null by definition). - **`messages (short|verbose)` app-level command**. In-session state on `App::messages_verbosity`, threaded through `TranslateContext`. Default `verbose` (pedagogical headline + hint + optional diagnostic table). `short` drops the hint. Persistence waits on a future settings ADR. - **`AppEvent::DslFailed`** carries `(command, error: DbError, facts: FailureContext)` so the App can defer rendering and apply its current verbosity at display time. - **Catalog validator** (`tests::keys_validate_against_catalog`) enforces six invariants at build time: every key declared, every placeholder used and declared, no format specifiers, no forbidden engine vocabulary, no orphan YAML entries. - **`main.rs`** parses the catalog at the very top so a corrupted build artefact fails loudly there rather than at the first `t!()` call deep inside the event loop. What the ADR explicitly leaves out (still bounded to future ADRs): - Advanced-mode SQL error sanitisation (waits on Q1). - Settings persistence for `messages` (future settings ADR). - Plural-form rules per locale (intentionally not a goal — see ADR §8.5 amendment). - Runtime locale selection (§8.2). - Locale-aware value formatting (rejected, not deferred — §8.7). - Constraint-management surface for CHECK (C3 territory; the catalog has CHECK wording ready as a placeholder). - Echo prefix tags + mode labels in `ui.rs` — left as literals because they're width-coupled to the alignment math; documented in commit `a6fd26d`. ### Anchor phrases preserved (ADR-0019 §10) The catalog's anchor-phrase commitments held throughout: "no such table", "no such column", "no such relationship", "already exists", "already has the value", "cannot be converted", "discard information", "referenced by", "[client-side]". Existing tests asserting on these substrings still pass without rewording. ## Recommended next move ### Parser-as-source-of-truth ADR + H1a implementation **This is the strongest recommendation.** Rationale: - It's the natural follow-on from H1. The friendly-error layer dramatically improved engine-error wording; the parser-error wording is now the visibly-weakest user surface. - A concrete user gap surfaced during manual testing in this session: typing `create` produces `after `create`, expected `table`` — informative about the next missing token but not about the grammar of the command. The user explicitly asked "can we illustrate the expectation?" and we agreed it was a separate piece of work that needs its own ADR. - The handoff-5 pending list named it as the "load-bearing piece" because it unblocks H1a (syntax help in parse errors), I3 (tab completion), I4 (syntax highlighting), and on-the-fly error squiggles in one go. - The chumsky `keyword_ci` structural-error rework is the specific technical piece — today `keyword_ci` emits `Rich::custom` errors that don't aggregate across `choice` alternatives, so we get "expected `table`" instead of "expected `data` or `table`". Fixing that unlocks the rest. Suggested ADR scope: - What structured information chumsky already gives us (expected sets, span-tagged AST, partial parses on failure) and what we currently throw away. - `keyword_ci` rework so `choice` alternatives aggregate (the load-bearing change). - Per-command grammar templates surfaced in the error ("`create table` expects: ` with pk [:...]`" rather than a single missing-token pointer). - Thinking ahead: how the same parse-output feeds tab completion (next valid token at cursor position) and syntax highlighting (token classification from the AST). - Catalog migration: parse-error wording joins `parse.*` once the grammar templates are in place. The current `parse.error` / `parse.caret` / `parse.empty` keys cover the wrapper; the per-command templates would land as new keys (`parse.usage.create` etc.). Estimated: ADR design 200-400 lines; implementation probably 300-500 lines plus tests. Comparable in scope to ADR-0017's reception path. ## Other open work, in suggested priority order ### Easy alternative if you want a quick win first: A1 (CI workflow) Single GitHub Actions YAML at `.github/workflows/ci.yml`. Cross-platform Linux / macOS / Windows; `cargo test` + `cargo clippy --all-targets -- -D warnings`. Locks in the 610-test green baseline. Standard Rust CI template adapted to nursery-clippy. 1-2 hours. Detailed plan in handoff-5 §A1, unchanged. ### Larger pending pieces **Query DSL ADR + implementation.** Biggest remaining design piece. Earlier discussions landed on extending `show data` into a SELECT-style command with WHERE / projection / order; expose generated SQL as a pedagogical hook; bundle C5a's complex WHERE into one coherent feature. Then QA1 (EXPLAIN QUERY PLAN) becomes meaningful. **Constraint management surface (C3).** UNIQUE / CHECK / NOT NULL DDL operations. The friendly-error layer has CHECK wording ready; the missing piece is the DDL surface itself. Probably 400-600 lines + tests. **V-series UX projects** (handoff-5 §"Bigger UX projects"): - V4 — session log + Markdown export. - V1/V2 — relationship rendering (the "two structures + arrow" view). - V3 — ER diagram export. ### Smaller items still on the table - I1 — multi-line input (Enter inserts newline, Ctrl-Enter submits). - I1b — readline shortcuts (Ctrl-A/E, Ctrl-W/K/U). - I3 — tab completion (depends on parser-as-source-of- truth). - I4 — syntax highlighting (depends on parser-as-source-of-truth). - C4 — m:n convenience (auto-junction-table). Rebuild primitive is solid so this should be straightforward. ### Tracked but explicitly bounded to other ADRs - Q1 (SQL handling in advanced mode) — waits on Q4 (SQL subset ADR). - U-series undo/snapshot (replay landed this session as A3; undo + snapshot are independent and need their own pass). - Settings persistence — feeds the deferred `messages` persistence among other things. ## Sharp edges and subtleties (delta vs. handoff-5) Carried-over edges still apply (sync `update`, worker thread, metadata transactions, rebuild-table primitive, modal infrastructure, project-switch lock dance, `[temp]` cleanup guards, persistence ordering, `DataResult` carries `column_types`, `output_render` is the only place tabular output should originate, `Type::Serial` no longer implies PK, `add column` returns `AddColumnResult`, `ChangeColumnTypeResult.client_side` field shape, non-PK serial INSERT auto-fill via `MAX(col)+1`, schema_to_ddl inline UNIQUE for non-PK, `read_schema` reads UNIQUE via `pragma_index_list`, structured parse-error rendering). New ones this session: - **Every user-visible string flows through the catalog.** When adding a new error / hint / modal label / shortcut, the workflow is: add the entry to `src/friendly/strings/en-US.yaml`, add the `(key, &[placeholders])` tuple to `src/friendly/keys.rs::KEYS_AND_PLACEHOLDERS`, then call `crate::t!("category.key", placeholder = value)` at the use site. The validator unit test fails the build if any of those three steps are missed. - **The translator's input is `TranslateContext` (owned Strings).** It used to be borrowed; the move to owned strings landed when runtime enrichment took over the schema-resolved facts. App's `build_translate_context` combines runtime-supplied `FailureContext` with the Command's operation derivation and the App's verbosity. - **Anchor phrases are load-bearing.** ADR-0019 §10 lists 9 substrings the catalog commits to keeping stable. Many existing tests assert on these. When migrating a category to new wording, preserve the anchor or consciously update the catalog comment block. - **The `running: ` prefix is hard-coded against the caret-padding math.** `app.rs` derives the caret position from `prefix.chars().count() = 9`. The `dsl.running` catalog template **must** start with "running: " for caret rendering to align. Documented inline. - **`main.rs` initialises the catalog before args parsing** so the args-error path can use `help_text()`. A corrupted catalog (impossible in practice since it's `include_str!`'d and validated) would panic before the args error surfaces. Acceptable for a teaching tool. - **`AppEvent::DslFailed` carries structured payload, not a pre-rendered string.** Tests that synthesise this event (in `tests/walking_skeleton.rs` and `app::tests`) must construct a `DbError` and a `FailureContext` (use `::default()` if you don't care about enrichment). - **`Database::find_rows_matching(table, column, value, limit)`** is the public hook for row-pinpoint queries. The runtime uses it for UNIQUE conflict diagnostics. If a future feature wants similar row-finding (e.g. FK parent-side pinpoint, which is structurally plumbed but not yet populated — see runtime.rs's `enrich_fk_violation`'s "FK pinpoint not implemented in v1" comment), reuse this method. - **`Database::read_relationships(table)` returns `(outbound, inbound)`.** The lifted version of the previously-private `read_relationships_outbound/inbound` pair. ## ADR index (read these before touching the related areas) ``` 0000 Record architecture decisions (process) 0001 Language and TUI framework (Rust + Ratatui) 0002 Database engine — User-facing posture (engine-vocabulary audit regression-tested via tests/engine_vocabulary_audit.rs) 0003 Input modes and command dispatch 0004 Project file format — amended by 0015 0005 Column type vocabulary — definition of `serial` generalised by ADR-0018 0006 Undo snapshots and replay log — replay command landed this session (ADR-0019 §9 migration sweep covered its message wording too) 0007 Sharing and export — amended by 0015 amendment 1 0008 Testing approach 0009 DSL command syntax conventions 0010 Database access via worker thread — ADR-0019 §6 enrichment uses the worker via two new public methods (read_relationships, find_rows_matching) 0011 FK column type compatibility 0012 Internal metadata for user-facing column types 0013 Relationships, naming, and rebuild-table strategy 0014 Data operations, value literals, and auto-show 0015 Project storage runtime — ColumnSchema gained `unique: bool` for ADR-0018 0016 Pretty table rendering for data and structure views — ADR-0019 §7 reuses `render_diagnostic_table` for the friendly-error pinpoint output 0017 Column type-change compatibility 0018 Auto-fill contracts for serial and shortid columns 0019 Friendly error layer (H1) and i18n message catalog — IMPLEMENTED (this session). Catalog covers ~170 entries across 16 categories. Runtime enrichment per §6, migration sweep per §9 both done. ``` ## Repository layout (delta vs. handoff-5) ``` src/ friendly/ — new module (ADR-0019) mod.rs — public API, t!() macro format.rs — catalog loader, substitution keys.rs — KEYS_AND_PLACEHOLDERS + validator error.rs — FriendlyError + DiagnosticTable + render translate.rs — DbError → FriendlyError classification strings/ en-US.yaml — the catalog body (~170 entries) action.rs — Action::Replay app.rs — messages command + verbosity field; build_translate_context; attempted-value extraction (later moved to runtime); note_ok_summary helper; modal/help/system notes all go through t!() cli.rs — HELP_TEXT const replaced with pub fn help_text() that reads catalog db.rs — Request::ReadRelationships + Request::FindRowsMatching; read_relationships / find_rows_matching public methods; RelationshipsReply type alias; friendly_message body delegates to translator; friendly_change_column_engine_error + enrich_fk_message removed; fk_violation_message_lists_outbound_relationships test rewritten as fk_violation_returns_engine_classified_constraint_error dsl/ command.rs — Command::Replay variant parser.rs — replay rule + path_literal terminal event.rs — DslFailed { command, error, facts } + Replay events main.rs — catalog init at top; help_text() use runtime.rs — Action::Replay handler; spawn_replay + run_replay (pub for integration tests); enrich_dsl_failure (pub) + helpers; resolve_replay_path ui.rs — modal/status/panel/shortcut strings routed through t!() (left echo prefix tags + mode labels alone — alignment- coupled, documented) docs/ adr/ 0019-friendly-error-layer-and-i18n.md — new (this session) README.md — indexed handoff/ 20260509-handoff-6.md — this file tests/ engine_vocabulary_audit.rs — new in this session (A2) friendly_enrichment.rs — 8 integration tests for ADR-0019 §6 replay_command.rs — 9 integration tests for A3 (U4) ``` ## How to take over 1. Read this file. 2. Read `CLAUDE.md` for the working-style rules. 3. Read `docs/requirements.md` for the granular progress table. 4. **If picking up the recommended next move (parser-as- source-of-truth ADR)**: read `docs/adr/0019-*` to see how ADR-0019 framed catalog wording, since parse errors join the catalog under `parse.*`. The current keys are `parse.error`, `parse.caret`, `parse.empty` — the new work would add `parse.usage.` and friends. Read `src/dsl/parser.rs` for the chumsky scaffolding and `src/app.rs::dispatch_dsl` for the source-line + caret rendering. The `keyword_ci` rework is the technical core. 5. **If picking up A1 (CI)**: handoff-5 §A1 has a complete plan. Nothing new to add. 6. **If picking up Query DSL or another bigger piece**: start with an ADR draft. Don't implement without one — those touch enough code to warrant the discipline. 7. Run `cargo test` to confirm the 610-test green baseline. 8. Run `cargo clippy --all-targets` to confirm clippy-clean. 9. Run `cargo run --release` and try the smoke test in the next section. ### End-to-end smoke test (current state) Demonstrates ADR-0019's friendly-error wording with row pinpointing. Replaces handoff-5's recipe (which is now stale — every error path renders through the catalog and shows pinpointed rows where applicable). ``` $ rm -rf /tmp/handoff6-smoke $ rdbms-playground --data-dir /tmp/handoff6-smoke # Inside the app: help -- in-app help (now from catalog) messages -- shows current verbosity (verbose by default) # Setup: create table Customers with pk id:int add column Customers: Name (text) insert into Customers (1, 'Alice') insert into Customers (2, 'Bob') create table Orders with pk id:serial add column Orders: CustId (int) add column Orders: Total (real) add 1:n relationship from Customers.id to Orders.CustId insert into Orders (CustId, Total) values (1, 9.99) # UNIQUE INSERT — original report case from this session: insert into Customers (1, 'Carol') -- emits: -- "insert into Customers" failed: -- `Customers.id` already has the value `1`. -- The `id` column on `Customers` is unique — -- pick a different value, or update the existing -- row instead. -- + bordered table showing Alice's row. # UNIQUE UPDATE — operation-tailored hint: update Customers set id=1 where Name='Bob' -- "your update would create a duplicate" -- (different from the INSERT wording) # FK INSERT (child-side) — was broken pre-§6, now resolves # parent_table/parent_column/value via outbound-FK lookup: insert into Orders (CustId, Total) values (999, 5.50) -- "no parent row in `Customers` has `id` = `999`" -- + hint about inserting a matching parent. # FK DELETE (parent-side) — child_table from inbound-FK lookup: delete from Customers where id=1 -- "`Customers` rows are referenced by `Orders`" # Compare verbosity: messages short insert into Customers (1, 'Carol') -- headline only, no hint, but pinpoint table still shows messages verbose insert into Customers (1, 'Carol') -- full headline + hint + pinpoint # Replay (A3 from earlier in session): # Save a few commands to a file then replay: save -- prompts for project name # Or use `replay history.log` to re-run the entire session. replay history.log # Anchor phrases: show data Ghost -- "no such table: `Ghost`" (anchor: "no such table") quit ``` ### Manual spot-checks worth running - `--help` produces the CLI banner from the catalog (no literal const anymore). - `mode advanced` then any input produces the not-implemented placeholder ("advanced mode SQL not implemented yet — echo: …"). - `messages` toggles verbosity in-session; not persisted across restarts (waits on settings ADR). - Switch to a non-existent project path → see "path `…` does not exist" via `project.load_path_missing`. - Trigger a parse error (e.g. `create`) → see the caret pointer aligned under the offending character + the structural "after `create`, expected `table`" message (still chumsky-derived; the parser-as-source-of-truth ADR addresses this). This is the recommended-next-move hook.