# 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.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: `<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.

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