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

593 lines
24 KiB
Markdown

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