Files
rdbms-playground/docs/handoff/20260509-handoff-6.md
T
claude@clouddev1 47601f7c85 Handoff doc for end of 2026-05-09 (#6)
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)
2026-05-10 07:48:08 +00:00

24 KiB

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 <path>` 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 <path> 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.rsFriendlyError { headline, hint, diagnostic_table } payload + renderer composing the three blocks per ADR-0019 §7.
    • translate.rstranslate(&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.

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: <name> with pk [<col>:<type>...]" 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.

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.<command> 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.