From 47601f7c85217cd0bdad2b4745d542cb400cf4a7 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Sun, 10 May 2026 07:48:08 +0000 Subject: [PATCH] Handoff doc for end of 2026-05-09 (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents this session's work and the recommended next move: ## Session totals - 11 commits since handoff-5 - 534 → 610 tests passing (+76) - Release binary 7.2 → 7.8 MB ## What landed - All four non-CI items from handoff-5's Independent Work list: B2 (int→bool tests), B1 (help update), A2 (engine- vocabulary audit), A3 (replay command) - ADR-0019 fully implemented end-to-end: - Friendly-error layer + i18n catalog (~170 entries across 16 categories) - §6 runtime row-pinpoint enrichment with schema-resolved facts - §9 migration sweep — every user-visible literal in src/ now flows through the catalog (caught a ui.rs gap during the post-sweep manual sanity check, folded it in as sweep 3/3) ## Recommended next move Parser-as-source-of-truth ADR + H1a implementation. The friendly-error layer made engine errors much better; parse-error wording is now the visibly-weakest user surface. User explicitly surfaced the gap during manual testing this session ("typing `create` should illustrate the expectation"). Bounded scope, high pedagogical value, unblocks I3/I4 in passing. A1 (CI workflow) noted as the easy alternative for a quick win first. ## Sharp edges captured - New i18n workflow: catalog + keys.rs + t!() at every use site, validator catches drift - TranslateContext is owned (no lifetime); App combines runtime FailureContext with verbosity - Anchor phrases load-bearing per ADR-0019 §10 - `running: ` prefix coupled to caret-padding math - main.rs initialises catalog before args parsing - Several alignment-coupled strings deliberately left out of the catalog (echo prefix tags, mode labels) --- docs/handoff/20260509-handoff-6.md | 592 +++++++++++++++++++++++++++++ 1 file changed, 592 insertions(+) create mode 100644 docs/handoff/20260509-handoff-6.md diff --git a/docs/handoff/20260509-handoff-6.md b/docs/handoff/20260509-handoff-6.md new file mode 100644 index 0000000..c3123d3 --- /dev/null +++ b/docs/handoff/20260509-handoff-6.md @@ -0,0 +1,592 @@ +# 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.