diff --git a/CLAUDE.md b/CLAUDE.md
index a925f5b..952c2a3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -37,9 +37,9 @@ Current decisions at a glance (each backed by an ADR):
simple to advanced (ADR-0003). No other sigils.
- **Project format:** `project.yaml` + `data/
.csv` +
`history.log`; `playground.db` is a derived artifact (ADR-0004,
- amended by ADR-0015). Implemented through Iteration 4 +
- cleanup; export/import (Iter 5) and migration framework /
- --resume / persistent input history (Iter 6) pending.
+ amended by ADR-0015). Fully implemented (ADR-0015 Iterations
+ 1–6): export/import, `--resume`, persistent input history, and
+ the migration framework scaffold are all done.
- **Project storage runtime:** every command persists through to
db + yaml + csv + history.log in one execution context, gated
by the combined db persistence logic; commit-db-last ordering
@@ -338,16 +338,8 @@ all of `target/`, forcing a full from-scratch rebuild).
These are explicitly tracked (mostly in `requirements.md`) but
not yet implemented:
-- **Project storage** (track 2): largely implemented through
- Iteration 4 + cleanup pass + safety hardening (Iterations
- 1–4 of ADR-0015). Pending pieces: `export` / `import` (Iter
- 5), `--resume` + persistent input history hydration +
- migration framework scaffold (Iter 6).
- **Modify relationship** (C3a): drop+add covers the use case
today.
-- **m:n convenience** (C4): auto-generates a junction table
- with appropriate FKs — depends on relationships being solid
- (they are).
- **Strong syntax-help in parse errors** (H1a): point users at
missing keywords/clauses rather than the unexpected
character. *(H1 — the friendly **database**-error layer — is
@@ -358,11 +350,8 @@ not yet implemented:
- **Session log + Markdown export** (V4): the bigger UX
project — scrollable session journal, smart structure
rendering, save-as-markdown.
-- **Readline shortcuts** (I1b): Ctrl-A/Ctrl-E, Ctrl-W/Ctrl-K/
- Ctrl-U.
- **Multi-line input** (I1): Enter inserts newline,
Ctrl-Enter submits.
-- **Tab completion** (I3), **syntax highlighting** (I4).
- **ER diagram export** (V3).
- **Full TT5** (CI): the pipeline is live (see the CI decision
above / `docs/ci/adr/`), but "all tiers on all OSes" isn't
diff --git a/docs/adr/0047-demonstration-overlay-layer.md b/docs/adr/0047-demonstration-overlay-layer.md
index 4258e95..9678648 100644
--- a/docs/adr/0047-demonstration-overlay-layer.md
+++ b/docs/adr/0047-demonstration-overlay-layer.md
@@ -414,5 +414,41 @@ time-boxed-`recv` path. We therefore test the **pure pieces**
exhaustively (label fn, capture state machine, nearest-deadline helper)
and assert plumbing via Tier-3, rather than over-claiming an integration
test of the `tokio` timeout itself.
-
-
+
+## Amendment 1 — `Ctrl-G` demo-mode alias for F1 (2026-06-15)
+
+**Context.** The contextual `hint` overlay (ADR-0053 / H2) is opened with
+**F1**. But F1 reaches the app only as an escape sequence (`\eOP` /
+`\e[11~`), and the `autocast` recorder used for our screencasts **cannot
+emit escape sequences** — so a cast can never trigger F1, and the single
+most teaching-relevant overlay is unreachable in recordings. The same
+wall already bit step-captions (which is why `Ctrl+]`, a single control
+byte, was chosen over `Ctrl+!`).
+
+**Decision.** In **demo mode only**, **`Ctrl-G`** is an alias for F1. It
+runs the exact F1 hint logic (live-input → form hint; empty input →
+recent-error / getting-started) and is **badged as `[F1]`** (not
+`[CTRL-G]`) so a recorded cast is visually identical to a genuine F1
+press. `Ctrl-G` is the only viable choice: it is a single legacy control
+byte autocast can send, whereas `Ctrl`+digit (e.g. the mnemonic `Ctrl-1`)
+is **not encodable in a legacy terminal at all** — digits have no control
+byte, so `Ctrl-1` arrives as a bare `1`; the kitty protocol *would* encode
+it but only as an escape sequence (the very thing autocast can't send),
+and this app deliberately does not enable keyboard-enhancement flags.
+
+**Why demo-gated.** The shipped keymap stays F1-only — a real user never
+trips the alias, and demo mode is also the mode teachers/presenters run,
+so the alias is available exactly where it's wanted. Outside demo mode
+`Ctrl-G` falls through to the inert catch-all (the `Char(c)` insert arm
+excludes CONTROL, so no `g` is typed).
+
+**Scope.** `hint_key` guard in `App::handle_key` gains the demo-gated
+`Ctrl-G` disjunct; `demo_badge_label` maps `Ctrl-G → [F1]` (consulted
+only in demo mode). Test-first: three `app.rs` Tier-1 tests (alias fires
+on input + on empty input; inert when demo off) + the badge-map
+assertion. The keybinding strip (ADR-0051) is **not** changed — F1 stays
+the advertised key; `Ctrl-G` is a recorder aid, and the badge already
+reads `[F1]`.
+
+*(Editorial: this amendment also removed two stray `` /
+`` lines accidentally committed at the end of this file.)*
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 85daf6e..c831b28 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -59,7 +59,7 @@ This directory contains the project's ADRs, recorded per
- [ADR-0044 — Relationship visualization (two-table connector diagrams)](0044-relationship-visualization.md) — **Accepted 2026-06-09; implemented 2026-06-10** (closes `requirements.md` V1; second `/runda` pass over the implementation; §3 last-resort helper line considered and rejected). Resolves **ADR-0016 OOS-1** and closes the open half of `requirements.md` **V1** ("a selected relationship as two tables joined by a line"). Renders a relationship as **Style A** (two structure boxes + connector). **Reach = "relationship-relevant"** (user-chosen over global / show-only): diagrams on the surfaces where the relationship is the *subject* — `show relationship ` (one full diagram), `show table ` (T's structure box then a **Relationships** section of **stacked compact** per-relationship diagrams — chosen over a focal-centred subgraph: no crossing lines, scales via scroll, two-boxes-wide fits any terminal), and relationship DDL echoes (`add`/`drop`/`modify relationship`); incidental DDL echoes (`add column`, `drop index`, `change column`, plain `create table`) keep the terse prose, via a `Diagram`|`Prose` render mode on `render_structure`. Reading convention **child(FK)-left / parent-right, arrow →, `n`…`1` cardinality**, applied uniformly; every box gets a **bold title row + rule** so the name can't read as a column. **Compound FKs** (ADR-0043) route one connector per positional pair + an explicit pairing line. **Width-aware** (first in the codebase) but **App-side**: `render_structure`/diagram rendering runs in `app.rs` (the worker only returns `TableDescription`s), a new `App::last_output_width` (set from `ui.rs`) drives side-by-side vs a **vertical-stack** fallback + last-resort "run `show relationship`" pointer; rendered once at command time, **no live reflow** (V4). `show relationship`'s worker path (`do_show_one`, prose-only) is restructured to return both endpoint `TableDescription`s. Styling reuses **ADR-0028** App-side styled runs (new classes: table-name/key/connector/cardinality/action) — no worker→UI contract change. **Partially supersedes ADR-0016 §5** (prose block replaced on relationship-subject surfaces, retained on incidental ones); extends §4 (layout width-awareness, still no cell truncation) and §6 (per-span theming). Tests: insta snapshots (single, compound, vertical fallback, helper line, self-referential, multi-rel `show table`) + width-threshold/routing unit tests + Tier-3 wiring; enumerated prose-fallout updates (`output_render.rs:121/135/793`, the relationships snapshot, `walking_skeleton.rs:477/530`). A `/runda` DA pass corrected three inverted-architecture claims (App-side rendering, untracked width, prose-in-worker show-relationship) before acceptance. OOS: user-configurable display setting (OOS-7), live reflow (V4), whole-DB ER export (V3), m:n (C4), ASCII fallback (ADR-0016 OOS-5)
- [ADR-0045 — `create m:n relationship` convenience command (C4)](0045-mn-convenience.md) — **Accepted + implemented 2026-06-10** (closes `requirements.md` **C4**; all forks user-confirmed + a `/runda` DA pass that verified the `do_create_table` reuse against code and corrected the "no PK-less tables" assumption — advanced SQL `create table t (a int)` has none, so a parent-PK guard is retained). Implementation corrected a second ADR premise: "the walker already dispatches multiple nodes per entry word" held only in *advanced* mode — two simple-mode spots (dispatcher `decide`, completion continuation-merge) assumed ≤1 DSL form per entry word and were generalized **behaviour-preservingly** (dispatch reduces to the old single-candidate commit; completion merge gated on `simple_count > 1`). Junction echo wired (`render_create_m2n`, round-trips as SQL). `create m:n relationship from to [as ]` generates a junction table with one FK column per parent PK column, a **compound PK over all the FK columns** (the textbook junction — the pair is unique, no duplicate links), and **two 1:n relationships**, all in **one transaction = one undo step** (built by reusing `do_create_table`, which already takes `foreign_keys` + writes relationship metadata — no batch bracketing). Forks all user-chosen: junction PK = compound-over-FKs (vs surrogate serial / no PK); referential actions = **`CASCADE`** on delete+update (vs NO ACTION / RESTRICT); naming = auto `{T1}_{T2}` + optional `as` (vs auto-only); available in **both modes** (Simple-category DSL, like the sibling relationship commands). FK columns named `{parent_table}_{pk_column}` (disambiguates shared `id`; generalises to compound parents via ADR-0043), typed via `fk_target_type` (ADR-0011). A distinct `Command::CreateM2nRelationship` (not lowered to `CreateTable`) preserves command identity (X5) and lets the teaching echo speak in m:n terms. Cross-cutting wiring enumerated: separate `CREATE_M2N` `CommandNode` (own `help_id`/`usage_ids`), `("m","m:n")` completion composite, `HintMode`s, grammar-driven highlighting, `help`/`help create`, `parse_error_pedagogy` near-miss matrix, teaching echo. OOS: **self-referential m:n** (`from T to T`) refused outright (user-confirmed "full stop" — directional column-naming is more than this beginner convenience warrants); per-relationship action overrides; extra junction payload columns; m:n diagram echo; renaming the auto-generated relationships
- [ADR-0046 — Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20/#21/#23)](0046-sidebar-navigation-and-responsive-input-hint.md) — **Accepted + implemented 2026-06-10, phased A→B→C** (8 commits `9f5f76b`…`22bec61`; closes Gitea **#20** hint jumpiness, **#21** left-column improvements, **#23** long input — all forks user-confirmed, including the persistent show/hide toggle which is **deferred**: the Ctrl-O peek covers #21's "keystroke to show and hide"). Two decisions landed differently from the draft (recorded inline): relationship data on **`App`** not `SchemaCache` (DB2); the nav overlay clears **only the sidebar strip + a one-column gutter**, panels staying visible behind (DC2). Treats the three UI issues as one coupled decision because they share the terminal's width/height budget. **Phase A (input & hint):** the hint panel's height becomes a function of **terminal geometry, fixed between resizes** (not of hint content), eliminating the #20 jump at its source — measured catalog shows ≥ ~54-col right-column width never needs > 2 hint lines, so 3 lines is a rare narrow-terminal-only case; height buckets `H<40` compact (input 1 row + horizontal scroll / hint 2) vs `H≥40` comfortable (input 2 rows soft-wrap / hint 2), output `Min(5)` honoured first under degradation; input gains horizontal scroll (`input_scroll_offset`, single logical `String` — **not** I1 multi-line) and 2-row soft-wrap display when tall, preserving ADR-0027's 6-col indicator reserve. **Phase B (sidebar):** the 26-col Tables column is **kept but made optional and richer** (not deleted — pedagogy wins ties) — **width-derived session-only** visibility (visible iff width > 90 or a Ctrl-O peek is active — no stored field; hides at width ≤ 90 so the 90-col screencasts drop it; ADR-0015 format untouched), plus a **relationships panel** rendered narrow with endpoints broken at the arrow, ellipsized — a **separate sibling panel** that **overrides S2**'s nested-list extension model (relationships are cross-table). the full records live on a new **`App.relationships`** field (revised from the ADR's original `SchemaCache.relationship_details` at implementation — `SchemaCache` is walker-facing and needs only the names, kept in `relationships: Vec`; details are UI-only, so `App` mirrors `app.tables` and avoids ~23 fixture edits), delivered by `Database::read_all_relationships` + an `AppEvent::RelationshipsRefreshed`; the two left panels split vertically with the relationships panel floored at 5 rows ("(none)" when empty) and capped at 50 % of the column (DB4). **Phase C (navigation mode):** **`Ctrl-O`** enters a focus cycle (Input → Tables → Relationships → Input; `Esc` exits) orthogonal to the ADR-0003 input mode — **`Ctrl-B` was rejected on review as the default tmux prefix** (unreachable inside tmux); the focused panel **expands to ~40–50 cols as a `Clear` overlay** (right panels stay unchanging underneath) and scrolls via **Up/Down (line) + PageUp/PageDown (page)** (context-rebind, reusing the output-scroll viewport mechanism), with an accent focus border; all non-nav keys inert in nav mode (and nav keys inert while a modal is open). Forks all user-chosen: keep-optional-richer (vs remove/narrow); navigation-mode (vs modeless modifier scroll); `Ctrl-O` (Ctrl-B rejected = tmux prefix); overlay (vs layout re-split); inert-non-nav-keys; geometry-fixed hint height; `H<40/≥40` thresholds; session-only persistence; Up/Down line-scroll; **separate relationships panel overriding S2**; **no hint-area toggle** (S4's stale "keyboard-toggleable" claim struck — never implemented, unwanted). A pre-build `/runda` DA pass drove these corrections: caught the `Ctrl-B`/tmux collision, the `SchemaCache` retype that would have broken completion, the 2-row-input/indicator placement, the missing nav-mode key disposition + modal gate, and three unreferenced requirements (S1 evolved, S2 overridden, S4 corrected); also cross-checked open issue **#22** (overlay/annotation layer — separate ADR, adjacent). OOS: true multi-line input (I1); readline shortcuts (I1b); cross-session sidebar persistence; output as a third nav focus; relationship search/edit from the panel; hint-area toggle; #22's annotation layer. Accepted consequence: the 90-col visibility threshold makes a terminal's output *narrower* when widened across the boundary (sidebar appears); **Amendment 1, 2026-06-12** (issue #25): DC3's focus accent is now a **non-bold accent colour** (`theme.mode_simple`, blue) rather than bold bright-`fg` — bold box-drawing glyphs render as broken/gapped line-art in the asciinema cast player (and are fragile in some terminals), so `panel_border_style` carries no `Modifier::BOLD` on a border (bold stays fine on text spans); pure style change — the text-only Tier-2 snapshots were unaffected, the Tier-1 assertion was updated, and a render-level test now checks the focused border cells carry the accent and no bold
-- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22)** (commits `f879d54`→`2d0f4b2`; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build `/runda` pass that produced 10 tightening findings and a whole-implementation `/runda` pass that returned PASS, no blockers). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating flat black-on-yellow rectangles** (solid fill, **no border glyphs** — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, `2d0f4b2`) **at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing `IndicatorDebounce`). One intentional, user-acknowledged behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, by spec). Final tally **2290 passing / 0 failing / 0 skipped** (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website `casts.mjs` scripts (website-branch follow-up). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) + a flat-rectangle restyle
+- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22)** (commits `f879d54`→`2d0f4b2`; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build `/runda` pass that produced 10 tightening findings and a whole-implementation `/runda` pass that returned PASS, no blockers). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating flat black-on-yellow rectangles** (solid fill, **no border glyphs** — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, `2d0f4b2`) **at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing `IndicatorDebounce`). One intentional, user-acknowledged behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, by spec). Final tally **2290 passing / 0 failing / 0 skipped** (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website `casts.mjs` scripts (website-branch follow-up). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) + a flat-rectangle restyle. **Amendment 1 (2026-06-15):** in demo mode, **`Ctrl-G` aliases F1** (the ADR-0053 hint overlay) and **badges as `[F1]`**, so an `autocast` cast — which can't emit F1's escape sequence — looks identical to a real F1 press; `Ctrl-G` is the only fit (`Ctrl`+digit is unencodable in a legacy terminal, e.g. `Ctrl-1` arrives as a bare `1`), demo-gated so the shipped keymap stays F1-only; also cleaned two stray ``/`` lines from the ADR file.
- [ADR-0048 — `seed` fake-data generation command](0048-seed-fake-data-generation.md) — **Accepted 2026-06-11; Phase 1 + Phase 2 implemented 2026-06-11** (Phase 1 commits `202e25a`→`fbd219b`; design settled with the user across an extended fork dialogue, hardened by a pre-build `/runda` pass (six blockers folded in), a post-implementation `/runda` pass (eight gaps closed — FK/shortid determinism so **D4 holds with no exceptions**, plus six untested ADR decisions), and a Phase-2 pre-build `/runda` pass (which caught the no-date-literal-token reality → the D2 quoted-dates amendment), and a post-implementation `/runda` pass (which added a friendly error for a bounded override on a UNIQUE column — see D2); **2400 tests pass, clippy clean**). Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1**. **Phase 1 shipped:** whole-row `seed [count] [--seed ]` with realistic name-aware generation (the `fake` crate + a type-gated heuristic catalogue, table-context name disambiguation, hand-rolled `product` generator, bounded dates), identifier + constraint uniqueness incl. junction distinct-combos, FK sampling from existing parent rows (empty-parent error), `IN`-CHECK derivation + complex-CHECK advisory, a required-column block guard, `--seed` reproducibility (serial/FK/shortid all deterministic), undo as one batch step, replay as a data write, a capped auto-show preview, the enum/CHECK advisory, and an O(N) single-transaction insert path. **Phase 2 shipped (2026-06-11):** the `set` override clause (D2 — fixed value / pick-list / `as ` / `between` range, **quoted** dates per the D2 amendment, type-aware, override drops the column from the advisory) and the `.` column-fill form (D1 form 2 — an UPDATE over existing rows, refusing PK/autogen targets, empty-table no-op, FK/unique-respecting, one undo step), with the new `KNOWN_GENERATORS` vocabulary (D9), a range `Generator`, full completion/highlight (`HighlightClass::Function`)/validity (`IdentSource::Generators`)/help/pedagogy wiring, and the D13 advisory's Phase-2/3 wording. Further SD2 increments (custom generators, NULL injection, multi-locale, recursive auto-seed) out of scope. Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1** (the other being `hint`/**H2**). A dedicated `seed` command (own AST variant + `do_seed` executor, **both modes**) generating **realistic, name-aware** fake data. Two forms: **`seed [count]`** (new rows, default **20**, capped) and **`seed .`** (fill a column on existing rows, an UPDATE). Generation adds the **`fake` crate** (v5, English) driven by a **type-gated, token-matched name-heuristic catalogue** (~30 patterns, documented false-positive guards), with **table-context** disambiguating the `name`/`title` family (`products.name`→product, `users.name`→person, `vendors.name`→company), a **hand-rolled `product` generator** (`fake` has no commerce module), **bounded dates** (`date`/`timestamp`/`dob`/`*_at` recognised, recent windows — never "all of history"), the **identifier family** (`id`/`code`/`ref`/`number`, non-FK/non-PK) → **unique sequential**, and **enum-ish names** (`role`/`status`/`type`/…) left generic + a **post-seed Hint advisory** pointing at `set … in (…)`. A **`set` override clause** — `= value` / `in (a,b,c)` / `as ` / `between a and b` (numeric **and** date), reusing ADR-0026 operators — answers the heuristic-miss case. **`--seed `** makes runs reproducible (and enables exact-value tests). **FK** columns sampled uniformly from existing parent rows (**empty parent → friendly error**, no recursion v1); **junction/compound-PK** tables seeded with **distinct combinations**, capped + noted (SD1). A **required-column block guard** refuses rather than NULL-violate a `NOT NULL` column it can't fill (e.g. `NOT NULL blob`). Full ambient wiring (completion incl. a new generator-name vocabulary highlighted as `tok_function`, hints, `help seed`, ADR-0042 near-miss matrix, ADR-0027 validity); **no DSL→SQL teaching echo** (seed is a utility command, not a SQL twin). Honours **X5** — `do_seed` reuses insert/update *mechanics as helpers*, not by emitting `Command::Insert`. Implementation phased: (1) core whole-row seed → (2) `set` overrides → (3) column-fill. Deferred (future SD2): recursive auto-seed, NULL injection, multi-locale, user-defined custom generators, full per-column report. **Amendment 1, 2026-06-12** (issues #33/#34): two additive D7 catalogue rules — **year-as-int** (`year`/`*_year`/`published`/`founded` → a bounded `int` year, 1950–2025, or the `dob`-style birth window 1945–2007 for `birth`/`born`/`dob`; fixes nonsense like `9419`; `int`-gated, after the quantity rule so `year_count` stays a count; two new `YearRecent`/`YearBirth` generators, *not* added to the D9 vocabulary) and **conventional choice sets** (`priority`/`prio`, `severity`, `rating`/`stars` → type-gated built-in `PickFrom` value sets reusing the existing generator; `priority` leaves `ENUM_TOKENS`). `status` is **deliberately excluded** (user-confirmed — values too domain-specific; keeps the D12 "don't guess" + advisory); a user `IN`-CHECK still wins. Website `seed` cast re-record tracked on the `website` branch
- [ADR-0049 — Input-field readline keymap: Esc-clear + Ctrl-A/E/W/K/U (I1b)](0049-input-field-readline-keymap.md) — **Accepted + implemented 2026-06-12 (issue #29)**, closes Gitea **#29** and the deferred **I1b** readline requirement. **Amends ADR-0046**, which listed "readline shortcuts (I1b)" as out-of-scope — that item is now in scope and decided here; orthogonal to ADR-0003's input-*mode* model and extends the I1a single-line cursor editing already shipped. Binds, in the input field (non-modal, non-nav, both modes): **`Esc`** clears a partly-typed command (empty buffer, cursor→0, scroll→0); **`Ctrl-A`/`Ctrl-E`** alias Home/End (line start/end); **`Ctrl-W`** deletes the previous word (readline-style — eats trailing whitespace then the preceding non-whitespace run, UTF-8-safe on char boundaries, only back to the cursor); **`Ctrl-K`** kills to end of line; **`Ctrl-U`** kills to start. **Esc precedence:** a live Tab-completion memo still wins (Esc undoes the completion first, ADR-0022; Esc clears only when no memo) — Esc-once backs out the completion, Esc-again clears. Forks all user-chosen: **single-Esc-clears** (not double-Esc — discoverable over accident-proof; an unsubmitted draft can be lost, a submitted line is always in history); the **full I1b set** (not just the issue's literal Ctrl-A/E + Esc); a **new ADR** (not an ADR-0046 amendment / no-ADR). Cursor-only keys (Ctrl-A/E) leave history navigation intact like Home/End; buffer-mutating keys (Esc-clear, Ctrl-W/K/U) end it like Backspace. Helpers `clear_input`/`delete_prev_word`/`kill_to_end`/`kill_to_start` in `src/app.rs`; **22 new Tier-1 tests, 2458 pass / 0 fail / 0 skip (1 ignored), clippy clean**. OOS: on-screen keybinding hints (issue #27 owns surfacing per-focus keybindings in the bottom status line — this ADR makes the keys *work*, #27 makes them *discoverable*); demo-mode badges for the new chords (ADR-0047 follow-up — Esc already badges `[ESC]`, the glyph-less Ctrl-chords are flagged but not added); multi-line input (I1); word-wise cursor motion (Alt-B/F) / transpose / yank
- [ADR-0050 — Incidental-DDL confirmations omit relationship info (structure-only)](0050-incidental-ddl-confirmations-omit-relationships.md) — **Accepted + implemented 2026-06-12 (issue #28)**, closes Gitea **#28**. **Supersedes** the incidental-DDL clause of **ADR-0044 §1** and the relationship-block half of **ADR-0016 §5**. Incidental-DDL confirmation echoes (`create table`, `add`/`drop`/`rename`/`change column`, `add`/`drop index`) now render **structure only** — header + column box + `Indexes:` + constraints — with **no `References:` / `Referenced by:` block** (neither prose nor diagram), even when the table carries relationships the user did not touch. Rationale (owner): a confirmation echo reports the change just made, not untouched relationships; ADR-0044's terse prose was the lesser of "prose vs diagram", but the right answer for these surfaces is **neither**. **Relationship-subject surfaces are unchanged** — `show table`, `add`/`drop relationship`, `show relationship` still render ADR-0044 diagrams; relationships appear only when the user asks for (`show table`) or acts on (`add`/`drop relationship`) one, and are one `show table ` away — **no information lost**. Forks both user-chosen: **scope = all incidental DDL** (not just `add column` — the rationale is uniform, the mental model clean, and it's the simpler edit) and **delete the prose renderer** (not retain it dormant — no dead code). **Mechanism:** the `handle_dsl_success` `matches!` routing is unchanged (relationship-subject → diagrams; else → `render_structure`); the change is one line inside `render_structure` (`output_render.rs` — drop the relationship-block call) since all its callers are incidental DDL, plus deletion of the orphaned `relationship_prose_lines` + `cols_disp` helpers. The prose format survives in ADR-0016 §5 + git history for a future OOS-7 always-prose setting. **Tests:** the prose-presence unit test + its snapshot removed; a new unit test asserts `render_structure` on a description carrying **both** inbound and outbound relationships emits the box but no prose; the misnamed `add_relationship_flow_shows_inbound_section_on_parent` integration test (which sent an `AddColumn`) inverted + renamed to assert the add-column echo omits the prose; the diagram tests (`show table`, `add relationship`) unaffected. **2458 pass / 0 fail / 0 skip (1 ignored), clippy clean**. `requirements.md` unaffected (ADR-tracked refinement of a decided area, like ADR-0044 itself)
diff --git a/docs/handoff/20260615-handoff-72.md b/docs/handoff/20260615-handoff-72.md
new file mode 100644
index 0000000..c90b5be
--- /dev/null
+++ b/docs/handoff/20260615-handoff-72.md
@@ -0,0 +1,113 @@
+# Session handoff — 2026-06-15 (72)
+
+Short, focused handover. Continues from handoff-71, which asked the next
+session to run a **systematic semantic verification pass over every
+`hint` block** (handoff-70 shipped H2 / ADR-0053, but a user smoke-test
+found a wrong hint and implicated the whole corpus). **That pass is now
+done.** Four content errors fixed, a durable parse-guard added, two stale
+docs corrected. Commit `5a37437`.
+
+## §1. State
+
+**Branch:** `main`, clean, all committed (local; **push pending** — your
+step). **2500 pass / 0 fail / 1 ignored** (the long-standing `friendly`
+doctest), **clippy clean** (nursery, all targets). The +1 vs handoff-71's
+2499 is the new guard test. Open Gitea issues unchanged: **#35–#38**.
+
+## §2. The verification pass (commit `5a37437`)
+
+Method: cross-checked every `hint.cmd.*` example against its
+`parse.usage.*` template, ground-truthed every concept claim against the
+authoritative ADR **and a named existing test** (not grep+extrapolation —
+the trap handoff-71 §3 warned about), and parse-validated all 49 command
+examples via a new guard.
+
+### Four content errors fixed (`src/friendly/strings/en-US.yaml`)
+
+| Block | Bug | Fix |
+|---|---|---|
+| `cmd.create_table` | Example `with pk id(serial), name(text), email(text)` declares a **3-column compound PK**, not a PK + regular columns. Every `with pk` column is a key member — confirmed by the grammar test comment *"Every `create table` column is a primary-key column"* (`ddl.rs`), ADR-0005. | Single-column PK + `add column` for the rest; `what`/`concept` aligned. |
+| `cmd.save` | `save as my-shop` **does not parse** — `build_save` yields `AppCommand::SaveAs` with **no inline name**; `save as` opens a path-entry modal (`iteration4b` tests). | Example → `save as`; `what` de-implied; added an accurate temp-vs-named-auto-save `concept`. |
+| `cmd.import` | Target `shop-copy` **does not parse** — the `as ` slot is an `IdentSource::NewName` ident that tokenises only up to the hyphen. (The zip path is a BarePath and *does* accept hyphens, hence `export my-shop.zip` is fine.) | → `shop_copy`. |
+| `err.foreign_key.child_side.concept` | Offered `on delete set null/cascade` as the remedy — but `error_hint_class` maps child_side to **insert/update** violations; `on delete` governs the **parent** direction. The tier-1 hint (line 64) correctly omits it. | Corrected: parent must exist first; clarified `on delete` is the *other* direction. |
+
+### Durable guard added
+
+`every_cmd_hint_example_parses_in_its_mode` (`src/dsl/grammar/mod.rs`,
+in the `hint_key_tests` module). **Catalog-driven** — it iterates
+`catalog().keys()` for `hint.cmd.*.example` rather than the REGISTRY, so
+an orphaned/mis-keyed block can't slip past; floor-asserts ≥49 examples.
+Each parses in its taught mode (advanced for the SQL surface, simple
+otherwise). It caught the `save` and `import` errors **test-first** (red
+before the YAML fix). Registered the new `hint.cmd.save.concept` key in
+`keys.rs` (the `keys_validate_against_catalog` test requires every catalog
+key be declared).
+
+### Verified correct (not changed)
+
+All other `cmd`/`err` blocks. Notably the guard-*concept* claims were each
+confirmed against a named runtime test, not assumed:
+`drop_column_refuses_primary_key` / `…_column_in_a_relationship`,
+`drop_table_with_inbound_relationship_errors`,
+`add_not_null_column_without_default_to_populated_table_is_refused`. The
+corrected `create_table` story stays coherent with the `Customers`-
+referencing examples (id serial PK → `add column` name/email → `insert`
+skips the auto id).
+
+## §3. Docs corrected (same commit)
+
+Discovered while verifying `create_m2n` (which **is** implemented —
+`db.rs::do_create_m2n_relationship` + `tests/it/m2n.rs`):
+
+- **CLAUDE.md** carried two **stale "deferred" claims**, both already
+ implemented. Removed/updated: (a) the at-a-glance project-format line
+ said export/import (Iter 5) + `--resume`/input-history/migration (Iter
+ 6) were "pending" — all `[x]` in `requirements.md` (ADR-0015); (b) the
+ "Things deliberately deferred" list still had the **m:n convenience
+ (C4)** bullet and the same project-storage bullet. `requirements.md`
+ was already correct (C4 done 2026-06-10, ADR-0045), so only a
+ verification-pass note was appended to its **H2** entry.
+
+## §4. Scope note — what the guard does *not* do
+
+The bug class here is **semantic** (an example that parses and runs but
+misrepresents the prose — e.g. `create_table`). The guard enforces only
+the **syntactic floor**: examples parse in their mode. It backstops
+future typos/clause-drift but cannot police meaning. Semantic correctness
+of the current corpus rests on this session's review (recorded in the
+commit + requirements.md H2). A stronger-but-brittler option was offered
+to the user and **not built pending their call**: per-form assertions
+that each example resolves to the *expected command shape* (e.g.
+create_table → single-column PK). `hint.err.*` examples are fix-recipe
+prose, not runnable, so they're verified by review only — inherent.
+
+## §5. Next session — start here
+
+The hint corpus is now trustworthy. Open roadmap (verify against the CI
+merge first, per handoff-70 §5):
+
+1. **Push** (your step) — this commit + the still-unpushed backlog from
+ handoffs 70/71 (the CI merge + all of H2).
+2. **#35 (cargo fmt gate)** — the natural pairing with the merged CI; the
+ user wanted it done once, before first publication. The tree is **not**
+ fmt-clean (~1800 pre-existing diffs).
+3. Other `requirements.md` open items: **TT4** PTY tier-4 (unwired),
+ **I1** multi-line input, **I5/B3** in-flight cancellation, **V4**
+ session journal (own ADR), **TU1** tutorial system (own ADR).
+4. Hint follow-ups if wanted: **#37** clause-concept hints, **#38**
+ diagnostic route + `diagnostic.*` blocks, **#36** `help` advanced-SQL.
+
+## §6. How to take over
+
+1. Read handoffs 70 → 71 → 72, `CLAUDE.md`, `docs/requirements.md`.
+2. Confirm green: `cargo test` (**2500 / 1 ignored**) + `cargo clippy
+ --all-targets` (clean).
+3. For anything in the `hint` area, read **ADR-0053** first. For the
+ corpus, `src/friendly/strings/en-US.yaml` (`hint.cmd.*` / `hint.err.*`)
+ is the content; the guard in `src/dsl/grammar/mod.rs` is the regression
+ net.
+4. Workflow unchanged: phased, test-first, `/runda` + DA before commits,
+ ADR amendment + README index-upkeep for decided-area changes, confirm
+ commit messages with the user.
+5. Consider a `cargo sweep` at this milestone (`target/` grows; see
+ CLAUDE.md "Build hygiene").
diff --git a/docs/requirements.md b/docs/requirements.md
index a30fc57..f578a1c 100644
--- a/docs/requirements.md
+++ b/docs/requirements.md
@@ -820,7 +820,12 @@ since ADR-0027.)
(`what`/`example`/`concept`) covers every command form + the 9 runtime
error classes, enforced by a comprehensiveness coverage test. Deferred:
the pre-submit-diagnostic route + `diagnostic.*` blocks (#38),
- clause-concept hints (#37).)*
+ clause-concept hints (#37). **Content verified 2026-06-15 (handoff-71):**
+ a semantic pass over every `hint.cmd.*`/`hint.err.*` block fixed four
+ errors — `create_table` (compound-PK misread), `save` (no inline name),
+ `import` (hyphen-rejecting target), and `foreign_key.child_side` (wrong
+ `on delete` remedy) — and added a catalogue-driven guard test that parses
+ every command example in its taught mode.)*
- [x] **H3** `help` provides general reference and per-command
help.
*(Done 2026-06-07: the **general reference** is `help` (no arg) —
diff --git a/src/app.rs b/src/app.rs
index 5750535..1db2815 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -520,10 +520,11 @@ const HISTORY_CAPACITY: usize = 1000;
/// produces a glyph of its own (and so needs no badge) — ADR-0047 D2.
///
/// The set is exactly the *otherwise-invisible* keys: motion, editing,
-/// submission, and the `Ctrl-O` navigation toggle. Plain character keys
-/// already appear on the input line, and `Ctrl-C` (quit) / `Ctrl+]`
-/// (the caption toggle) are deliberately excluded. Pure and total, so
-/// it is exhaustively unit-testable without a running app.
+/// submission, the `Ctrl-O` navigation toggle, and the `Ctrl-G` F1-alias
+/// (ADR-0047 amendment). Plain character keys already appear on the input
+/// line, and `Ctrl-C` (quit) / `Ctrl+]` (the caption toggle) are
+/// deliberately excluded. Pure and total, so it is exhaustively
+/// unit-testable without a running app.
pub const fn demo_badge_label(key: &KeyEvent) -> Option<&'static str> {
match (key.code, key.modifiers) {
(KeyCode::Tab, _) => Some("[TAB]"),
@@ -541,8 +542,12 @@ pub const fn demo_badge_label(key: &KeyEvent) -> Option<&'static str> {
(KeyCode::PageDown, _) => Some("[PGDN]"),
(KeyCode::Backspace, _) => Some("[BKSP]"),
(KeyCode::Delete, _) => Some("[DEL]"),
- // The only badged control chord: the ADR-0046 navigation toggle.
+ // The ADR-0046 navigation toggle.
(KeyCode::Char('o'), m) if m.contains(KeyModifiers::CONTROL) => Some("[CTRL-O]"),
+ // ADR-0047 amendment: the Ctrl-G F1-alias badges AS [F1] so a
+ // cast recorded by a tool that can't send F1 looks identical to a
+ // real F1 press. (Only consulted in demo mode — the caller gates.)
+ (KeyCode::Char('g'), m) if m.contains(KeyModifiers::CONTROL) => Some("[F1]"),
_ => None,
}
}
@@ -1223,7 +1228,18 @@ impl App {
// the memo-clearing completion match below. Non-empty input →
// a hint for the command being typed; empty input → expand on
// the most recent error (or a getting-started pointer).
- if key.code == KeyCode::F(1) {
+ //
+ // ADR-0047 amendment: in demo mode, Ctrl-G is an alias for F1.
+ // The cast recorder (autocast) can't emit F1 (an escape
+ // sequence) but can send the single control byte Ctrl-G; it
+ // badges AS [F1] (see `demo_badge_label`) so the cast is visually
+ // identical to a real F1 press. Demo-gated, so the shipped keymap
+ // stays F1-only.
+ let hint_key = key.code == KeyCode::F(1)
+ || (self.demo_mode
+ && (key.code, key.modifiers)
+ == (KeyCode::Char('g'), KeyModifiers::CONTROL));
+ if hint_key {
if self.input.trim().is_empty() {
self.note_hint_for_recent_error();
} else {
@@ -3459,6 +3475,13 @@ mod tests {
demo_badge_label(&ke(KeyCode::Char('o'), KeyModifiers::CONTROL)),
Some("[CTRL-O]")
);
+ // ADR-0047 amendment: the Ctrl-G F1-alias badges AS [F1], so a
+ // cast recorded with autocast (which can't send F1) is visually
+ // identical to a real F1 press.
+ assert_eq!(
+ demo_badge_label(&ke(KeyCode::Char('g'), KeyModifiers::CONTROL)),
+ Some("[F1]")
+ );
}
#[test]
@@ -5756,6 +5779,56 @@ mod tests {
assert_eq!(app.input, input, "F1 must not change the buffer");
}
+ // ── ADR-0047 amendment: Ctrl-G is a demo-mode alias for F1 ──────
+ // The cast recorder (autocast) cannot emit F1 — an escape sequence —
+ // but Ctrl-G is a single legacy control byte it can send. Demo-gated
+ // so the real keymap stays F1-only, and badged as [F1] (see
+ // `demo_badge_label`) so a recorded cast looks identical to a genuine
+ // F1 press.
+
+ fn ctrl_g() -> AppEvent {
+ key_mod(KeyCode::Char('g'), KeyModifiers::CONTROL)
+ }
+
+ #[test]
+ fn ctrl_g_in_demo_mode_aliases_f1_on_input() {
+ let mut app = App::new();
+ app.demo_mode = true;
+ type_str(&mut app, "insert into T");
+ let input = app.input.clone();
+ let before = app.output.len();
+ app.update(ctrl_g());
+ assert_eq!(app.input, input, "Ctrl-G must not change the buffer (no `g` typed)");
+ assert!(app.output.len() > before, "Ctrl-G must emit the same hint F1 does");
+ }
+
+ #[test]
+ fn ctrl_g_in_demo_mode_aliases_f1_on_empty_input() {
+ let mut app = App::new();
+ app.demo_mode = true;
+ let before = app.output.len();
+ app.update(ctrl_g());
+ assert!(
+ app.output.len() > before,
+ "Ctrl-G on empty input emits the getting-started hint"
+ );
+ assert!(output_contains(&app, "press F1"));
+ }
+
+ #[test]
+ fn ctrl_g_outside_demo_mode_is_inert() {
+ // Not in demo mode: Ctrl-G is neither the hint alias nor a typed
+ // glyph (the `Char(c)` insert arm excludes CONTROL), so it falls
+ // through to the inert catch-all — no `g`, no hint.
+ let mut app = App::new();
+ type_str(&mut app, "insert");
+ let input = app.input.clone();
+ let before = app.output.len();
+ app.update(ctrl_g());
+ assert_eq!(app.input, input, "Ctrl-G must not insert a `g`");
+ assert_eq!(app.output.len(), before, "Ctrl-G does nothing when demo mode is off");
+ }
+
#[test]
fn dsl_failure_sets_hint_class_and_a_later_dsl_command_clears_it() {
let mut app = App::new();
diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs
index 3842073..337b039 100644
--- a/src/dsl/grammar/mod.rs
+++ b/src/dsl/grammar/mod.rs
@@ -1012,6 +1012,75 @@ mod hint_key_tests {
assert!(cat.get(&key).is_some(), "missing tier-3 error block `{key}`");
}
}
+
+ /// Semantic-verification guard (handoff-71): every `hint.cmd.