Bring main's latest (ADRs 0049-0053 + their features) onto the CI branch so the gate runs against current main before CI lands on main. Clean merge — ci and main touched disjoint files.
This commit is contained in:
@@ -213,6 +213,14 @@ working copy.
|
||||
|
||||
### 6. Persistence ordering
|
||||
|
||||
> **Amended by ADR-0052 (2026-06-13, issue #30):** `history.log` is no
|
||||
> longer written inside the worker transaction. It is a *journal* of typed
|
||||
> commands, not state, so success journaling moved to the dispatch layer
|
||||
> (next to the already-top-level failure journaling); `commit-db-last` now
|
||||
> governs the three **state** targets only (db + `project.yaml` +
|
||||
> `data/*.csv`), which still commit atomically in the worker. The journal
|
||||
> write is best-effort (amends ADR-0040).
|
||||
|
||||
A successful user command produces effects in four targets:
|
||||
the SQLite database, `project.yaml`, the relevant
|
||||
`data/<table>.csv` file(s), and `history.log`. INV-2 from the
|
||||
|
||||
@@ -197,6 +197,16 @@ Referenced by:
|
||||
The relationship sections retain today's plain-text format
|
||||
to leave room for the future relationship-rendering ADR.
|
||||
|
||||
> **Superseded.** ADR-0044 replaced this prose block with compact
|
||||
> diagrams on relationship-subject surfaces (`show table`,
|
||||
> `add`/`drop relationship`). **ADR-0050 (2026-06-12, issue #28)** then
|
||||
> removed the relationship block entirely from incidental-DDL structure
|
||||
> echoes (`create table`, `add`/`drop`/`rename`/`change column`,
|
||||
> `add`/`drop index`) — those render structure only — and **deleted the
|
||||
> prose renderer**. The `References:` / `Referenced by:` format above is
|
||||
> retained here as documentation/provenance should the OOS-7
|
||||
> always-prose display setting ever be built.
|
||||
|
||||
### 6. Theme integration
|
||||
|
||||
Theme colors apply to the box-drawing characters via the
|
||||
|
||||
@@ -772,6 +772,58 @@ invalid_ident_does_not_fire_for_column_prefix_at_sql_expr_slot}`;
|
||||
`theme::function_colour_is_distinct_from_keyword_identifier_and_type`.
|
||||
See ADR-0031's status note for the grammar-side anchor.
|
||||
|
||||
## Amendment 7 — optional positional args reach the hint panel (2026-06-12)
|
||||
|
||||
Issue #26. At `seed <table> ▮` the hint panel showed only the
|
||||
`set` / `--seed` continuation chips and never mentioned the
|
||||
**optional row count** — even though a count (`seed users 50`) is
|
||||
the most common next move. The count is a bare positional
|
||||
`NumberLit` with no keyword/candidate text, so the candidate ladder
|
||||
can't surface it; and `seed <table>` is already a *complete*
|
||||
command, so the hint resolver short-circuits (empty expected set).
|
||||
|
||||
The existing `IntroProse` `HintMode` (ADR-0024 §HintMode-per-node;
|
||||
issue #4's CREATE-TABLE element hint) is the right tool — it shows
|
||||
prose that *introduces* a position whose first-class move has no
|
||||
candidate, with the keyword alternatives folded into the prose and
|
||||
Tab still cycling them. But it did not reach this position: a
|
||||
`Node::Hinted`'s mode lives in `pending_hint_mode`, which the very
|
||||
next match clears — including the **empty** match of a skipped
|
||||
`Optional`. The CREATE-TABLE element survives only because it sits
|
||||
in a *required* `Repeated(min:1)`; an optional positional followed
|
||||
by more optionals (the seed count) is cleared before the resolver
|
||||
reads it.
|
||||
|
||||
### Mechanism
|
||||
|
||||
A small, general carry: when `walk_optional` skips its inner (the
|
||||
inner didn't engage), it stashes any `IntroProse` key the inner
|
||||
left in `pending_hint_mode` into a new `WalkContext` field,
|
||||
`surviving_intro_hint: Option<(key, position)>`, **before** the
|
||||
empty match clears `pending_hint_mode`. The trailing optionals,
|
||||
which are not `IntroProse`, don't overwrite it. The hint snapshot
|
||||
keeps the key **only when `position == cursor`** (the slice end),
|
||||
so it shows while the cursor sits at the count slot but not once a
|
||||
later clause (`set …`) consumes input past it, nor once the count
|
||||
itself is supplied. The resolver returns that `IntroProse` even for
|
||||
an otherwise-complete command (ahead of the empty-expected
|
||||
short-circuit).
|
||||
|
||||
The seed grammar wraps the count in
|
||||
`Hinted { IntroProse("hint.seed_count"), NumberLit }`; the prose
|
||||
names the count (with its default 20) plus the `.column`
|
||||
column-fill form and the `set` / `--seed` keywords (user-chosen
|
||||
scope: mention every option). Only `IntroProse` is carried —
|
||||
`ProseOnly` / `ForceProse` mark *active* slots and reach the
|
||||
resolver through the normal path, unchanged. The CREATE-TABLE
|
||||
element (in a `Repeated`, not an `Optional`) is untouched.
|
||||
|
||||
This is a refinement of ADR-0024 §HintMode-per-node and a sibling
|
||||
of issue #4; no `AmbientHint` / renderer change. Covered by
|
||||
`input_render::{seed_count_is_advertised_at_the_optional_position,
|
||||
seed_count_hint_does_not_leak_once_the_count_or_a_clause_is_given,
|
||||
seed_count_hint_also_fires_after_a_column_fill_target}`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Deliberately deferred to keep this ADR shippable as a single
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
Accepted. **Amended by ADR-0052 (2026-06-13, issue #30):** the status
|
||||
field gains an optional `:adv` mode suffix (`ok:adv` / `err:adv`) — the
|
||||
"non-breaking future extension" this ADR reserved — and **success
|
||||
journaling moves out of the worker to the dispatch layer**
|
||||
(`spawn_dsl_dispatch` / `run_replay` / app-command sites), next to the
|
||||
failure path, where the submission mode is in scope. `status_is_ok` keys
|
||||
off the base token, so `ok:adv` replays like `ok`.
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
**Accepted** — 2026-05-30 (issue #9). Amends the output conventions of
|
||||
ADR-0014 (data operations), ADR-0028 (query plans / `explain`), and
|
||||
ADR-0019 (failure rendering); builds on ADR-0037's mode-tagged echo
|
||||
line.
|
||||
line. **Amended by ADR-0052 (2026-06-13, issue #30):** a `history.log`
|
||||
*journal*-write failure on a **successful** command is no longer fatal —
|
||||
journaling moved to the dispatch layer (after the db commit), so it is
|
||||
best-effort (logged + ignored), consistent with the failure-journal path.
|
||||
State-write failures (yaml/csv/db) remain fatal.
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -103,6 +103,10 @@ Prose-retained surfaces (**unchanged** from ADR-0016 §5):
|
||||
`add`/`drop index` — keep the terse `References:` /
|
||||
`Referenced by:` prose. A simple `add column` on a heavily-related
|
||||
table should not print a wall of diagrams.
|
||||
*(**Superseded 2026-06-12 by ADR-0050** (issue #28): these incidental
|
||||
DDL echoes now render **structure only** — no relationship block at
|
||||
all, neither prose nor diagram. The prose renderer was deleted. The
|
||||
diagram surfaces below are unchanged.)*
|
||||
|
||||
So this **partially supersedes ADR-0016 §5**: the prose block is
|
||||
replaced by diagrams on the relationship-subject surfaces and
|
||||
|
||||
@@ -525,7 +525,9 @@ All tiers green, zero skips; clippy clean (nursery).
|
||||
submits over a multi-logical-line buffer. DA3/DA4 keep a single
|
||||
logical line; this remains a separate, deferred feature.
|
||||
- **Readline shortcuts (I1b)** — Ctrl-A/E/W/K/U stay reserved-deferred;
|
||||
not touched here.
|
||||
not touched here. *(Superseded 2026-06-12: I1b is now in scope and
|
||||
decided by **ADR-0049** — Esc-clear + Ctrl-A/E/W/K/U in the input
|
||||
field, issue #29.)*
|
||||
- **Cross-session sidebar persistence** — visibility is session-only
|
||||
(DB1); persisting it would amend ADR-0015.
|
||||
- **The output panel as a third navigation focus target** — navigation
|
||||
@@ -554,3 +556,27 @@ All tiers green, zero skips; clippy clean (nursery).
|
||||
and is accepted: 90 is the screencast width, real terminals sit well
|
||||
to one side of it, and `Ctrl-O` peek covers the in-between case. The
|
||||
`90` threshold is a tunable constant.
|
||||
|
||||
## Amendment 1 — focus accent is a colour, not bold (2026-06-12)
|
||||
|
||||
Issue #25. DC3's "accent border" on the focused sidebar panel was
|
||||
first implemented as bright `theme.fg` **plus `Modifier::BOLD`** on
|
||||
the box-drawing border. Bold box-drawing glyphs render as broken /
|
||||
gapped line-art in the asciinema player used for the website casts
|
||||
(vertical strokes don't connect to the corner glyphs) and are
|
||||
fragile in some terminals.
|
||||
|
||||
**`panel_border_style` now marks focus with a non-bold accent
|
||||
colour — `theme.mode_simple` (blue) — and never `Modifier::BOLD` on
|
||||
a border.** The unfocused border stays muted `theme.border`. This
|
||||
makes the ADR's "accent border (lazygit convention)" wording
|
||||
literal — it is now a true accent hue rather than bold bright-fg —
|
||||
and is what renders cleanly in casts. Bold remains fine on *text*
|
||||
spans (titles, key hints); the constraint is specifically that
|
||||
box-drawing borders carry no bold attribute.
|
||||
|
||||
Note: this is a pure style change. The Tier-2 snapshots are
|
||||
text-only (`render_to_string` captures cell symbols, not styles),
|
||||
so none needed re-accepting; the Tier-1 `panel_border_style`
|
||||
assertion was updated and a render-level test now checks the actual
|
||||
border cells carry the accent colour and no bold.
|
||||
|
||||
@@ -317,6 +317,8 @@ with the implementation):
|
||||
| `url`/`website`/`homepage` · `color`/`colour` | URL / hex colour | text |
|
||||
| `price`/`amount`/`cost`/`salary`/`balance`/`total` | currency-range number | numeric |
|
||||
| `age` · `quantity`/`qty`/`stock`/`count` | 18–80 · small int | numeric |
|
||||
| `year`/`*_year`/`published`/`founded` (Amendment 1) | bounded year (birth window for `birth`/`born`/`dob`, else 1950–2025) | int |
|
||||
| `priority`/`prio` · `severity` · `rating`/`stars` (Amendment 1) | built-in `PickFrom` value set | text/int |
|
||||
| `date`/`*_date` | date, recent ~3 yr window | date |
|
||||
| `dob`/`birthday` | date, adult window (18–80 yr ago) | date |
|
||||
| `timestamp`/`datetime` · `created_at`/`updated_at`/`*_at` | datetime, recent window (`updated_at` ≥ `created_at`) | datetime |
|
||||
@@ -675,3 +677,66 @@ the regression floor.
|
||||
derive-`IN`-else-friendly-fail tier.
|
||||
- **`set`-driven NULL / per-column report / recursive parent seed:**
|
||||
deferred — see Out of scope.
|
||||
|
||||
## Amendment 1 — year-as-int + conventional choice sets (2026-06-12)
|
||||
|
||||
Two SD2-style refinements to the D7 catalogue, surfaced while writing
|
||||
the website `seed` docs. Both are additive name rules; no change to D8
|
||||
(type fallback), the executor, or the grammar.
|
||||
|
||||
### Issue #33 — year-like `int` columns
|
||||
|
||||
A column such as `published` or `birth_year` was just an `int`, so it
|
||||
fell through to the unbounded type-based `int` path (D8) and produced
|
||||
nonsense like `9419` or `1426` — implausible as years, undercutting the
|
||||
"realistic data" pedagogy. Added an **`int`-gated** year rule, placed
|
||||
*after* the quantity rule (so `year_count` stays a count):
|
||||
|
||||
- `year` / `*_year` / `published` / `founded` → **`YearRecent`**, a
|
||||
bounded window of **1950–2025** (75 years relative to the fixed
|
||||
`REF_YEAR`, wide enough for published books / founding years /
|
||||
release years; matches the issue's own `between 1950 and 2020`
|
||||
workaround).
|
||||
- the same with a `birth` / `born` / `dob` token (e.g. `birth_year`) →
|
||||
**`YearBirth`**, mirroring the existing `dob → DateAdult` adult birth
|
||||
window as years (**1945–2007**).
|
||||
|
||||
Both emit a plain `int`. `published` / `founded` are included
|
||||
(user-confirmed): an `int` so named is almost always a year (a flag
|
||||
would be `is_published`). The generators are **not** added to the D9
|
||||
named-generator vocabulary — explicit control stays with `set <col>
|
||||
between <lo> and <hi>`.
|
||||
|
||||
### Issue #34 — built-in value sets for conventional choice names
|
||||
|
||||
D12 deliberately does not guess values for enum-ish names. For a few,
|
||||
though, there is a near-canonical small set that reads far better than
|
||||
lorem text. Added a **type-gated `PickFrom`** lookup (reusing the
|
||||
existing generator — no new machinery), placed ahead of the enum-ish
|
||||
fallthrough:
|
||||
|
||||
| Name (tokens) | text | int |
|
||||
|---|---|---|
|
||||
| `priority` / `prio` | `low`/`medium`/`high` | `1`/`2`/`3` |
|
||||
| `severity` | `low`/`medium`/`high`/`critical` | `1`/`2`/`3`/`4` |
|
||||
| `rating` / `stars` | — | `1`–`5` |
|
||||
|
||||
A user-declared `IN`-CHECK (D17) still wins — it is resolved before the
|
||||
heuristics. Any name that gains a set is **removed from the enum-ish
|
||||
advisory trigger** (`priority` left `ENUM_TOKENS`); since the advisory
|
||||
(D13) only fires on `Generator::Generic`, a `PickFrom` name is excluded
|
||||
either way, but the removal keeps `is_enum_ish` semantically "names seed
|
||||
still can't guess".
|
||||
|
||||
**`status` is deliberately excluded** (user-confirmed on the issue): its
|
||||
real values are too domain-specific (`active/inactive`,
|
||||
`open/closed/pending`, `draft/published`, …), so it keeps the D12
|
||||
"don't guess" stance — generic text + the advisory pointing at `set
|
||||
status in (…)`. `state` stays its US-state-name generator (D7);
|
||||
`type`/`kind`/`category`/`stage`/`gender` and `size`/`tier`/`plan` were
|
||||
considered and left to the advisory.
|
||||
|
||||
**Website follow-up** (tracked on the `website` branch, not here): the
|
||||
`seed` cast exercises a `tickets` table with `priority`; it should be
|
||||
re-recorded so the table tightens once `priority` collapses to a short
|
||||
value — likely subsumed by the pre-publication cast sweep.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# ADR-0049: Input-field readline keymap — Esc-clear + Ctrl-A/E/W/K/U (I1b)
|
||||
|
||||
## Status
|
||||
|
||||
**Accepted + implemented 2026-06-12 (issue #29).** Closes Gitea **#29**
|
||||
("Command input keystroke support") and the deferred **I1b** readline
|
||||
requirement in `requirements.md`. Every fork below was escalated to the
|
||||
user and user-chosen before any code was written; implemented test-first
|
||||
(22 new Tier-1 tests in `src/app.rs`, all green; clippy nursery clean).
|
||||
|
||||
This ADR **amends ADR-0046**, which explicitly listed "readline
|
||||
shortcuts (I1b)" in its out-of-scope set: that item is now in scope and
|
||||
decided here. It is orthogonal to ADR-0003's input-*mode* model (simple
|
||||
vs advanced, the `:` sigil) — these are editing keys within the input
|
||||
field, not mode or sigil changes — and it extends the single-line cursor
|
||||
editing already shipped under requirement **I1a** (Left/Right/Home/End/
|
||||
Backspace/Delete, `app.rs`).
|
||||
|
||||
## Context
|
||||
|
||||
The input field already supported in-line cursor editing (I1a): Left/
|
||||
Right by char (UTF-8 aware), Home/End to the extremes, Backspace/Delete.
|
||||
Two gaps remained, raised in issue #29:
|
||||
|
||||
1. No way to **clear a partly-typed command** in one keystroke — a user
|
||||
who started typing the wrong thing had to hold Backspace.
|
||||
2. No **readline cursor/kill shortcuts** (Ctrl-A/Ctrl-E and friends) for
|
||||
keyboards without Home/End and for muscle-memory in a command-driven
|
||||
workflow. This is requirement I1b, deferred by ADR-0046.
|
||||
|
||||
`Esc` was free in the input field except that a *live Tab-completion
|
||||
memo* consumes it first (to undo the completion in one keystroke,
|
||||
ADR-0022). Ctrl-A/E/W/K/U were unbound. The existing chords are Ctrl-C
|
||||
(quit), Ctrl-O (nav focus cycle, ADR-0046), and Ctrl-`]` (demo caption
|
||||
toggle, ADR-0047) — none collide with a/e/w/k/u.
|
||||
|
||||
## Decision
|
||||
|
||||
Bind the following in the input field (non-modal, non-navigation,
|
||||
both input modes), in `App::handle_key`:
|
||||
|
||||
| Key | Action |
|
||||
|-----------|---------------------------------------------------|
|
||||
| `Esc` | Clear the input (empty buffer, cursor→0, scroll→0)|
|
||||
| `Ctrl-A` | Cursor to line start (alias of Home) |
|
||||
| `Ctrl-E` | Cursor to line end (alias of End) |
|
||||
| `Ctrl-W` | Delete the word before the cursor |
|
||||
| `Ctrl-K` | Kill from the cursor to end of line |
|
||||
| `Ctrl-U` | Kill from start of line to the cursor |
|
||||
|
||||
Behavioural rules:
|
||||
|
||||
- **Esc precedence.** A live completion memo still wins: the first Esc
|
||||
undoes the completion (ADR-0022), and Esc only *clears* when no memo
|
||||
is alive. This is a natural progression — Esc once to back out the
|
||||
completion, Esc again to clear.
|
||||
- **Esc does not clear while navigating the sidebar.** When a sidebar
|
||||
panel is focused (Ctrl-O, ADR-0046 DC3), `handle_key` routes every
|
||||
key to the navigation handler *before* the input-field keymap, where
|
||||
Esc exits navigation mode (`nav_exit`). Entering nav mode never
|
||||
touched the input buffer, so Esc-to-close-the-panel returns focus to
|
||||
the input with the partly-typed command intact — it cannot reach the
|
||||
clear binding. Locked by a regression test.
|
||||
- **Single Esc clears** (user-chosen over double-Esc). Discoverable and
|
||||
fast; the trade-off (an accidental Esc wipes an unsubmitted line) was
|
||||
accepted. A submitted line is always recoverable from history; only
|
||||
*unsubmitted* draft text is lost.
|
||||
- **Cursor-only keys don't touch history navigation.** Ctrl-A/Ctrl-E,
|
||||
like Home/End, move the cursor without ending history recall.
|
||||
- **Buffer-mutating keys end history navigation.** Esc-clear and
|
||||
Ctrl-W/K/U call `cancel_history_navigation` (the cleared/edited line
|
||||
*is* the new draft), matching Backspace/Delete.
|
||||
- **Ctrl-W is readline-style and UTF-8 safe.** It eats any run of
|
||||
trailing whitespace, then the preceding run of non-whitespace; word
|
||||
boundaries are found on char boundaries so multi-byte words delete
|
||||
cleanly. It only ever deletes back to the cursor (a mid-line Ctrl-W
|
||||
leaves the suffix intact).
|
||||
|
||||
Helpers added: `clear_input`, `delete_prev_word`, `kill_to_end`,
|
||||
`kill_to_start` (`src/app.rs`), mirroring the existing `cursor_left` /
|
||||
`delete_before_cursor` style.
|
||||
|
||||
## Forks (all user-chosen)
|
||||
|
||||
- **Esc semantics:** single-Esc-clears, *not* double-Esc — discoverable
|
||||
over accident-proof.
|
||||
- **Scope:** the *full* I1b set (Esc-clear + Ctrl-A/E/W/K/U), not just
|
||||
the issue's literal Ctrl-A/E + Esc — closes the whole I1b requirement
|
||||
in one pass rather than leaving Ctrl-W/K/U for a follow-up.
|
||||
- **Documentation:** a new ADR (this one), recording the input-field
|
||||
keymap convention and amending ADR-0046's OOS list — over folding it
|
||||
into ADR-0046 or shipping it I1a-style with no ADR.
|
||||
|
||||
## Consequences
|
||||
|
||||
- I1b is complete; `requirements.md` I1b moves to `[x]`.
|
||||
- The new keys are **not yet advertised on screen.** Surfacing per-focus
|
||||
keybindings in the bottom status line is issue #27's domain (a
|
||||
separate, in-design UX change); this ADR makes the keys *work*, #27
|
||||
will make them *discoverable*.
|
||||
- **Demo-mode badges** (ADR-0047) are *not* extended to the new Ctrl-
|
||||
chords here. Esc already badges as `[ESC]`; Ctrl-A/E/W/K/U are
|
||||
glyph-less and would be invisible in an asciinema cast. Whether to add
|
||||
`[CTRL-A]`…`[CTRL-U]` badges is left to ADR-0047's scope and flagged
|
||||
as a follow-up — it is a cast-polish concern, not a #29 requirement.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- On-screen keybinding hints for the input field (issue #27).
|
||||
- Demo badges for the new chords (ADR-0047 follow-up; flagged above).
|
||||
- Multi-line input (I1) and its Ctrl-Enter submit — unrelated, still
|
||||
deferred.
|
||||
- Word-wise *cursor motion* (Alt-B/Alt-F) and transpose/yank — not
|
||||
requested; not part of I1b.
|
||||
@@ -0,0 +1,119 @@
|
||||
# ADR-0050: Incidental-DDL confirmations omit relationship info (structure-only)
|
||||
|
||||
## Status
|
||||
|
||||
**Accepted + implemented 2026-06-12 (issue #28).** Closes Gitea **#28**.
|
||||
Both forks below were escalated to the user and user-chosen before any
|
||||
code was written; implemented test-first. **Supersedes** the
|
||||
incidental-DDL clause of **ADR-0044 §1** and the part of **ADR-0016 §5**
|
||||
that placed a relationship block under every structure echo. The
|
||||
diagram behaviour ADR-0044 introduced for relationship-subject surfaces
|
||||
is unchanged.
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0016 §5 rendered a structure box followed by a plain-text
|
||||
`References:` / `Referenced by:` relationship block under **every**
|
||||
structure echo. ADR-0044 §1 split that by surface:
|
||||
|
||||
- **Relationship-subject surfaces** — `show table <T>`,
|
||||
`add 1:n relationship`, `drop relationship`, `show relationship <name>`
|
||||
— render relationships as compact **diagrams** (the user asked for, or
|
||||
acted on, a relationship).
|
||||
- **Incidental DDL auto-shows** — `create table`, `add`/`drop`/`rename`/
|
||||
`change column`, `add`/`drop index` — kept the terse **prose** block,
|
||||
with the rationale *"a simple `add column` on a heavily-related table
|
||||
should not print a wall of diagrams."*
|
||||
|
||||
Issue #28 reconsiders the deeper question ADR-0044 did not ask: should
|
||||
an incidental-DDL confirmation show relationship information **at all**?
|
||||
Owner preference: **no.** A confirmation echo should focus on the change
|
||||
just made — the new / updated structure — not re-print the table's
|
||||
relationships, which the user did not touch. The terse prose was the
|
||||
lesser of "prose vs diagram", but the right answer for these surfaces is
|
||||
**neither**.
|
||||
|
||||
## Decision
|
||||
|
||||
**Incidental-DDL confirmation echoes render the structure only** — the
|
||||
table-name header, the column / type / constraints box, the `Indexes:`
|
||||
section, and the constraint section — with **no relationship section**
|
||||
(neither prose nor diagram).
|
||||
|
||||
- **Scope: all incidental DDL** (user-chosen, over "just `add column`"):
|
||||
`create table`, `add column`, `drop column`, `rename column`,
|
||||
`change column`, `add index`, `drop index`. The rule is uniform — a
|
||||
structural edit confirms structure, never relationships. (For a
|
||||
freshly `create`d table the relationship section was empty anyway; the
|
||||
rule still applies for consistency of the mental model.)
|
||||
- **Relationship-subject surfaces are unchanged.** `show table`,
|
||||
`add`/`drop relationship`, and `show relationship <name>` still render
|
||||
diagrams. Relationships appear **only** when the user asks for them
|
||||
(`show table` / `show relationship`) or acts on one
|
||||
(`add`/`drop relationship`).
|
||||
- **No information is lost.** Anything dropped from an incidental echo is
|
||||
one `show table <T>` away.
|
||||
|
||||
### Mechanism
|
||||
|
||||
The `handle_dsl_success` routing (`app.rs`) is **unchanged**: it still
|
||||
sends relationship-subject commands to the diagram renderer and
|
||||
everything else to `render_structure`. The change is entirely inside
|
||||
`render_structure` (`output_render.rs`): it no longer appends the
|
||||
relationship block — `render_structure` = structure box + indexes +
|
||||
constraints. All of `render_structure`'s callers are incidental DDL
|
||||
(verified), so this single edit covers the whole scope with no
|
||||
per-command branching.
|
||||
|
||||
### Prose renderer disposition
|
||||
|
||||
The orphaned prose renderer (`relationship_prose_lines`, and its
|
||||
sole helper `cols_disp`) is **deleted** (user-chosen, over retaining it
|
||||
dormant). After this change no shipped surface renders the prose form,
|
||||
so keeping it would be dead code. The prose format remains documented in
|
||||
**ADR-0016 §5** and in git history; if ADR-0044's OOS-7 user-configurable
|
||||
"always-prose" display setting is ever built, it re-introduces the ~30
|
||||
lines from that provenance.
|
||||
|
||||
## Forks (all user-chosen)
|
||||
|
||||
- **Scope:** *all incidental DDL*, not just `add column` — the owner's
|
||||
rationale ("confirm the change, not untouched relationships") applies
|
||||
uniformly, gives a clean mental model, and is the simpler edit (remove
|
||||
one call vs a per-command flag).
|
||||
- **Prose renderer:** *delete* it — no dead code — over retaining a
|
||||
public, tested-but-uncalled renderer for the speculative OOS-7 setting.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Incidental confirmations are shorter and on-topic; a heavily-related
|
||||
table no longer prints a relationship wall after `add column`.
|
||||
- One relationship renderer (prose) leaves the codebase; the diagram
|
||||
renderer (ADR-0044) is the only relationship render path that ships.
|
||||
- `requirements.md` is unaffected (this is an ADR-tracked refinement of a
|
||||
decided area, like ADR-0044 itself); the change is cross-referenced
|
||||
from the commit + this ADR.
|
||||
|
||||
## Tests
|
||||
|
||||
- **Unit (`output_render.rs`):** the prose-asserting test
|
||||
`render_structure_with_relationships` (+ its snapshot) is removed; a
|
||||
new test asserts `render_structure` on a description **carrying** both
|
||||
inbound and outbound relationships emits the structure box but **no**
|
||||
`References:` / `Referenced by:` lines. The box/index/constraint tests
|
||||
are unaffected (their descriptions have no relationships).
|
||||
- **Integration (`walking_skeleton.rs`):** the misnamed
|
||||
`add_relationship_flow_shows_inbound_section_on_parent` (which sends an
|
||||
`AddColumn` and asserted the inbound prose) is inverted + renamed to
|
||||
assert the add-column confirmation shows the structure but **omits**
|
||||
the relationship prose.
|
||||
- **Unchanged:** the diagram tests (`show_list.rs` `show table`,
|
||||
`walking_skeleton.rs` `add relationship`) still pass — they already
|
||||
assert prose is absent and diagrams are present.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The diagram form and its per-surface defaults (ADR-0044) — unchanged.
|
||||
- The OOS-7 user-configurable display setting (always-prose / -diagram /
|
||||
auto-by-width) — still a future follow-up; this ADR removes the prose
|
||||
*renderer*, not the *idea* of a prose mode.
|
||||
@@ -0,0 +1,147 @@
|
||||
# ADR-0051: Bottom keybinding strip — context- and state-aware
|
||||
|
||||
## Status
|
||||
|
||||
**Accepted 2026-06-13 (issue #27).** Closes Gitea **#27**. All forks
|
||||
below were escalated to the user and user-chosen before any code was
|
||||
written; to be implemented test-first. Builds on ADR-0046 (nav focus),
|
||||
ADR-0003 (input modes), ADR-0049 (the #29 readline keys this strip now
|
||||
advertises), and ADR-0022 (the Tab-completion memo).
|
||||
|
||||
## Context
|
||||
|
||||
The bottom status line (`render_status_bar`, `ui.rs`) mixed keystrokes
|
||||
with typed-command words: `Enter submit · : advanced once · mode
|
||||
advanced switch · Ctrl-C quit`. That is redundant — the hint panel
|
||||
already teaches `help` and `Enter` when the input is empty — and it is
|
||||
static apart from a three-way mode branch, so it never reflects what the
|
||||
user can actually do *right now* (navigating the sidebar, cycling a
|
||||
completion, browsing history, editing a line).
|
||||
|
||||
Issue #27: repurpose the line as a **keybindings-only** strip that is
|
||||
**context-sensitive to nav focus** and **state-aware of the current
|
||||
transient interaction**, and move mode discovery into the empty-input
|
||||
hint.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. The strip is keybindings-only and state-selected
|
||||
|
||||
A single pure function `status_bar_bindings(app) -> Vec<Binding>`
|
||||
computes the strip from app state; `render_status_bar` is a thin
|
||||
renderer over it (so the binding sets are unit-testable without a
|
||||
Frame). `history_cursor` is private to `App`, so a small
|
||||
`pub fn is_browsing_history(&self) -> bool` accessor exposes the
|
||||
history-navigation predicate; `mode` / `nav_focus` / `last_completion`
|
||||
are already `pub` and `effective_mode()` is a `pub` method. The state is
|
||||
chosen by **priority — first match wins**:
|
||||
|
||||
| Priority | State (predicate) | Strip |
|
||||
|---|---|---|
|
||||
| 1 | **Sidebar focus** (`nav_focus` in a sidebar) | `Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input` |
|
||||
| 2 | **Completion memo live** (`last_completion.is_some()`) | `Tab/Shift-Tab cycle · Esc cancel · Enter run` |
|
||||
| 3 | **History navigation** (`history_cursor.is_some()`) | `↑↓ browse · Esc clear · Enter run` |
|
||||
| 4 | **Editing** (Input focus, input non-empty) | `Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run` |
|
||||
| 5 | **Default** (Input focus, input empty) | `Ctrl-O sidebar · Tab complete · ↑ history · Enter run` |
|
||||
|
||||
Priority order matters: a completion memo or history navigation is a
|
||||
non-empty-input situation, so states 2 and 3 must precede state 4. The
|
||||
sidebar overlay occludes the input entirely (ADR-0046), so state 1 wins
|
||||
outright.
|
||||
|
||||
### 2. Mode discovery moves off the strip, into the empty-input hint
|
||||
|
||||
The typed-command advertisements (`mode advanced` / `mode simple`
|
||||
switch, the `:` one-shot) leave the strip — they are not keystrokes.
|
||||
Mode discovery moves to the **empty-input hint** (`resolve_hint_lines`'s
|
||||
`(None, None)` arm), in **simple mode only**:
|
||||
|
||||
- **Simple:** `… · \`mode advanced\` for SQL`
|
||||
- **Advanced (persistent):** no pointer.
|
||||
|
||||
The pointer omits the verb "type" — the surrounding prompt already
|
||||
implies it (we don't say "type `help`" either). Advanced mode shows
|
||||
**no** pointer (user decision, post-trial): a user who switched into
|
||||
advanced mode knows how they got there, and `help` covers the way back —
|
||||
a "switch back" pointer only reads naturally in the moment right after
|
||||
switching, so it earns its space poorly.
|
||||
|
||||
The one-shot advanced state's old `Backspace cancel one-shot` label is
|
||||
**subsumed** by the editing state (the input is non-empty in one-shot;
|
||||
Esc-clear and Backspace both cancel it). No behaviour is lost — only the
|
||||
dedicated label.
|
||||
|
||||
### 3. Width: no drop machinery; a budget test instead
|
||||
|
||||
The longest strip (state 4, editing) is ≈ **65 display columns**, which
|
||||
fits every supported width (90-col screencasts, 80-col terminals) with
|
||||
margin — so the priority-drop / abbreviation machinery considered would
|
||||
never trigger and is not built (user-confirmed). Ratatui's existing
|
||||
**clip-at-edge** is the trivial fallback for pathologically narrow
|
||||
(< 65-col) terminals. Instead, a **width-budget unit test** pins the
|
||||
longest rendered strip within an 80-col budget, keeping the strip lean
|
||||
*by construction* — a future over-long strip fails the test rather than
|
||||
silently clipping in a cast.
|
||||
|
||||
## Forks (all user-chosen)
|
||||
|
||||
- **Editing state — yes:** when the input has text, surface the #29
|
||||
readline keys (Esc-clear, Ctrl-A/E, Ctrl-W); the strip stays lean
|
||||
(nav/complete/history) when empty. (vs not advertising the #29 keys.)
|
||||
- **`Ctrl-C quit` — omitted** from the strip (vs always shown): quit is
|
||||
a near-universal convention; omitting it keeps the strips lean and
|
||||
matches the issue's sketch.
|
||||
- **Width — budget test, no drop logic** (vs graceful priority-drop /
|
||||
abbreviation): the strips fit at supported widths, so the machinery
|
||||
would be dead weight (user's own observation).
|
||||
|
||||
## Consequences
|
||||
|
||||
- The strip now teaches the keys for the *current* situation; learners
|
||||
see `Tab/Shift-Tab cycle` exactly while cycling, the editing keys
|
||||
exactly while editing, etc.
|
||||
- The #29 readline keys (ADR-0049) gain their on-screen advertisement,
|
||||
closing that ADR's deferred item.
|
||||
- 15 existing full-panel insta snapshots churn (the bottom line — and,
|
||||
on empty-input views, the hint pointer — changes in every one,
|
||||
including the rebuild-confirm modal view, whose modal box is itself
|
||||
unchanged); each diff was reviewed, not blind-accepted.
|
||||
- `requirements.md` is unaffected (an ADR-tracked UI refinement); the
|
||||
change is cross-referenced from the commit + this ADR.
|
||||
|
||||
## Tests
|
||||
|
||||
- **Tier-1 (`ui.rs` unit):** `status_bar_bindings` returns the expected
|
||||
key set for each of the five states (sidebar, completion-live,
|
||||
history-nav, editing, default) — the completion/history states driven
|
||||
through real key events (`update`) so the predicate transitions are
|
||||
exercised, the others by setting `App` fields; plus the width-budget
|
||||
assertion across states. (Per-state coverage is these unit tests, not
|
||||
snapshots — a one-line strip is asserted more precisely by its exact
|
||||
key list than by a full-panel snapshot.)
|
||||
- **Tier-1:** the empty-input hint appends the correct mode pointer in
|
||||
Simple vs Advanced, and does **not** append it when an ambient hint is
|
||||
showing (non-empty input).
|
||||
- **Tier-3 (`walking_skeleton`):** the old `status_bar_lists_quit_and_
|
||||
submit_in_all_modes` (which asserted the pre-ADR strip) is rewritten +
|
||||
renamed to assert the keystroke-only, state-aware strip end-to-end
|
||||
through the real render path (default → editing transition).
|
||||
- **Tier-2 (insta):** the 15 full-panel snapshots re-accepted (each diff
|
||||
reviewed — strip line and/or hint pointer only).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Modal-aware strip.** While a modal is open (load picker, rebuild /
|
||||
undo confirm) it owns the keyboard and carries its own in-box key
|
||||
hints; the bottom strip under a modal computes from input state
|
||||
exactly as it does today (modals render *over* the status bar). This
|
||||
issue does not redesign the modal case — pre-existing behaviour,
|
||||
unchanged and not worsened.
|
||||
- A persistent/togglable help overlay listing *all* keys (the strip is a
|
||||
contextual subset, not a cheatsheet).
|
||||
- Per-key colour theming beyond the existing key/label/separator styles.
|
||||
- Localisation of the new label strings beyond adding catalog entries.
|
||||
- The remaining I1b kill keys' (Ctrl-K/Ctrl-U) advertisement — the
|
||||
editing strip shows the highest-value subset (Esc/Ctrl-A/E/Ctrl-W) to
|
||||
stay within the width budget; Ctrl-K/U remain unadvertised muscle
|
||||
memory.
|
||||
@@ -0,0 +1,250 @@
|
||||
# ADR-0052: Mode-tagged history for cross-mode recall
|
||||
|
||||
## Status
|
||||
|
||||
**Accepted + implemented 2026-06-13 (issue #30).** Closes Gitea **#30** —
|
||||
both the feature ("reuse advanced history commands in simple mode by
|
||||
prepending `:`") and the bug reported in its comment (the `:` one-shot
|
||||
prefix lost across sessions). All forks user-chosen before any code.
|
||||
**Amends ADR-0034** (journal status field gains a `:adv` tag; *journaling
|
||||
moves from the worker to the dispatch layer*), **ADR-0015 §5/§6**
|
||||
(history.log leaves the worker transaction — `commit-db-last` now scopes
|
||||
yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure
|
||||
is best-effort, no longer fatal); references ADR-0003 (the `:` one-shot
|
||||
sigil). Plan: `docs/plans/20260613-issue-30-top-of-chain-journaling.md`
|
||||
(pre-build `/runda`, then a second `/runda` that drove the journaling
|
||||
relocation + the app-command exclusion). **2471 tests pass / 0 fail / 0
|
||||
skip (1 ignored), clippy clean.**
|
||||
|
||||
> **Why journaling moved (the key architectural turn).** The first draft
|
||||
> kept journaling in the worker and threaded the mode down to it (~30-site
|
||||
> plumbing). On review the user asked the right question: why is the
|
||||
> journal written deep in the worker at all, when the failure path already
|
||||
> journals at the top of the chain where command + mode + outcome are all
|
||||
> in scope? It shouldn't — `history.log` is a *journal of typed commands*,
|
||||
> not *state*. So success journaling moved up next to the failure path
|
||||
> (`spawn_dsl_dispatch` / `run_replay` / the app-command sites), the
|
||||
> mode-plumbing dilemma dissolved, and the worker's `finalize_persistence`
|
||||
> now writes only the state sources (yaml/csv). Consequence: the journal
|
||||
> write is best-effort (the command is already committed), consistent with
|
||||
> the failure path — see §5.
|
||||
|
||||
## Context
|
||||
|
||||
The input-history ring and `history.log` carry **no mode information**,
|
||||
which causes two coupled problems:
|
||||
|
||||
1. **Feature gap.** A command typed in advanced mode (`select * from T`)
|
||||
is stored bare. Recalled in simple mode it is not valid DSL → it just
|
||||
errors. There is no way to know it was an advanced (SQL) command and
|
||||
offer it back in a runnable form.
|
||||
|
||||
2. **Bug (issue #30 comment).** A `:`-one-shot advanced command in simple
|
||||
mode recalls correctly **in-session** (the in-memory ring stores the
|
||||
raw `:select 1`), but after quit+resume it comes back **without** the
|
||||
`:` and is unusable. Root cause: the ring stores the raw input
|
||||
(`:select 1`), but the worker journals the **stripped** `effective_input`
|
||||
(`select 1`) — submission strips the `:` before dispatch (ADR-0003) —
|
||||
so the on-disk `source` never carried the `:`, and hydration loses it.
|
||||
|
||||
Both reduce to: **history does not record the submission mode**, and the
|
||||
in-memory and on-disk representations disagree about the `:`.
|
||||
|
||||
## Decision
|
||||
|
||||
Record the **submission mode** per history entry, keep the on-disk
|
||||
`source` **canonical** (stripped — replay is unaffected), and have
|
||||
**recall reconstruct the runnable line** for the current mode.
|
||||
|
||||
### 1. In-memory ring stores the `:`-prefixed runnable form
|
||||
|
||||
`App.history` stays `Vec<String>` — no type change, so the public ring,
|
||||
the `ProjectSwitched` payload, and `seed_history` are untouched. An
|
||||
**advanced** entry is stored in its **simple-mode runnable form**, the
|
||||
`: `-prefixed string (e.g. `: select * from T`); a **simple** entry is
|
||||
stored bare. This is exactly what the in-session one-shot ring already
|
||||
does (`:select 1` recalls as typed) — generalised to *persistent*-advanced
|
||||
commands too, and made reconstructable on hydration. Because a simple
|
||||
DSL command can never begin with `:` (the sole sigil, ADR-0003), a
|
||||
leading `:` unambiguously marks an advanced entry.
|
||||
|
||||
`submit` builds the stored line from the submission: advanced →
|
||||
`": " + effective_input` (the `: ` matches the auto-space the typed
|
||||
one-shot inserts), simple → `effective_input`. This is computed **after**
|
||||
`effective_input` (today `push_history` runs on the raw `trimmed` before
|
||||
stripping; the reorder also drops a bare `:`, which never executed). The
|
||||
draft (`history_draft`) stays a plain `String`. `push_history` itself is
|
||||
unchanged — it still takes one `&str`.
|
||||
|
||||
### 2. Recall strips the `:` for advanced mode
|
||||
|
||||
`history_back` / `history_forward` set `self.input` from the stored
|
||||
string, then strip a leading `:` **iff the current persistent mode is
|
||||
Advanced**:
|
||||
|
||||
```
|
||||
if self.mode == Mode::Advanced && stored.starts_with(':') { stored[1..].trim_start() } else { stored }
|
||||
```
|
||||
|
||||
So an advanced entry recalls as `: select * from T` in **simple** mode
|
||||
(runs via the one-shot escape — the feature, and the cross-session bug
|
||||
fix) and bare `select * from T` in **advanced** mode (runs as SQL). A
|
||||
simple entry recalls bare in either mode (simple DSL already runs in
|
||||
advanced mode — issue #30). In-session and cross-session paths share the
|
||||
same stored form, so they finally agree.
|
||||
|
||||
### 3. On-disk: a mode tag in the status field
|
||||
|
||||
The record stays three pipe-separated fields `<ts>|<status>|<source>`
|
||||
(so `source` remains the last, pipe-tolerant, canonical field — replay
|
||||
reads it unchanged). The **status token** gains an optional `:adv`
|
||||
suffix:
|
||||
|
||||
| Submission | Success | Failure |
|
||||
|---|---|---|
|
||||
| Simple | `ok` | `err` |
|
||||
| Advanced (persistent or one-shot) | `ok:adv` | `err:adv` |
|
||||
|
||||
ADR-0034 §1 already reserved the status field for "additional values …
|
||||
a non-breaking future extension"; this is that extension. The status
|
||||
parser splits the token on `:`: the base (`ok`/`err`) gives replayability
|
||||
(`status_is_ok` ⇔ base == `ok`), the `adv` suffix gives the mode — so an
|
||||
unknown future token degrades to "not ok, simple" rather than mis-parsing.
|
||||
|
||||
### Journaling location: the dispatch layer, not the worker
|
||||
|
||||
Both tags are written **at the dispatch layer**, where command + mode +
|
||||
outcome are all in scope — so the mode needs no plumbing into the worker:
|
||||
|
||||
- **Success:** `spawn_dsl_dispatch`, immediately after
|
||||
`execute_command_typed` returns `Ok`, calls
|
||||
`append_history(source, submission_mode.is_advanced())` (best-effort).
|
||||
`run_replay` does the same per replayed line (tagged simple — replay is
|
||||
mode-agnostic), and the app-command sites (`perform_switch` /
|
||||
`spawn_export` / `spawn_rebuild`) journal **simple** (`advanced = false`
|
||||
— app commands run in any mode, so no `:` on recall; this also avoids a
|
||||
redundant `: undo`).
|
||||
- **Failure:** unchanged location (the App→`JournalFailure`→runtime path,
|
||||
already at the top), now carrying the mode — `JournalFailure` gains
|
||||
`advanced`, and `DslFailed` gains `submission_mode` for the
|
||||
worker-rejection sub-path (the parse-failure sub-path has it in
|
||||
`dispatch_dsl`). `Ok`/`Err` are exclusive, so success-in-spawn and
|
||||
failure-in-App-path never double-journal.
|
||||
|
||||
The worker's `finalize_persistence` and the four no-op-skip / three
|
||||
read-only sites **no longer journal** — they leave the state writes
|
||||
(yaml/csv) in the worker transaction and let the dispatch layer journal
|
||||
the `Ok` outcome.
|
||||
|
||||
### 4. Hydration reconstructs the `:`-prefixed form
|
||||
|
||||
`read_recent_sources` parses each record's status tag and, for an
|
||||
advanced record, **reconstructs** the `: `-prefixed string from the
|
||||
canonical `source` (`format!(": {source}")`); simple records pass through
|
||||
bare. It still returns `Vec<String>`, so `read_history_seed`,
|
||||
`seed_history`, and the `ProjectSwitched` payload are **unchanged**. A
|
||||
hydrated entry is therefore byte-identical to its in-session form, and
|
||||
recall behaves identically.
|
||||
|
||||
### Back-compatibility
|
||||
|
||||
Old `history.log` files have only `ok` / `err` tokens → parsed as
|
||||
`advanced = false` (simple). Their advanced commands stay un-`:`-able on
|
||||
recall — the pre-existing behaviour, not a regression; nothing migrates.
|
||||
`status_is_ok` keys off the base token, so `ok:adv` records replay
|
||||
exactly as `ok` does today (source is canonical either way).
|
||||
|
||||
### Journal write is best-effort (amends ADR-0040)
|
||||
|
||||
Because the journal is now written *after* the worker replies (i.e. after
|
||||
`tx.commit`), a journal-write failure can no longer roll the command back.
|
||||
It is **best-effort** — logged and ignored, exactly like the failure path
|
||||
already is (ADR-0034 §4) — so the two journal paths are finally
|
||||
consistent. State integrity is unchanged: yaml/csv/db still commit
|
||||
atomically in the worker (a *state*-write failure still rolls back and is
|
||||
fatal). The only property given up: on a rare journal-write failure (disk
|
||||
full) a committed command may be missing from `history.log` — not
|
||||
recallable/replayable next session, but the state is correct. User-chosen
|
||||
over keeping journaling coupled in the worker (which would have needed the
|
||||
~30-site mode plumbing). See the plan's §2 for the full trade-off.
|
||||
|
||||
## Forks (user-chosen)
|
||||
|
||||
- **Format = mode tag in the status field** (`ok:adv`/`err:adv`), over a
|
||||
new 4th field (ambiguous with unescaped pipes in old `source`s without
|
||||
a version bump) or a `:`-prefix in `source` (would make `source`
|
||||
non-canonical and force replay to strip it).
|
||||
- **Scope = unified** (bug + feature) over bug-only: one mechanism does
|
||||
both, and keeping `source` canonical for replay needs the mode tag
|
||||
regardless, so bug-only is barely smaller and leaves the main ask open.
|
||||
- **Journaling location = dispatch layer, best-effort** over keeping it
|
||||
worker-coupled-and-fatal (which needed the ~30-site mode plumbing). The
|
||||
user's architectural call (§Status).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Advanced history is reusable in simple mode; the `:` one-shot survives
|
||||
resume. The in-memory and on-disk representations agree.
|
||||
- **Journaling left the worker.** `finalize_persistence` and the
|
||||
no-op-skip / read-only sites no longer journal; success is journalled at
|
||||
the dispatch layer (`spawn_dsl_dispatch` / `run_replay` / app-command
|
||||
sites). The ring stays `Vec<String>`; `seed_history` / `ProjectSwitched`
|
||||
are untouched. The vestigial worker `source` plumbing has since been
|
||||
**fully unwound** (2026-06-14 follow-up): `_source` removed from
|
||||
`finalize_persistence` / `do_rebuild_from_text`; the three read-only
|
||||
`*_request` wrappers inlined and deleted; and — because the cascade ran
|
||||
deeper than first estimated — the now-dead `source` param dropped from
|
||||
the ~30 worker handlers (leaf + composite) that only forwarded it, plus
|
||||
the `source` field removed from the `DescribeTable` / `QueryData` /
|
||||
`RunSelect` requests and the matching `DatabaseHandle` method parameters
|
||||
(the ~164 call-site churn was mostly tests). The only `source` left in
|
||||
the worker is the snapshot/undo label (`snapshot_then` /
|
||||
`stage_pre_mutation` / `begin_batch`), passed at the match-arm level.
|
||||
Purely mechanical, compiler-guided, no behaviour change.
|
||||
- **App commands recall bare.** Because they are dispatched outside the
|
||||
`ExecuteDsl`/spawn path, app commands journal **simple** (`advanced =
|
||||
false`) at their own sites, and `submit` excludes them from the ring's
|
||||
`advanced` flag (`!is_app_command`) — so `mode advanced` / `undo` recall
|
||||
bare and run fine in simple mode, with no redundant `:`.
|
||||
- **Journaling is now uniform (user-confirmed).** The spawn journals on
|
||||
`outcome.is_ok()`, so **every** successful command is recorded — closing
|
||||
a pre-existing gap where `show table` / `show data` / `select` journalled
|
||||
but `show tables`/`show relationships`/`show indexes`, `show relationship
|
||||
<name>`, and `explain` did **not** (their worker arms carried no
|
||||
`source` / no journal call). The new behaviour matches ADR-0034 §1
|
||||
("record every submitted command"); those reads are now recallable and
|
||||
are re-run harmlessly on replay (`explain` never executes; shows produce
|
||||
output, no state change). A DA finding, accepted as the more-correct
|
||||
behaviour over re-adding command-outcome gating to preserve the old
|
||||
inconsistency.
|
||||
- **Replay re-journaling.** When `replay` re-dispatches a line, the
|
||||
re-written record is tagged from how replay dispatched (mode-agnostic →
|
||||
`ok`), so a replayed advanced command may be re-journalled without
|
||||
`:adv`. Replay correctness of execution is unchanged (it already parses
|
||||
mode-agnostically); this only affects the *tag* of the re-written line.
|
||||
Noted; not addressed here (replay's own mode-fidelity is out of scope).
|
||||
|
||||
## Tests
|
||||
|
||||
- **Tier-1 (`app.rs`):** an advanced one-shot / persistent-advanced
|
||||
submission is stored `: `-prefixed; it recalls as `: …` in simple mode
|
||||
and bare in advanced mode; a simple entry recalls bare in both; a bare
|
||||
`:` is not stored; a parse-failure is still recallable; dedup/cap hold.
|
||||
- **Tier-1 (`history.rs`):** the writer emits `ok:adv`/`err:adv`;
|
||||
`read_recent_sources` reconstructs the `: `-prefix for `:adv` records
|
||||
and leaves `ok`/`err` records bare (so old logs read as simple);
|
||||
`status_is_ok` is true for `ok` and `ok:adv`.
|
||||
- **Tier-3 (`iteration6_resume_history` / it):** the headline
|
||||
**regression** — type a `:`-one-shot advanced command, journal +
|
||||
hydrate, and assert it recalls **with** `:` in simple mode (fails on
|
||||
current code); plus a persistent-advanced command round-tripping to a
|
||||
`: …` recall.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Replay re-journaling mode-fidelity (above).
|
||||
- Special-casing app commands to avoid the redundant recall `: `.
|
||||
- Distinguishing one-shot from persistent advanced on recall (both are
|
||||
simply "advanced" — the `:` is what simple mode needs either way).
|
||||
- A format version marker / pipe-escaping in `source` (unneeded — the
|
||||
status-tag approach keeps `source` last and canonical).
|
||||
@@ -0,0 +1,404 @@
|
||||
# ADR-0053: Contextual `hint` — F1 live-input keybinding + `hint` command, with a tier-3 teaching corpus (H2)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — implementation in progress. Revised after a `/runda` review
|
||||
(2026-06-14): corrected the verbosity-default fact; re-keyed tier-3
|
||||
content off `help_id`; split the pre-submit-diagnostic and runtime-error
|
||||
paths; added a comprehensiveness coverage test. Revised again during
|
||||
Phase B implementation (2026-06-15): the first exemplar showed per-*node*
|
||||
keying is too coarse for multi-form commands (`add`/`drop`/`show`/
|
||||
`create`), so D3 now keys tier-3 content **per form** via a
|
||||
`hint_ids: &[&str]` array mirroring `usage_ids` — and **clause-concept
|
||||
hints** are recorded as a deferred extension (separate tracking issue).
|
||||
The parallel question of whether the in-app `help` command should
|
||||
likewise distinguish advanced-SQL forms is tracked **separately** as
|
||||
Gitea issue #36 (it touches shipped, ADR-backed `help` behaviour).
|
||||
|
||||
Decided in conversation 2026-06-14. Closes the last open piece of **A1**
|
||||
(the canonical app-command set, ADR-0003): every app command is
|
||||
implemented except `hint`, which ADR-0003's command table listed as
|
||||
*"Request a hint for the current input (ADR pending)."* This ADR is that
|
||||
pending decision. Tracked as **H2** in `docs/requirements.md`.
|
||||
|
||||
References ADR-0003 (app-command set + the `:` escape), ADR-0019 (the
|
||||
friendly error layer / H1), ADR-0021 (per-command usage templates / H1a),
|
||||
ADR-0022 (ambient typing assistance — colour + hint panel + completion),
|
||||
ADR-0027 (input validity indicator), ADR-0046 (sidebar navigation +
|
||||
responsive input hint), ADR-0049 (input-field readline keymap), and
|
||||
ADR-0051 (context/state-aware keybinding strip).
|
||||
|
||||
## Context
|
||||
|
||||
`hint` is the only unbuilt app command. The naive reading — "show a hint" —
|
||||
hides a real subtlety, and a real cost.
|
||||
|
||||
**The subtlety: a submitted `hint` command cannot see live input.** App
|
||||
commands are submitted with Enter, which empties the input buffer. By the
|
||||
time `hint` dispatches, the partial command it was meant to help with is
|
||||
gone. So "a hint for the current input" cannot be served by a submitted
|
||||
command alone — it needs a *keybinding* that acts on the live buffer
|
||||
without submitting. ADR-0003 said "current input"; `requirements.md`
|
||||
broadened it to "current input **or the most recent error**." Both are
|
||||
wanted; they map to two different trigger surfaces.
|
||||
|
||||
**The cost: the value of `hint` is content, not plumbing.** The app
|
||||
already carries two tiers of contextual text:
|
||||
|
||||
- **Tier 1** — terse, always-on: syntax colour (ADR-0022); the error
|
||||
*headline* alone (ADR-0019, when `messages_verbosity: Short`).
|
||||
- **Tier 2** — short contextual lines: the ambient typing prose /
|
||||
`expected` set, shown live while typing (ADR-0022, catalogue
|
||||
`hint.ambient_*` / `hint.value_slot_*`); and the error `hint:` field —
|
||||
which, because `Verbosity::Verbose` is the **default**
|
||||
(`src/friendly/translate.rs:46`), is shown **by default** beneath every
|
||||
error headline (`messages short` is the opt-*out*, not `messages
|
||||
verbose` the opt-in).
|
||||
|
||||
So the verbose error hint is **already on screen by default**. If `hint`
|
||||
merely re-showed it, it would duplicate what the user can already see (and
|
||||
the ambient panel). To justify itself, `hint` must add a **tier 3**: a
|
||||
genuinely deeper, *teaching*-grade explanation — what the command/error
|
||||
means, a worked example, and the underlying relational concept. That
|
||||
corpus does not exist yet, and
|
||||
authoring it (to the standard of a teaching tool, where "pedagogy wins
|
||||
ties") is the bulk of the work.
|
||||
|
||||
The mechanism is small and reuses everything already present: the command
|
||||
REGISTRY (`src/dsl/grammar/mod.rs`), the `AppCommand` enum
|
||||
(`src/dsl/command.rs`), key dispatch (`App::handle_key`,
|
||||
`src/app.rs:1155`), the `note_help`/`note_help_topic` renderers
|
||||
(`src/app.rs:2982`/`3021`), the parser/walker expected-set
|
||||
(`ParseError.expected`, `WalkResult.tail_expected`), the friendly
|
||||
catalogue + `t!` macro + `keys.rs` validation, and the output styling
|
||||
vocabulary (`OutputStyleClass::Hint`).
|
||||
|
||||
## Decision
|
||||
|
||||
### D1 — Two surfaces, no topic argument
|
||||
|
||||
`hint` is delivered through **two complementary surfaces**:
|
||||
|
||||
1. **F1 keybinding → live input.** Pressing **F1** while typing renders a
|
||||
tier-3 hint for the command currently in the buffer, into the output
|
||||
panel, **without submitting or altering the buffer**. This is the
|
||||
primary, most-valuable path (it serves the literal "current input").
|
||||
2. **`hint` command → most recent error.** Submitting `hint` renders the
|
||||
tier-3 expansion of the most recent error. This is why the command
|
||||
exists despite the empty-buffer problem: the thing it helps with is
|
||||
the *last thing you tried*, not the now-empty buffer.
|
||||
|
||||
`hint` takes **no topic argument**. Explicit per-command reference is
|
||||
already `help <topic>` (H3); `hint` is purely *contextual*, which keeps
|
||||
the two cleanly distinct (`hint` = "help me with what I'm doing right
|
||||
now"; `help insert` = "show me the insert reference").
|
||||
|
||||
F1 is a **read-only overlay**: it never alters the input buffer, the
|
||||
cursor, or the live completion memo (ADR-0022) — it only emits a block
|
||||
into the output journal. (It must therefore be handled in `handle_key`
|
||||
*before* the "any other key clears the memo" fall-through.)
|
||||
|
||||
### D2 — Trigger matrix
|
||||
|
||||
| Trigger | Buffer / state | Result |
|
||||
|---|---|---|
|
||||
| **F1** | non-empty input | tier-3 hint for the command being typed, plus the live "expected next" (from the walker's `tail_expected` / parser `expected`) |
|
||||
| **F1** | empty input, a recent error exists | tier-3 expansion of that error |
|
||||
| **F1** | empty input, no recent error | a short "getting started" pointer (press F1 while typing a command; `help` for the full list) |
|
||||
| **`hint`** (submitted) | a recent error exists | tier-3 expansion of that error (primary use) |
|
||||
| **`hint`** (submitted) | no recent error | the same "getting started" pointer |
|
||||
|
||||
F1 is inert behind a modal and while a sidebar panel holds navigation
|
||||
focus (consistent with the existing `handle_key` gates, ADR-0046); it is
|
||||
active in the input context in both Simple and Advanced mode.
|
||||
|
||||
**Two error sources, one namespace.** Errors come in two kinds and reach
|
||||
`hint` by different routes:
|
||||
|
||||
- **Pre-submit diagnostics** (the ~33 `diagnostic.*` classes — arity,
|
||||
type, unknown table/column) are computed *while typing* by the walker.
|
||||
The **F1 live-input path** reads the current under-cursor diagnostic
|
||||
directly from the walker (the same source the ambient panel uses) and
|
||||
renders its `hint.err.<class>` block — no stored state needed.
|
||||
- **Runtime errors** (the 9 `translate_error` classes) occur *after*
|
||||
submit. The **`hint` command / empty-input F1** path reads them via the
|
||||
stored `last_error_hint_key` (D5).
|
||||
|
||||
Both render from the same `hint.err.*` namespace. **`:`-prefix handling:**
|
||||
on the simple-mode one-shot escape (`: SELECT …`), command
|
||||
identification for the F1 path strips the leading `:` first, so the
|
||||
advanced form is matched.
|
||||
|
||||
### D3 — The tier-3 content model
|
||||
|
||||
Tier-3 blocks live in the friendly catalogue under the existing `hint:`
|
||||
top-level namespace (where tier-2 ambient strings already live), in two
|
||||
new sub-namespaces:
|
||||
|
||||
- **`hint.cmd.<hint_id>`** — one per command **form**, keyed by a **new
|
||||
`hint_ids: &'static [&'static str]`** field on `CommandNode`
|
||||
(`src/dsl/grammar/mod.rs:512`), **mirroring the existing `usage_ids`**.
|
||||
The F1 live-input path resolves the current input to its form's hint key
|
||||
via `hint_key_for_input_in_mode`, which reuses the same form-word
|
||||
disambiguation as `usage_key_for_input_in_mode`.
|
||||
|
||||
**Why an array mirroring `usage_ids`, not a per-node `hint_id`**
|
||||
*(`/runda`/implementation revision, 2026-06-15)*: a single per-node key
|
||||
is too coarse. Several entry words are **one node spanning many forms** —
|
||||
`add` (column/relationship/index/constraint), `drop` (table/column/
|
||||
relationship/index), `show` (data/table/tables/relationships/indexes),
|
||||
`create` (table/index). A live-input hint for `add 1:n relationship` is
|
||||
only useful if it is *specific to relationships*, so the content must be
|
||||
**per form**, not per node. The project already solved exactly this for
|
||||
usage templates (`usage_ids` is a per-form array, disambiguated by the
|
||||
form word), so `hint_ids` mirrors it. Single-form nodes carry one entry;
|
||||
multi-form nodes carry one per form. This also covers the advanced-SQL
|
||||
forms whose `usage_ids` are empty (`SQL_INSERT/UPDATE/DELETE`,
|
||||
`EXPLAIN_SQL`) — they get their own `hint_ids` directly, independent of
|
||||
usage, with mode-correct SQL examples. (The `help`-list collapse of
|
||||
advanced-SQL forms is a separate gap — issue #36.)
|
||||
|
||||
**Deferred extension — clause-concept hints** (issue #37): per-form is
|
||||
the right granularity for tier-3 *teaching* (position-awareness within a
|
||||
form is owned by tier-2 ambient + the live `Next:` line, D4). But some
|
||||
**concepts live inside a clause**, not a form — `… on delete ⟨cascade|
|
||||
set null|restrict⟩` (referential actions), the `create table` constraint
|
||||
slots (`primary`/`unique`/`check`/`foreign`), `with pk`, `1:n`/`m:n`
|
||||
cardinality. A learner parked in such a clause may want teaching deeper
|
||||
than tier-2's candidate list but narrower than the whole-form block. v1
|
||||
does **not** build this (it would multiply content for points whose value
|
||||
we can't yet measure, and we don't expect to accumulate usage statistics
|
||||
to drive it empirically — it will be tackled as a deliberate follow-up
|
||||
job). The keying does not lock it out: a later `hint.concept.<topic>`
|
||||
namespace can be surfaced when the cursor sits in a recognized clause,
|
||||
layered on top of the per-form block.
|
||||
- **`hint.err.<class>`** — one per error/diagnostic class, keyed by the
|
||||
friendly error/diagnostic key (e.g. `hint.err.foreign_key.child_side`,
|
||||
`hint.err.type_mismatch`, `hint.err.insert_arity_mismatch`). Used by
|
||||
both error routes (D2).
|
||||
|
||||
Each tier-3 block is a **structured entry with three labelled parts**, so
|
||||
the voice stays consistent and the renderer can style them uniformly:
|
||||
|
||||
```yaml
|
||||
hint.cmd.dsl.insert:
|
||||
what: "Add one or more rows to a table."
|
||||
example: "insert into Customers values ('Ann', 'ann@x.io')"
|
||||
concept: "A row is one record; each value lines up with a column, in
|
||||
order. Columns typed `serial`/`shortid` fill themselves — leave them out."
|
||||
```
|
||||
|
||||
- **`what`** — one or two plain sentences: what this command does / what
|
||||
this error means.
|
||||
- **`example`** — a single concrete, copyable line (rendered neutral, not
|
||||
muted, so it stands out as runnable).
|
||||
- **`concept`** — the underlying relational idea, in teaching voice; the
|
||||
part that makes this tier-3 rather than tier-2.
|
||||
|
||||
`concept` is optional where there is genuinely no concept beyond the
|
||||
mechanics (e.g. `quit`); `what` + `example` are always present.
|
||||
|
||||
### D4 — Rendering
|
||||
|
||||
Both surfaces render through one new renderer, `App::note_hint*` (sibling
|
||||
of `note_help`/`note_help_topic`, `src/app.rs`), emitting a small framed
|
||||
block into the `output` buffer as `OutputKind::System` with
|
||||
`OutputStyleClass::Hint` on the `what`/`concept` prose and `Neutral` on
|
||||
the `example` line. The block is **persistent** (scrolls in the journal),
|
||||
unlike the transient ambient panel — pressing F1 is an explicit request
|
||||
to *keep* the deeper guidance on screen. The bottom keybinding strip
|
||||
(ADR-0051) advertises F1 in the editing/typing state.
|
||||
|
||||
### D5 — "Most recent (runtime) error" state
|
||||
|
||||
The **runtime-error route** (submitted `hint`, and empty-input F1) needs
|
||||
to map the last runtime error back to its `hint.err.<class>` key. Runtime
|
||||
errors today live only as rendered text in the `output` buffer. We add a
|
||||
single small piece of `App` state — **`last_error_hint_key:
|
||||
Option<String>`** — set at the `translate_error` call sites
|
||||
(`runtime.rs:2615`, `app.rs:2424`) when a friendly error is rendered,
|
||||
cleared when a later command succeeds. Absent → the "getting started"
|
||||
pointer.
|
||||
|
||||
The **pre-submit-diagnostic route** (the F1 live-input path) needs no
|
||||
stored state: it reads the current diagnostic from the walker at F1 time
|
||||
(D2). This is the cleaner split the `/runda` pass surfaced — typing-time
|
||||
diagnostics and post-submit runtime errors are genuinely different
|
||||
sources and should not be funnelled through one stored key.
|
||||
|
||||
### D6 — Content scope: comprehensive for v1
|
||||
|
||||
v1 ships tier-3 content for the **whole inventory**, not a subset (the
|
||||
graceful tier-2 fallback below is a safety net, not the plan):
|
||||
|
||||
- **~37 command forms** — every distinct node in `REGISTRY` gets its own
|
||||
`hint.cmd.<hint_id>` block (app + DSL + DDL + advanced-mode SQL forms),
|
||||
each with a **mode-correct example** (the advanced-SQL forms show SQL
|
||||
syntax, their simple siblings show DSL — no sharing).
|
||||
- **9 runtime error classes** — `unique`, `foreign_key` (×4 sides),
|
||||
`not_null`, `check`, `type_mismatch`, `not_found`, `already_exists`,
|
||||
`generic`, `invalid_value` — each gets a `hint.err.*` block.
|
||||
- **~33 `diagnostic.*` pre-submit classes** — arity, type, unknown
|
||||
table/column, etc. — each gets a `hint.err.*` block.
|
||||
|
||||
The full enumerated checklist is the implementation plan's tracking
|
||||
artifact (see *Content inventory*, below).
|
||||
|
||||
**Fallback (safety net):** if a tier-3 key is ever missing at runtime,
|
||||
the surface degrades to tier 2 — the ambient prose for the command path,
|
||||
or the verbose error `hint:` for the error path — never to a blank or an
|
||||
error. The `keys.rs` build-time validation keeps the corpus honest, so a
|
||||
missing key is caught in tests, not in front of a student.
|
||||
|
||||
### D7 — Authoring process: exemplars-first
|
||||
|
||||
Because the corpus is large and its *voice* is a pedagogical decision the
|
||||
maintainer owns, content is produced in two stages:
|
||||
|
||||
1. This ADR carries **2–3 worked exemplars** (below) as the canonical
|
||||
style reference. The `/runda` review of this ADR is where the voice and
|
||||
depth are approved.
|
||||
2. Once approved, the remaining blocks are authored to that template in
|
||||
**reviewable batches** (grouped by area: DDL, DML, app commands,
|
||||
error classes), not one monolithic drop.
|
||||
|
||||
### Exemplars (the style reference to approve)
|
||||
|
||||
**Command (F1 live-input), `insert`:**
|
||||
|
||||
```
|
||||
Hint — insert
|
||||
What: Add one or more rows to a table.
|
||||
Example: insert into Customers values ('Ann', 'ann@x.io')
|
||||
Concept: A row is one record; each value lines up with a column, in
|
||||
order. Columns typed serial/shortid fill themselves — leave
|
||||
them out.
|
||||
Next: a value list `(...)`, or `(col, ...) values (...)` to name columns
|
||||
```
|
||||
(The "Next:" line is the live expected-set from the walker, shown only on
|
||||
the non-empty-input F1 path.)
|
||||
|
||||
**Error (`hint` command), foreign-key child-side violation:**
|
||||
|
||||
```
|
||||
Hint — no parent row to point at
|
||||
What: The value you inserted into Orders.customer_id doesn't match
|
||||
any Customers row, so the foreign key has nothing to point at.
|
||||
Example: First insert into Customers values ('Ann', ...)
|
||||
Then insert into Orders values (..., 'Ann')
|
||||
Concept: A foreign key is a promise that every child points at a real
|
||||
parent. The parent must exist first. To allow orphans on
|
||||
delete instead, set the relationship's `on delete` to
|
||||
`set null` or `cascade`.
|
||||
```
|
||||
|
||||
**Command (F1 live-input), `add 1:n relationship`:**
|
||||
|
||||
```
|
||||
Hint — add relationship
|
||||
What: Link two tables so a parent row can own many child rows.
|
||||
Example: add 1:n relationship from Customers.id to Orders.customer_id
|
||||
Concept: The "1:n" means one parent, many children. The child column
|
||||
holds the foreign key; `--create-fk` adds it for you if it
|
||||
doesn't exist yet.
|
||||
```
|
||||
|
||||
## Forks (all user-chosen, 2026-06-14)
|
||||
|
||||
- **Trigger model:** both a keybinding (live input) and a submitted
|
||||
command (last error), rather than command-only or keybinding-only — the
|
||||
live-input path is the most useful, but the command completes the A1
|
||||
slot and serves the error case.
|
||||
- **Keybinding = F1:** the universal help convention; the key is
|
||||
genuinely free (no `KeyCode::F(1)` binding exists today — the `"F1"`
|
||||
strings in `input_render.rs`/tests are scenario labels, not the key, and
|
||||
ADR-0022 uses no `F1` requirement label). No collision with the ADR-0049
|
||||
readline keys, `Ctrl-O` (ADR-0046), `Esc`-clear, or the reserved
|
||||
`Ctrl-C` cancel (I5). Rejected: `?` (a typeable character — fiddly
|
||||
position-dependent handling) and a Ctrl/Alt chord (less discoverable, no
|
||||
advantage).
|
||||
- **No topic argument:** contextual only; `help <topic>` already owns
|
||||
explicit reference lookup.
|
||||
- **Comprehensive content for v1:** the full inventory, not a starter
|
||||
subset.
|
||||
- **Exemplars-first authoring:** lock the voice on a few blocks, then
|
||||
mass-author to template.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A1 closes.** With `hint` registered and built, all 15 canonical
|
||||
app-level commands exist in both modes.
|
||||
- **A third contextual tier exists.** Students get on-demand, teaching-
|
||||
grade guidance that is deeper than the always-on colour, the headline,
|
||||
the ambient one-liner, and the verbose error hint — without cluttering
|
||||
those terse defaults.
|
||||
- **One new keybinding (F1)** joins the keymap and the ADR-0051 strip.
|
||||
- **A new `hint_ids: &[&str]` field on `CommandNode`** (mirroring
|
||||
`usage_ids`) + a `hint_key_for_input_in_mode` lookup (reusing the
|
||||
`usage_key_for_input_in_mode` form-disambiguation), one new field of
|
||||
`App` state (`last_error_hint_key`), and one new renderer family
|
||||
(`note_hint*`); the `AppCommand` enum gains `Hint`, the grammar a `HINT`
|
||||
node, the REGISTRY one entry.
|
||||
- **A large, durable content corpus** (~37 command blocks + ~42 error/
|
||||
diagnostic blocks ≈ 80) enters the catalogue under `hint.cmd.*` /
|
||||
`hint.err.*`, validated by `keys.rs`. This is ongoing surface area: new
|
||||
commands/error classes should ship with their tier-3 hint (a checklist
|
||||
item for future feature ADRs).
|
||||
- **Testing:** Tier-1 unit tests for the trigger matrix (F1 with
|
||||
empty/non-empty input; `hint` with/without a recent error;
|
||||
`last_error_hint_key` set on the `translate_error` sites and cleared on
|
||||
success; the pre-submit-diagnostic vs runtime-error routing; the `:`
|
||||
strip), the command-identification logic, and the tier-2 fallback;
|
||||
Tier-2 `insta` snapshots for a representative rendered hint block;
|
||||
Tier-3 integration tests for the end-to-end flows (type a partial
|
||||
command → F1 → block appears, **buffer and completion memo untouched**;
|
||||
run a failing command → `hint` → error expansion). **A
|
||||
comprehensiveness coverage test** (enforces D6): iterate the REGISTRY
|
||||
and assert every node has a `hint_id` resolving to a `hint.cmd.*` block,
|
||||
and every runtime-error/diagnostic class has a `hint.err.*` block —
|
||||
`keys.rs` only checks that *referenced* keys resolve, not that every
|
||||
command/error *has* one, so this test is what makes "comprehensive"
|
||||
enforceable rather than aspirational.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Per-topic `hint <topic>`** — OOS (rejected): `help <topic>` already
|
||||
serves explicit lookup; a topic arg would overlap it and double the
|
||||
content-authoring surface.
|
||||
- **Re-showing tier-3 inline as the always-on ambient hint** — OOS
|
||||
(rejected): the ambient panel stays terse by design (ADR-0022); tier-3
|
||||
is on-demand. Promoting it would defeat the tiering.
|
||||
- **Localised tier-3 content beyond `en-US`** — OOS (deferred): the
|
||||
catalogue is structured for i18n (ADR-0019), but additional locales
|
||||
follow the project's English-only-for-v1 stance (requirements X2).
|
||||
- **`hint` for a *successful* command's deeper teaching** (e.g. "you just
|
||||
created a table — here's what an index would add") — OOS (deferred): a
|
||||
plausible future tier-3 use, but v1 scopes the command path to errors
|
||||
and the F1 path to in-progress input.
|
||||
- **Clause-concept hints** (`… on delete ⟨action⟩`, constraint slots,
|
||||
`with pk`, cardinality) — OOS (deferred, issue #37): a
|
||||
`hint.concept.<topic>` layer surfaced when the cursor sits in a
|
||||
recognized clause, deeper than tier-2's candidate list but narrower than
|
||||
the per-form block. Per-form keying (D3) does not lock it out. To be
|
||||
tackled as a deliberate follow-up job, not gated on usage statistics.
|
||||
|
||||
## Content inventory (implementation tracking)
|
||||
|
||||
The implementation plan enumerates and checks off every block:
|
||||
|
||||
- **`hint.cmd.<hint_id>`** — one per distinct `REGISTRY` node (~37), each
|
||||
with its own `hint_id` and a mode-correct example: app (`save`, `save
|
||||
as`, `load`, `new`, `rebuild`, `export`, `import`, `replay`, `undo`,
|
||||
`redo`, `mode`, `messages`, `copy`, `help`, `hint`, `quit`); DDL
|
||||
(`create table`, `create m:n`, `add column`/`relationship`/`index`,
|
||||
`drop`, `rename`, `change column`); DML (`insert`, `update`, `delete`,
|
||||
`show`, `seed`, `explain`, `select`/`with`). The **7 advanced-mode SQL
|
||||
forms** (`SQL CREATE TABLE`, `ALTER TABLE`, `CREATE/DROP INDEX`, `DROP
|
||||
TABLE`, `SQL INSERT/UPDATE/DELETE`, `EXPLAIN SQL`, raw `SELECT`/`WITH`)
|
||||
each get their **own** block with SQL syntax — they do **not** reuse
|
||||
their simple sibling's (this is the `/runda` correction; the parallel
|
||||
`help`-side gap is issue #36).
|
||||
- **`hint.err.*`** — one per runtime error class (`unique`,
|
||||
`foreign_key.{child,parent}_side`, `not_null`, `check`,
|
||||
`type_mismatch`, `not_found`, `already_exists`, `generic`,
|
||||
`invalid_value`) and per `diagnostic.*` pre-submit class.
|
||||
+8
-3
File diff suppressed because one or more lines are too long
@@ -0,0 +1,173 @@
|
||||
# Session handoff — 2026-06-12 (68)
|
||||
|
||||
Sixty-eighth handover. Continues directly from handoff-67 (which
|
||||
triaged a manual-testing pass into fixes + filed issues). This was an
|
||||
**issue-burndown session**: six Gitea issues closed across five
|
||||
commits, each landed with the full phased workflow + a `/runda` +
|
||||
Devil's-Advocate pass before commit. Net: **six issues closed, five
|
||||
commits, +29 tests, zero regressions.**
|
||||
|
||||
## §1. State at handoff
|
||||
|
||||
**Branch:** `main`. Working tree **clean**; all work committed.
|
||||
**Five unpushed commits** (push is the user's step).
|
||||
|
||||
**Tests: 2436 passing / 0 failing / 0 skipped / 1 ignored** (the
|
||||
long-standing `friendly` doctest). **Clippy clean** (nursery, all
|
||||
targets). Breakdown: 1730 lib + 506 integration (`it`) + 200
|
||||
typing-surface-matrix. +29 over handoff-67's 2407.
|
||||
|
||||
**Commits since handoff-67:**
|
||||
```
|
||||
ee3ccd8 feat(hint): advertise the optional seed count in the hint panel (#26)
|
||||
deb0948 feat(seed): year-as-int + conventional choice-set heuristics (#33, #34)
|
||||
fde50ce fix(ui): mark sidebar focus with an accent colour, not bold (#25)
|
||||
3d4a0fd fix(render): trim IEEE-754 noise from displayed decimal arithmetic (#32)
|
||||
7e4bc12 fix(completion): treat a bare in-scope table alias as an alias, not an unknown column (#31)
|
||||
```
|
||||
|
||||
## §2. Issues closed this session (all committed, all tested, all `/runda`-reviewed)
|
||||
|
||||
Each closed on `git.lazyeval.net/oli/rdbms-playground` with a summary
|
||||
comment. The `/runda` pass earned its keep on every one — see the
|
||||
"DA caught" notes.
|
||||
|
||||
1. **#31 (`7e4bc12`) — bare table alias treated as unknown column.**
|
||||
A bare in-scope table alias in a SQL expression (`… GROUP BY o`,
|
||||
`o` aliasing `FROM Orders o`) got `no such column o on table …` and
|
||||
zero completions. Now: completion offers each FROM source's
|
||||
*qualifier* (alias-if-present-else-table) at a bare `sql_expr_ident`
|
||||
slot; the `matched.len()==0` arm emits a targeted
|
||||
`alias_used_as_column` / `table_used_as_column` hint after the
|
||||
projection-alias check. **DA caught** two real bugs pre-commit: a
|
||||
DSL leak (the hint fired for simple-mode `expr_column` refs, which
|
||||
have no `table.column` syntax) and wrong advice for an
|
||||
aliased-table-by-real-name — both fixed by gating on
|
||||
`role == "sql_expr_ident"` + matching the *effective qualifier*.
|
||||
ADR-0032 Amendment 3.
|
||||
|
||||
2. **#32 (`3d4a0fd`) — decimal aggregation float noise.** `decimal`
|
||||
is exact TEXT, but SQLite has no decimal type, so arithmetic
|
||||
coerces to IEEE-754 double; `sum(price*qty)` rendered
|
||||
`298.59999999999997`. Now `format_real_display` (db.rs) rounds REAL
|
||||
to 15 sig figs **for display only**, wired into `format_cell`.
|
||||
**DA caught** a real regression: I'd also wired it into
|
||||
`render_value`, which is a *canonical identity key* for the
|
||||
uniqueness dry-runs (`dry_run_unique`, `check_uniqueness_collisions`)
|
||||
— rounding there would report collisions the exact-valued engine
|
||||
wouldn't. Reverted `render_value` to exact; locked with a
|
||||
regression test. CSV/FK-key/EXPLAIN paths stay exact. ADR-0005
|
||||
Amendment 1.
|
||||
|
||||
3. **#25 (`fde50ce`) — sidebar focus accent colour, not bold.** Bold
|
||||
box-drawing glyphs render broken in asciinema casts.
|
||||
`panel_border_style` now uses a non-bold accent colour
|
||||
(`theme.mode_simple`); bold stays fine on text spans. **DA caught**
|
||||
that the issue's "Tier-2 snapshots need re-accepting" was wrong —
|
||||
`render_to_string` is text-only, so no snapshot changed. Added a
|
||||
render-level test that inspects the actual border *cells*.
|
||||
User visually confirmed. ADR-0046 Amendment 1.
|
||||
|
||||
4. **#33 + #34 (`deb0948`) — seed heuristics: year-as-int + choice
|
||||
sets.** Two additive D7 catalogue rules. **#33:** `year`/`*_year`/
|
||||
`published`/`founded` → bounded `int` year (1950–2025, or the
|
||||
`dob`-style birth window 1945–2007 for `birth`/`born`/`dob`); new
|
||||
`YearRecent`/`YearBirth` generators. Placed *after* the quantity
|
||||
rule so `year_count` stays a count. **#34:** type-gated `PickFrom`
|
||||
sets for `priority`/`prio`, `severity`, `rating`/`stars`; `status`
|
||||
**deliberately excluded** (user-confirmed on the issue — values too
|
||||
domain-specific). `priority` left `ENUM_TOKENS`. A user `IN`-CHECK
|
||||
still wins. **DA/process caught** that I'd skipped reading the issue
|
||||
*comments* (where the `status` decision + a website cast note lived)
|
||||
— **lesson: always read issue comments**. Also closed a
|
||||
pre-existing column-fill integration-test gap. ADR-0048 Amendment 1.
|
||||
|
||||
5. **#26 (`ee3ccd8`) — optional `count` advertised in the hint panel.**
|
||||
At `seed <table> ▮` only `set`/`--seed` chips showed; the optional
|
||||
row count (a bare positional number) was invisible, and the prior
|
||||
`IntroProse` attempt was reverted because `pending_hint_mode` is
|
||||
cleared by the trailing optionals. Now `walk_optional` stashes a
|
||||
skipped inner's `IntroProse` key into a new
|
||||
`WalkContext.surviving_intro_hint` (key + position) before the empty
|
||||
match clears it; a **position guard** (`pos == cursor`) stops it
|
||||
leaking past a later `set …` clause or once the count is given. Tab
|
||||
still cycles the keywords. Prose mentions the count, `.column`
|
||||
column-fill, `set`, and `--seed` (user-chosen scope). **DA caught**
|
||||
a coverage gap (advanced-mode path untested — seed runs in both
|
||||
modes); added the test. ADR-0022 Amendment 7.
|
||||
|
||||
## §3. Open issues — next session's candidates
|
||||
|
||||
Four open, all on `git.lazyeval.net/oli/rdbms-playground`. **All four
|
||||
are interaction/UX design changes that need a decision or two from the
|
||||
user up front — none is a pure mechanical fix.** Read each issue body
|
||||
**and its comments** before starting (the #33/#34 lesson).
|
||||
|
||||
- **#28 — Reconsider relationship prose in `add column` (incidental
|
||||
DDL) confirmations** *(enhancement)*. **Revisits a decided area** →
|
||||
needs a **new ADR** superseding the relevant part of ADR-0016 §5 /
|
||||
ADR-0044 §1. User preference (from the issue): do **not** show the
|
||||
`References:` / `Referenced by:` block in the add-column
|
||||
confirmation. Confirm scope with the user (just `add column`, or all
|
||||
incidental DDL). The highest-ceremony of the four.
|
||||
|
||||
- **#27 — Bottom status line: keybindings-only, context- and
|
||||
state-aware; add `mode advanced` to empty hint** *(enhancement)*.
|
||||
Per-nav-focus keybindings (Input vs sidebar), **including transient
|
||||
states** (Tab-cycle, history) per user preference. May warrant a
|
||||
small ADR. Touches `src/ui.rs` rendering + the nav-focus model
|
||||
(ADR-0046).
|
||||
|
||||
- **#29 — Command input keystroke support.** Esc / double-Esc to clear
|
||||
a partly-typed command; possibly Ctrl-A/Ctrl-E (Home/End). Relates
|
||||
to the deferred **I1b readline shortcuts** (`requirements.md`).
|
||||
**Needs a key-set decision** from the user before coding.
|
||||
|
||||
- **#30 — History brings back all commands in both modes.**
|
||||
Advanced-mode history entries can't replay in simple mode; proposal:
|
||||
if we can distinguish them, prepend `:` to reuse advanced history
|
||||
from simple mode. Interaction design; touches the input-history +
|
||||
mode model (ADR-0003).
|
||||
|
||||
No strong ordering. **#28** is the only one that *must* produce an ADR.
|
||||
**#29** is closest to "small once the key-set is decided." **#27** and
|
||||
**#30** are medium UX work.
|
||||
|
||||
## §4. Carried-over follow-up (not a `main`-branch task)
|
||||
|
||||
- **Website `seed` cast re-record** (from #34's comment thread). The
|
||||
`website` branch ships a `seed` cast exercising a `tickets` table
|
||||
with `priority`; now that `priority` collapses to `low/medium/high`,
|
||||
the cast should be re-recorded (`cd website && pnpm casts seed`,
|
||||
needs a `../target/debug` binary) so the table tightens. The issue
|
||||
comment notes it is **likely redundant** — casts get a full
|
||||
re-record sweep before publication. Tracked on the `website` branch,
|
||||
**not** here. `website/` is not in the `main` tree.
|
||||
|
||||
## §5. Other open roadmap (unchanged from handoff-67 §5)
|
||||
|
||||
`seed` is feature-complete (`requirements.md` SD1/SD2 `[x]`, now with
|
||||
the #33/#34 catalogue refinements noted inline). User's call:
|
||||
|
||||
- **H2 `hint`** — the last A1 gap (its own ADR).
|
||||
- **TT5 CI** — test infra exists; no CI workflow yet (the `ci` branch
|
||||
exists — check its state before starting).
|
||||
- **TT4 PTY (Tier-4)** — ADR-0008 specifies it; not wired.
|
||||
- Larger: **V4 journal**, **tutorial/lesson system** (each needs an ADR).
|
||||
|
||||
## §6. How to take over
|
||||
|
||||
1. Read handoffs 66 → 67 → 68, `CLAUDE.md`, `docs/requirements.md`.
|
||||
2. Confirm green baseline: `cargo test` (expect 2436 pass / 1 ignored)
|
||||
+ `cargo clippy --all-targets` (clean).
|
||||
3. Pick from §3 (#28/#27/#29/#30). **For each, read the issue body AND
|
||||
its comments** before designing, and **escalate the design fork to
|
||||
the user** before coding — all four have genuine UX decisions. #28
|
||||
needs a new ADR.
|
||||
4. Follow the project workflow: phased (requirements → divergent →
|
||||
eval → execute → verify), test-first (failing test before the fix),
|
||||
`/runda` + DA pass before every commit, ADR amendment for any
|
||||
decided-area change + the README index-upkeep rule, and confirm the
|
||||
commit message with the user before committing.
|
||||
5. Consider a `cargo sweep` at this milestone (`target/` grows across
|
||||
sessions; see CLAUDE.md "Build hygiene").
|
||||
@@ -0,0 +1,203 @@
|
||||
# Session handoff — 2026-06-14 (69)
|
||||
|
||||
Sixty-ninth handover. Continues from handoff-68 (an issue-burndown that
|
||||
closed #25/#26/#31/#32/#33/#34). This session **closed the four
|
||||
remaining open issues** — #29, #28, #27, #30 — each landed with the full
|
||||
phased workflow + `/runda` + Devil's-Advocate passes before commit, and
|
||||
each producing a new ADR. Net: **four issues closed, four commits, four
|
||||
new ADRs (0049–0052), +63 tests, zero regressions, the tracker is now
|
||||
empty.**
|
||||
|
||||
The four interlock: **#29** added the input-field readline keys, **#27**
|
||||
advertises them in a state-aware status strip, and **#30**'s history
|
||||
recall now respects modes. **#30** also turned into a real architecture
|
||||
change (journaling relocation) — read §2.4 carefully before touching that
|
||||
area.
|
||||
|
||||
## §1. State at handoff
|
||||
|
||||
**Branch:** `main`. Working tree **clean**; all work committed. The two
|
||||
most recent commits are local (normal working state — push is the user's
|
||||
step).
|
||||
|
||||
**Tests: 2471 passing / 0 failing / 0 skipped / 1 ignored** (the
|
||||
long-standing `friendly` doctest). **Clippy clean** (nursery, all
|
||||
targets). Breakdown: 1771 lib + 500 integration (`it`) + 200
|
||||
typing-surface-matrix. **+35 over handoff-68's 2436** (net: #29 +22, #28
|
||||
+0, #27 +9, #30 +4 — its new history.rs/app.rs/iteration6 tests minus the
|
||||
15 retired worker-journaling tests; trust the live `cargo test` count).
|
||||
|
||||
**Commits this session:**
|
||||
```
|
||||
4aeea55 feat(history): mode-tagged history + top-of-chain journaling (#30)
|
||||
eceedc1 feat(ui): context- and state-aware bottom keybinding strip (#27)
|
||||
8ac3537 feat(render): incidental-DDL confirmations show structure only, no relationships (#28)
|
||||
66c8bda feat(input): readline keymap — Esc-clear + Ctrl-A/E/W/K/U (#29)
|
||||
```
|
||||
|
||||
**Open Gitea issues: none.** `tea issues list --state open` is empty.
|
||||
|
||||
## §2. Issues closed this session (all committed, tested, `/runda`-reviewed)
|
||||
|
||||
Each closed on `git.lazyeval.net/oli/rdbms-playground` with a summary
|
||||
comment.
|
||||
|
||||
### 2.1 — #29 (`66c8bda`) — input-field readline keymap (ADR-0049)
|
||||
|
||||
Implements the deferred **I1b** readline shortcuts: `Esc` clears a
|
||||
partly-typed command (only when no completion memo is alive — the memo
|
||||
wins first, ADR-0022); `Ctrl-A`/`Ctrl-E` = Home/End; `Ctrl-W` deletes
|
||||
the previous word (readline-style, UTF-8 safe); `Ctrl-K`/`Ctrl-U` kill to
|
||||
end/start. Cursor-only keys leave history nav intact; buffer-mutating
|
||||
keys end it. **DA caught** the need for the `Ctrl-O`+`Esc` (sidebar
|
||||
nav-exit) interaction not to clear the draft — locked with a regression
|
||||
test. `requirements.md` I1b → `[x]`.
|
||||
|
||||
### 2.2 — #28 (`8ac3537`) — incidental-DDL confirmations: structure-only (ADR-0050)
|
||||
|
||||
Incidental-DDL confirmation echoes (`create table`, `add`/`drop`/
|
||||
`rename`/`change column`, `add`/`drop index`) now render **structure
|
||||
only** — no `References:` / `Referenced by:` block. Relationship-subject
|
||||
surfaces (`show table`, `add`/`drop relationship`) keep their ADR-0044
|
||||
diagrams. The prose renderer (`relationship_prose_lines` + `cols_disp`)
|
||||
was deleted. **Supersedes** ADR-0044 §1's incidental-DDL prose clause and
|
||||
the relationship-block half of ADR-0016 §5 (both annotated).
|
||||
|
||||
### 2.3 — #27 (`eceedc1`) — context- and state-aware keybinding strip (ADR-0051)
|
||||
|
||||
The bottom status line is now keystrokes-only and **state-selected** by
|
||||
priority (sidebar focus / completion-memo / history-nav / editing /
|
||||
default). The editing state surfaces the #29 keys (closing ADR-0049's
|
||||
deferred advertisement). Mode-switch advertisements left the strip; the
|
||||
empty-input hint gained a simple-mode `` `mode advanced` for SQL `` pointer
|
||||
(advanced mode shows none — user decision). New `App::is_browsing_history()`
|
||||
exposes the private `history_cursor`. 15 full-panel snapshots re-accepted.
|
||||
|
||||
### 2.4 — #30 (`4aeea55`) — mode-tagged history + top-of-chain journaling (ADR-0052) **← read before touching journaling**
|
||||
|
||||
Closed both the feature (advanced history reusable in simple mode) and
|
||||
the bug (the `:` one-shot prefix lost across sessions). Two halves:
|
||||
|
||||
1. **Mode-tagged history.** The `history.log` status token gains an
|
||||
optional `:adv` suffix (`ok` / `ok:adv` / `err` / `err:adv`); `source`
|
||||
stays last + canonical so replay is unaffected. The in-memory ring
|
||||
(still `Vec<String>`) stores advanced entries in their `: `-prefixed
|
||||
simple-mode runnable form; recall **strips the `:` in advanced mode**
|
||||
and keeps it in simple; hydration reconstructs the prefix from the tag.
|
||||
App commands journal simple and are excluded from the ring's advanced
|
||||
flag, so they recall bare.
|
||||
|
||||
2. **Journaling relocation (the architecture change).** Success
|
||||
journaling **moved out of the worker** to the dispatch layer
|
||||
(`spawn_dsl_dispatch` / `run_replay` / the app-command sites), next to
|
||||
the already-top-level failure journaling — so the submission mode is in
|
||||
scope with no worker plumbing. `finalize_persistence` now writes only
|
||||
the **state** sources (yaml/csv); the journal write is **best-effort**
|
||||
(the command is already committed — consistent with the failure path).
|
||||
**Amends ADR-0015 §6** (history.log out of the worker tx; commit-db-last
|
||||
scopes yaml/csv/db only), **ADR-0034** (status tag + journaling
|
||||
location), **ADR-0040** (journal-write best-effort, not fatal).
|
||||
|
||||
**Two DA findings, both resolved:** (a) the app-command `advanced` flag
|
||||
must exclude app commands (else `: save as` diverges); (b) the spawn
|
||||
journals on `outcome.is_ok()`, so journaling is now **uniform** — read
|
||||
commands that didn't journal before (`show tables`/`show relationships`/
|
||||
`show indexes`, `show relationship <name>`, `explain`) now do, matching
|
||||
ADR-0034 §1. **User-confirmed** as the more-correct behaviour (harmless
|
||||
on replay — reads/`explain` don't mutate).
|
||||
|
||||
**Test migration:** 15 worker-level journaling tests were retired (the
|
||||
worker no longer journals — their yaml/csv/operation assertions were
|
||||
kept) and re-covered at the new layer: `history.rs` status-tag +
|
||||
`:`-reconstruct; `app.rs` recall matrix; the cross-session regression
|
||||
`advanced_command_journalled_then_hydrated_recalls_with_colon_in_simple`
|
||||
in `iteration6_resume_history`; the replay tests cover `run_replay`
|
||||
journaling.
|
||||
|
||||
Plan: `docs/plans/20260613-issue-30-top-of-chain-journaling.md`.
|
||||
|
||||
## §3. Next session — start here
|
||||
|
||||
The user's stated plan for the next session, in order:
|
||||
|
||||
1. **Pick up the ADR-0052 follow-up** (below).
|
||||
2. **Check for any newly-filed open issues** (`tea issues list --state
|
||||
open`) — none at handoff, but check fresh.
|
||||
3. **Then** take on remaining open tasks from the general requirements
|
||||
(`docs/requirements.md`) — see §5.
|
||||
|
||||
### The ADR-0052 follow-up — unwind the vestigial worker `source` plumbing
|
||||
|
||||
When journaling moved out of the worker, the `source` that the worker
|
||||
threaded purely for journaling became dead. To avoid orphaning the param
|
||||
across ~28 handlers, the refactor **left it in place** as vestigial:
|
||||
|
||||
- `finalize_persistence(conn, persistence, _source, changes)` — the
|
||||
`_source` param is now unused (kept so its ~28 callers still pass
|
||||
`source`, which they otherwise also use for `snapshot_then`).
|
||||
- `do_rebuild_from_text(conn, _persistence, _source, project_path)` —
|
||||
both `_persistence` and `_source` vestigial.
|
||||
- Three thin read-only wrappers in `db.rs` —
|
||||
`do_describe_table_request`, `do_query_data_request`,
|
||||
`do_run_select_request` — now just delegate to their non-`_request`
|
||||
twin (`do_describe_table` / `do_query_data` / `do_run_select`) with
|
||||
vestigial `_persistence` / `_source` params and one caller each
|
||||
(`db.rs` Request arms ~2409 / ~2749 / ~2759).
|
||||
|
||||
**The cleanup:** remove `_source` from `finalize_persistence` + drop the
|
||||
arg at its ~28 callers (the callers keep `source` for `snapshot_then`, so
|
||||
only the `finalize_persistence(...)` call loses the arg); remove the
|
||||
`_persistence`/`_source` params from `do_rebuild_from_text`; and inline
|
||||
the three `*_request` wrappers at their single call sites (replace
|
||||
`do_describe_table_request(conn, persistence, source, name)` with
|
||||
`do_describe_table(conn, &name)`, etc.), deleting the wrappers. Purely
|
||||
mechanical, compiler-guided, no behaviour change. Establish the green
|
||||
baseline first (`cargo test`), then verify nothing moved.
|
||||
|
||||
## §4. Carried-over follow-up (website branch, not `main`)
|
||||
|
||||
- **Website `seed` cast re-record** (from #34, handoff-68 §4) — still
|
||||
tracked on the `website` branch, not here. Likely redundant (full
|
||||
re-record sweep before publication).
|
||||
|
||||
## §5. Remaining roadmap — `docs/requirements.md` (next session's §3-step 3)
|
||||
|
||||
With the issue tracker empty, the next work comes from the document-based
|
||||
requirements. Open / partial items worth weighing (the user picks):
|
||||
|
||||
- **H2 `hint`** — the last A1 gap (contextual help for the current
|
||||
command); its own ADR. (`requirements.md` H2.)
|
||||
- **TT5 CI** — runs all tiers on Linux/macOS/Windows; no CI workflow yet
|
||||
(a `ci` branch reportedly exists — check its state first). Couples with
|
||||
**D1–D3** (cross-platform prebuilt binaries + Homebrew/Scoop).
|
||||
- **TT4 PTY (Tier-4)** — ADR-0008 specifies the PTY harness + four
|
||||
critical flows; still not wired (no PTY deps/tests).
|
||||
- **I1 multi-line input** (Ctrl-Enter submits, Enter inserts newline) and
|
||||
**I5 / B3 in-flight cancellation** (Ctrl-C cancels a running command).
|
||||
- **V4 session journal** — scrollable per-session log + Markdown export
|
||||
(the bigger UX project; own ADR).
|
||||
- **TU1 tutorial / lesson system** — design + ADR pending (acknowledged
|
||||
in scope).
|
||||
- Smaller partials: **C3a** modify relationship (drop+add covers it
|
||||
today), **C4** m:n convenience, **V3** ER-diagram export, the **NFR-***
|
||||
performance/visual targets (mostly unmeasured), **N4** global rolling
|
||||
history (OOS for v1).
|
||||
|
||||
No strong ordering — these are the user's call. Several need a new ADR
|
||||
(H2, V4, TU1); CI/release (TT5/D1–D3) is the most "shippable-product"
|
||||
track if that's the priority.
|
||||
|
||||
## §6. How to take over
|
||||
|
||||
1. Read handoffs 67 → 68 → 69, `CLAUDE.md`, `docs/requirements.md`.
|
||||
2. Confirm green baseline: `cargo test` (expect **2471 pass / 1 ignored**)
|
||||
+ `cargo clippy --all-targets` (clean).
|
||||
3. `tea issues list --state open` — pick up anything new first.
|
||||
4. Then the ADR-0052 follow-up (§3), then requirements (§5).
|
||||
5. Follow the project workflow: phased (requirements → divergent → eval →
|
||||
execute → verify), test-first, `/runda` + DA pass before every commit,
|
||||
ADR amendment for any decided-area change + the README index-upkeep
|
||||
rule, and confirm the commit message with the user before committing.
|
||||
6. Consider a `cargo sweep` at this milestone (`target/` grows across
|
||||
sessions; see CLAUDE.md "Build hygiene"). (`sweep.timestamp` was
|
||||
removed this session.)
|
||||
@@ -0,0 +1,247 @@
|
||||
# Plan — issue #30: mode-tagged history + top-of-chain journaling
|
||||
|
||||
**Status:** draft for `/runda` review (2026-06-13).
|
||||
**Issue:** #30 — advanced history reusable in simple mode (prepend `:`),
|
||||
and the bug: the `:` one-shot prefix is lost across sessions.
|
||||
**ADR:** ADR-0052 (new); amends ADR-0015 §6, ADR-0034, ADR-0040;
|
||||
references ADR-0003.
|
||||
|
||||
## 1. Goal & root cause
|
||||
|
||||
Two coupled needs, one root cause — **history entries carry no mode**:
|
||||
- **Bug:** the in-memory ring stores the raw `:select 1`, but the worker
|
||||
journals the *stripped* `select 1`, so cross-session the `:` is lost
|
||||
and the command recalls bare (unusable in simple mode).
|
||||
- **Feature:** persistent-advanced commands (`select 1` typed in advanced
|
||||
mode) can't be told apart from simple DSL, so they can't be offered
|
||||
back with a `:` in simple mode.
|
||||
|
||||
Fix: **record the submission mode per entry** (status tag `:adv`), keep
|
||||
the on-disk `source` canonical, and have **recall prepend/strip `:`** for
|
||||
the current mode.
|
||||
|
||||
## 2. The architecture insight (why this plan is shaped this way)
|
||||
|
||||
Journaling **success** lives deep in the worker: `finalize_persistence`
|
||||
(db.rs:3096-3099) writes `history.log` *inside the db transaction, before
|
||||
`tx.commit()`*, alongside yaml/csv — plus four no-op-skip sites and three
|
||||
read-only helpers. **Failure** journaling already lives at the top
|
||||
(runtime.rs:484-495, best-effort). Threading the mode *down* to the
|
||||
worker would mean ~30 `Request` variants + `Database` methods +
|
||||
`execute_command_typed` arms — because the journal write is far from
|
||||
where the mode is known.
|
||||
|
||||
So instead: **move success journaling up to the dispatch layer**, next to
|
||||
where failure journaling already is and where mode + outcome + source are
|
||||
all in scope. The mode then needs no plumbing. This is the correct
|
||||
separation anyway — `history.log` is an append-only *journal of what was
|
||||
typed*, not *state*; the state sources (yaml/csv/db) stay atomic in the
|
||||
worker.
|
||||
|
||||
### Semantic changes this entails (must be vetted)
|
||||
|
||||
1. **history.log leaves the worker transaction** (amends ADR-0015 §6).
|
||||
`commit-db-last` still governs yaml/csv/db (the state); the journal is
|
||||
written *after* the worker replies (i.e. after `tx.commit`), at the
|
||||
dispatch layer.
|
||||
2. **Success-journal write failure: fatal → best-effort** (amends
|
||||
ADR-0040). Today a failed `history.log` write on a *successful*
|
||||
command rolls the command back and shows a fatal banner. After: the
|
||||
command stays committed; the journal write is best-effort (logged +
|
||||
ignored), exactly like the failure path already is. The two journal
|
||||
paths become *consistent*.
|
||||
3. **Consequence:** on a rare journal-write failure (disk full /
|
||||
permissions) a successful command is applied but may be missing from
|
||||
`history.log` — not recallable next session, not replayable. The state
|
||||
(yaml/csv/db) is unaffected and consistent. This is a graceful
|
||||
degradation, not corruption, and is logged. (Today the same disk-full
|
||||
instead kills the app mid-command.)
|
||||
|
||||
**Open question for review/user:** is trading "fatal on journal-write
|
||||
failure" for "best-effort, command still succeeds" acceptable? The plan
|
||||
assumes **yes** (a journal is auxiliary; killing the app over it is worse
|
||||
UX). If not, journaling must stay coupled in the worker and we pay the
|
||||
~30-site mode plumbing instead.
|
||||
|
||||
## 3. On-disk format (mode tag in status — already chosen + partly built)
|
||||
|
||||
Record stays `<ts>|<status>|<source>`; the **status token** gains an
|
||||
optional `:adv` suffix (ADR-0052). `source` stays canonical so replay is
|
||||
unaffected.
|
||||
|
||||
| Submission | Success | Failure |
|
||||
|---|---|---|
|
||||
| Simple / app command | `ok` | `err` |
|
||||
| Advanced (SQL, persistent or one-shot) | `ok:adv` | `err:adv` |
|
||||
|
||||
**Done already** (history.rs / mod.rs):
|
||||
- `status_token(base, advanced)`, `parse_status(status) -> (is_ok, advanced)`.
|
||||
- `parse_record_source` reconstructs `": {cmd}"` for `:adv` records.
|
||||
- `parse_journal_record.status_is_ok` via `parse_status` (so `ok:adv` replays).
|
||||
- `append_history(text, advanced)`, `append_history_failure(text, advanced)`.
|
||||
|
||||
Back-compat: old `ok`/`err` logs → simple; nothing migrates.
|
||||
|
||||
## 4. In-memory ring & recall (app.rs) — the #30 behaviour
|
||||
|
||||
The ring stays `Vec<String>`. An **advanced** entry is stored in its
|
||||
`: `-prefixed simple-mode runnable form (matching the existing in-session
|
||||
one-shot ring); a **simple** entry bare. A leading `:` unambiguously
|
||||
marks advanced (simple DSL can never start with `:`).
|
||||
|
||||
- **`submit`** (app.rs:1704): compute `effective_input` + `submission_mode`,
|
||||
parse once for the app-command check (already done at 1751), then build
|
||||
the ring line. The **`advanced` flag excludes app commands** —
|
||||
`advanced = submission_mode.is_advanced() && !is_app_command` — because
|
||||
app commands (`undo`, `mode …`, `save as`, …) run in *any* mode and must
|
||||
**not** get a `:` on recall. Ring line: `": " + effective_input` if
|
||||
`advanced`, else `effective_input`; `push_history(&ring_line)`. (Today it
|
||||
pushes the raw `trimmed` *before* stripping; the reorder also drops a
|
||||
bare `:`, which executed nothing, and is what lets the app-command check
|
||||
precede the push.) `ExecuteDsl.source` stays the **canonical**
|
||||
`effective_input`.
|
||||
- *Why the app-command exclusion matters (DA finding):* without it,
|
||||
`: save as foo` (an app command via the one-shot) would store `: save
|
||||
as foo` in the ring but journal `save as foo` (app commands journal
|
||||
simple at their own sites, §5) — the very in-session-vs-cross-session
|
||||
divergence #30 is fixing, re-introduced for app commands. Excluding
|
||||
them keeps ring and disk agreeing (both bare).
|
||||
- **`history_back` / `history_forward`**: after cloning the stored entry
|
||||
into `self.input`, strip a leading `:` **iff `self.mode == Advanced`**
|
||||
(so an advanced entry runs as bare SQL in advanced mode, and as `: …`
|
||||
one-shot in simple mode). A small helper `recall_display(stored)`.
|
||||
- `seed_history` / `ProjectSwitched` payload: **unchanged** (`Vec<String>`);
|
||||
hydration already returns the `: `-prefixed form (§3).
|
||||
|
||||
Recall matrix:
|
||||
| entry \ current mode | Simple | Advanced |
|
||||
|---|---|---|
|
||||
| advanced (`: select 1`) | `: select 1` (one-shot) | `select 1` (SQL) |
|
||||
| simple (`create …`) | `create …` | `create …` |
|
||||
|
||||
## 5. Move success journaling worker → dispatch layer
|
||||
|
||||
**Remove** (worker stops journaling success):
|
||||
- `finalize_persistence` history write (db.rs:3096-3099). Keep yaml/csv.
|
||||
The now-unused `source` param: remove it + drop the arg at its ~30
|
||||
callers (mechanical, compiler-guided). (Handlers keep their own
|
||||
`source` for `snapshot_then`.)
|
||||
- The 4 no-op-skip `append_history` (db.rs:2267, 2311, 2524, 2560) — these
|
||||
outcomes (`SchemaSkipped` etc.) are `Ok` at the dispatch layer, so the
|
||||
new top-level journal covers them.
|
||||
- The 3 read-only helper `append_history` (db.rs:8372 show table, 9996
|
||||
show data, 10014 select) — `Ok(Query)`/`Ok(ShowList)` at the top.
|
||||
|
||||
**Add** (dispatch-layer journaling, all best-effort + logged):
|
||||
- **`spawn_dsl_dispatch`** (runtime.rs ~1433): pass `project_path` in;
|
||||
after `execute_command_typed`, `if outcome.is_ok() {
|
||||
Persistence::new(path).append_history(&source_for_journal,
|
||||
submission_mode.is_advanced()) }`. (Failures stay in the existing path,
|
||||
§6 — no double-journal, since Ok and Err are exclusive.)
|
||||
- **`run_replay`** (runtime.rs ~2540): after each line's
|
||||
`execute_command_typed`, `if outcome.is_ok() { append_history(
|
||||
&command_text, false) }` — replay is mode-agnostic, journalled
|
||||
**simple**. (Preserves ADR-0034 §3 "replayed sub-commands land in
|
||||
history"; a replayed advanced command re-journals without `:adv` — a
|
||||
documented OOS, not a regression: today it re-journals as plain `ok`.)
|
||||
- **`spawn_rebuild`** (runtime.rs ~503): after a successful rebuild,
|
||||
`append_history("rebuild"/source, false)`. (Rebuild journalled via
|
||||
`finalize_persistence` today; that write is gone, so add it here.)
|
||||
|
||||
**Unchanged** (already at the dispatch layer, app commands):
|
||||
- `perform_switch` (974: save-as/load/new) and `spawn_export` (1043) —
|
||||
already best-effort `append_history(&source)`; add the new `advanced`
|
||||
arg as `false` (app commands run in any mode → no `:` needed on recall;
|
||||
this also fixes the would-be "redundant `: undo`" — app commands
|
||||
journal **simple** because they're dispatched here, never via
|
||||
`ExecuteDsl`/the spawn).
|
||||
- `undo`/`redo`/`copy`/`help`/`quit`: not journalled today; unchanged.
|
||||
- The **`replay` command itself**: dispatched as `Action::Replay`, never
|
||||
reaches the spawn → not journalled (preserves the ADR-0034 §3 exclusion
|
||||
without extra work); nested `replay` skip in `run_replay` unchanged.
|
||||
|
||||
### DA-confirmed design choice: split, don't unify
|
||||
|
||||
Success journals in the spawn (`Ok` arm); **all** failures stay in the
|
||||
existing App→`JournalFailure`→runtime path (just gaining the mode).
|
||||
Considered and rejected: moving worker-rejection failures into the spawn
|
||||
too (to "unify"). It doesn't actually unify — parse failures never reach
|
||||
the spawn, so they'd stay in the App path regardless — and it adds a
|
||||
double-journal hazard (must also strip the App's `DslFailed`→
|
||||
`JournalFailure` emission). The split keeps the failure path **untouched
|
||||
in structure** (lowest risk); `Ok`/`Err` are exclusive so there is no
|
||||
double-journal. **Verified safe:** undo/redo never touches `history.log`
|
||||
(the snapshot copies db+yaml+csv only, undo.rs:15-16), and `snapshot_then`'s
|
||||
redo-clear keys on `source.is_some()`, independent of journaling — so
|
||||
removing the worker journal write does not perturb undo/snapshot at all.
|
||||
|
||||
## 6. Failure journaling — add the mode (location unchanged)
|
||||
|
||||
Keep both failure origins where they are (best-effort, dispatch/App
|
||||
layer); thread the mode so they tag `err:adv`:
|
||||
- **`Action::JournalFailure`** (action.rs:42): add `advanced: bool` (or
|
||||
`submission_mode`).
|
||||
- **`AppEvent::DslFailed`** (event.rs): add `submission_mode` (the
|
||||
worker-rejection path — the App can't recover the mode from an async
|
||||
reply otherwise).
|
||||
- **App**: the parse-failure path (`dispatch_dsl` Err arm) has
|
||||
`submission_mode` directly; the `DslFailed` handler reads it off the
|
||||
event. Both emit `JournalFailure { source, advanced }`.
|
||||
- **runtime.rs:492**: `append_history_failure(&source, advanced)`.
|
||||
|
||||
## 7. Tests
|
||||
|
||||
- **history.rs (Tier-1):** `status_token`/`parse_status` round-trip;
|
||||
`read_recent_sources` reconstructs `": …"` for `:adv` and leaves
|
||||
`ok`/`err` bare; `status_is_ok` true for `ok` & `ok:adv`; old-log
|
||||
back-compat.
|
||||
- **app.rs (Tier-1):** advanced submission stored `: `-prefixed; recall
|
||||
prepends in simple / strips in advanced; simple bare in both; bare `:`
|
||||
not stored; a parse-failure is still recallable; dedup/cap hold.
|
||||
- **iteration6_resume_history (Tier-3) — headline regression:** journal
|
||||
an advanced command (`append_history(text, true)`), hydrate, recall in
|
||||
simple → `: …`; and the full bug repro through `submit` + journal +
|
||||
hydrate if feasible.
|
||||
- **replay_command (Tier-3):** replayed commands still land in
|
||||
history.log (now via `run_replay`'s call); the `replay`-self-exclusion
|
||||
+ nested-skip still hold; advanced lines replay (status `ok:adv`
|
||||
treated as ok).
|
||||
- **Journaling relocation:** a success no longer fatals on a journal
|
||||
write failure (best-effort) — if cheaply testable; at minimum a worker
|
||||
test that previously asserted worker-side journaling is updated/removed.
|
||||
- **Update mechanical call sites:** `append_history(_, advanced)` /
|
||||
`append_history_failure(_, advanced)` at the db.rs inline tests
|
||||
(8372/9996/10014/11324 — likely now removed with the production sites),
|
||||
iteration6 (144-170), mod.rs (600).
|
||||
|
||||
## 8. ADR work
|
||||
|
||||
- **ADR-0052 (new):** the #30 feature + bug, the status-tag format, the
|
||||
`: `-prefixed ring + recall, AND the journaling relocation (it's the
|
||||
enabling refactor). Forks: status-tag format; unified scope;
|
||||
dispatch-layer journaling (best-effort).
|
||||
- **ADR-0015 §6 amendment:** history.log out of the worker transaction;
|
||||
commit-db-last now scopes yaml/csv/db; journal is a dispatch-layer
|
||||
best-effort side-record.
|
||||
- **ADR-0034 amendment:** journaling location (dispatch layer);
|
||||
status-field `:adv` extension (it already reserved the field).
|
||||
- **ADR-0040 amendment:** a success-path journal-write failure is no
|
||||
longer fatal — best-effort, consistent with the failure path.
|
||||
- README index upkeep for every ADR touched.
|
||||
|
||||
## 9. Risks / watch-list
|
||||
|
||||
- **Double-journaling**: ensure Ok→spawn and Err→App-path stay exclusive;
|
||||
do NOT also leave a worker journal.
|
||||
- **Under/over-journaling vs today**: top-level "journal on every Ok"
|
||||
must match today's "journal every command with a source" — verified:
|
||||
reads + skips are Ok outcomes, internal ops never reach the spawn.
|
||||
- **finalize_persistence source-param removal**: 30 mechanical call-site
|
||||
edits; compiler-guided.
|
||||
- **Replay re-journal mode fidelity**: replayed advanced commands
|
||||
re-journal as simple (OOS, not a regression).
|
||||
- **best-effort journal**: rare write-failure leaves a command unjournaled
|
||||
(logged). User decision (§2 open question).
|
||||
- **app-command mode**: journalled simple by construction (dispatched
|
||||
outside the spawn) — this is correct (they run in any mode), and
|
||||
resolves the earlier "redundant `: undo`" worry.
|
||||
@@ -0,0 +1,243 @@
|
||||
# Plan — ADR-0053: contextual `hint` command + F1 keybinding (H2)
|
||||
|
||||
Implements ADR-0053. Closes the last open piece of **A1** (the canonical
|
||||
app-command set) and requirements **H2**. No Gitea issue — this is
|
||||
requirements-driven work; any genuine "later" item found en route gets
|
||||
its own issue (cf. #36, already filed for the parallel `help`-side gap).
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Give learners on-demand, **teaching-grade** contextual help — a *third*
|
||||
tier beneath the existing terse always-on text (tier 1) and the
|
||||
short contextual lines that are already shown (tier 2: the live ambient
|
||||
prose, and the error `hint:` which is on by default since
|
||||
`Verbosity::Verbose` is the default). Two surfaces:
|
||||
|
||||
- **F1** (read-only overlay) → a tier-3 block for the **live partial
|
||||
input**, or — on empty input — for the **most recent runtime error**.
|
||||
- **`hint`** (submitted app command) → the tier-3 block for the **most
|
||||
recent runtime error** (the buffer is empty post-submit, so it can only
|
||||
act on recent context).
|
||||
|
||||
The mechanism is small; the **content corpus is the feature** (~80
|
||||
blocks, comprehensive for v1, authored exemplars-first per ADR-0053 D7).
|
||||
|
||||
## 2. The shape of the work (why this order)
|
||||
|
||||
The mechanism and the content are separable, and the mechanism should
|
||||
land first with **graceful tier-2 fallback** so every surface works
|
||||
before any tier-3 text exists. That lets us:
|
||||
|
||||
- build + test the trigger matrix / routing / `:`-strip / read-only-
|
||||
overlay behaviour against a skeleton (TDD), then
|
||||
- pour in content in reviewable batches without re-touching the wiring,
|
||||
- and turn on the **comprehensiveness coverage test** only once the
|
||||
corpus is complete (it is red until then — by design).
|
||||
|
||||
Build order: **Phase A** (mechanism skeleton, falls back to tier-2) →
|
||||
**Phase B** (catalogue structure + the three approved exemplars) →
|
||||
**Phase C** (comprehensive content, batched) → **Phase D** (polish:
|
||||
strip advertisement, snapshots, full green).
|
||||
|
||||
## 3. Grammar: the `hint_ids` field + the `HINT` node
|
||||
|
||||
### 3a. New `CommandNode.hint_ids` (per-form — revised in Phase B)
|
||||
- Add `pub hint_ids: &'static [&'static str]` to `CommandNode`
|
||||
(`src/dsl/grammar/mod.rs:512`, beside `help_id` / `usage_ids`),
|
||||
**mirroring `usage_ids`** — *not* a per-node `Option<&str>`. The Phase-B
|
||||
exemplar (`add 1:n relationship`) showed per-*node* keying is too coarse:
|
||||
`add`/`drop`/`show`/`create` are each one node spanning many forms, and
|
||||
a live-input hint must be specific to the typed form. Compiler forces
|
||||
every node literal (~37, across `grammar/app.rs`, `data.rs`, `ddl.rs`) to
|
||||
set it — Phase A/B leave most `&[]` (tier-2 fallback); Phase C fills them.
|
||||
**Multi-form nodes list ALL their form keys** (e.g. `add` →
|
||||
`["add_column", "add_relationship", "add_index", "add_constraint"]`) so
|
||||
the form-word disambiguation resolves correctly and unauthored forms fall
|
||||
back at render rather than mis-resolving to a sibling.
|
||||
- **Lookup:** `hint_key_for_input_in_mode(source, mode)` returns the single
|
||||
typed form's hint stem, reusing `pick_form_key` (factored out of
|
||||
`usage_key_for_input_in_mode` — shared digit/`m:n`/suffix disambiguation).
|
||||
- **Why a new field, not `help_id`** (ADR-0053 D3): `help_id` is `None` on
|
||||
the 7 advanced-SQL forms purely to dedup the `help` *list*; those forms
|
||||
have distinct SQL syntax and need their own block. `hint_ids` is per
|
||||
form. (The parallel `help`-side gap is issue #36; clause-concept hints
|
||||
are deferred — issue #37.)
|
||||
|
||||
### 3b. `AppCommand::Hint` + the `HINT` node
|
||||
- `AppCommand::Hint` variant (no fields — no topic arg) in
|
||||
`src/dsl/command.rs:544`.
|
||||
- `pub static HINT: CommandNode` in `grammar/app.rs` mirroring `HELP` but
|
||||
with **no topic shape** (bare keyword, like `UNDO`): `entry:
|
||||
Word::keyword("hint")`, `shape: EMPTY_SEQ` (as `UNDO`,
|
||||
`grammar/app.rs:333`), `ast_builder:
|
||||
build_hint` (returns `Command::App(AppCommand::Hint)`), `help_id:
|
||||
Some("app.hint")`, `hint_id: Some("app.hint")`, `usage_ids:
|
||||
&["parse.usage.hint"]`.
|
||||
- Register `(&app::HINT, CommandCategory::Simple)` in `REGISTRY`
|
||||
(`grammar/mod.rs`), beside `HELP`. (App commands are available in both
|
||||
modes via the existing mechanism.)
|
||||
|
||||
## 4. Command identification (live-input → node)
|
||||
|
||||
The F1 live-input path needs "which command form is being typed." **The
|
||||
lookup machinery already exists** — do not rebuild entry matching:
|
||||
- `command_for_entry_word(word) -> Option<(usize, &'static CommandNode)>`
|
||||
(`grammar/mod.rs:811`) returns the matched node for an entry word
|
||||
(Simple-first; the caller extracts the first word of the input).
|
||||
- `usage_keys_for_input_in_mode(source, mode)` (`grammar/mod.rs:564`)
|
||||
already performs the **mode-aware** Simple/Advanced selection the hint
|
||||
path needs (advanced `create` → the SQL nodes, simple → the DSL node) —
|
||||
it just returns `usage_ids` rather than the node.
|
||||
- **The only new bit:** a thin `hint_id_for_input_in_mode(source, mode)`
|
||||
(or a node-returning sibling of `usage_keys_for_input_in_mode`) that
|
||||
applies the same mode selection and returns the chosen node's
|
||||
`hint_id`. Mirror the existing function; don't duplicate its matching.
|
||||
- **`:`-strip:** in Simple mode, strip a leading `:` (one-shot escape,
|
||||
ADR-0003) before identification so `: SELECT …` resolves to the
|
||||
advanced `SELECT` node.
|
||||
- No match (empty / unrecognised entry word) → the "getting started"
|
||||
pointer (D2).
|
||||
|
||||
## 5. F1 keybinding (read-only overlay)
|
||||
|
||||
In `App::handle_key` (`src/app.rs:1155`):
|
||||
- Add an F1 arm (`KeyCode::F(1)`) **after** the modal gate and the
|
||||
sidebar-nav gate (inert there, per D2), and **before** the
|
||||
"any other key clears the completion memo" fall-through (`_ =>
|
||||
self.last_completion = None`, ~line 1228) — F1 must **not** clear the
|
||||
memo or touch the buffer/cursor (D1).
|
||||
- Behaviour (the trigger matrix, D2):
|
||||
- non-empty input → `note_hint_for_input()` (the command's `hint.cmd`
|
||||
block + the live "Next:" expected-set from the walker).
|
||||
- empty input + `last_error_hint_key` set → `note_hint_for_error()`.
|
||||
- empty input + no recent error → `note_getting_started()`.
|
||||
- Returns `Vec::new()` (pure output emission, like `help`).
|
||||
- `demo_badge_label` (`app.rs:520`) gains an `F1 → "[F1]"` entry so demo
|
||||
mode surfaces it (ADR-0047).
|
||||
|
||||
## 6. The two error routes (D2 / D5)
|
||||
|
||||
- **Runtime errors:** add `last_error_hint_key: Option<String>` to `App`.
|
||||
Set it where friendly errors are rendered (`runtime.rs:2615`,
|
||||
`app.rs:2424`) from the error's class key; clear on the next successful
|
||||
command. The `hint` command and empty-input F1 read it.
|
||||
- **Pre-submit diagnostics:** the F1 live-input path, when the input
|
||||
carries an under-cursor diagnostic, reads it straight from the walker
|
||||
(`input_diagnostics_in_mode`, the same source the ambient panel uses)
|
||||
and renders that diagnostic's `hint.err.<class>` block instead of (or
|
||||
alongside) the command block. No stored state.
|
||||
- Both render from `hint.err.*`.
|
||||
|
||||
## 7. Rendering: the `note_hint*` family (D4)
|
||||
|
||||
- New `App::note_hint_for_input`, `note_hint_for_error`,
|
||||
`note_getting_started` (siblings of `note_help`/`note_help_topic`,
|
||||
`app.rs:2982`/`3021`).
|
||||
- A tier-3 block is **structured** (`what` / `example` / `concept`, plus
|
||||
the live `Next:` line on the input path). The catalogue stores each part
|
||||
under sub-keys (`hint.cmd.<id>.what`, `.example`, `.concept`); the
|
||||
renderer fetches each via `t!` and lays them out as a small framed
|
||||
block.
|
||||
- Styling: `OutputKind::System`; `OutputStyleClass::Hint` (muted) on
|
||||
`what`/`concept`/`Next`, `Neutral` on `example` so the runnable line
|
||||
stands out. Reuse `OutputLine::styled` + `push_category_three_prose`
|
||||
patterns (`app.rs:3121`).
|
||||
- **Fallback:** if a node's `hint_id` is `None` or a key is missing,
|
||||
degrade to tier-2 (ambient prose for the input path; the verbose error
|
||||
`hint:` for the error path) — never blank.
|
||||
|
||||
## 8. Catalogue + `keys.rs`
|
||||
|
||||
- New sub-namespaces under the existing top-level `hint:` in
|
||||
`src/friendly/strings/en-US.yaml`: `hint.cmd.<hint_id>.{what,example,
|
||||
concept}` and `hint.err.<class>.{what,example,concept}`.
|
||||
- Register every key + its placeholders in `src/friendly/keys.rs`
|
||||
(`KEYS_AND_PLACEHOLDERS`) so the build-time validation covers them.
|
||||
- `parse.usage.hint` + `help.app.hint` strings for the command itself.
|
||||
|
||||
## 9. Content (Phase C — the bulk, batched per D7)
|
||||
|
||||
Exemplars approved in the ADR (`insert` live-input, FK child-side error,
|
||||
`add relationship`) are the template. Author in reviewable batches:
|
||||
1. **App commands** (~16): save/save as/load/new/rebuild/export/import/
|
||||
replay/undo/redo/mode/messages/copy/help/hint/quit.
|
||||
2. **DDL** (simple): create table, create m:n, add column/relationship/
|
||||
index, drop, rename, change column.
|
||||
3. **DML** (simple): insert, update, delete, show, seed, explain,
|
||||
select/with.
|
||||
4. **Advanced-mode SQL forms** (7): SQL CREATE TABLE, ALTER TABLE,
|
||||
CREATE/DROP INDEX, DROP TABLE, SQL INSERT/UPDATE/DELETE, EXPLAIN SQL —
|
||||
**own blocks, SQL-syntax examples**.
|
||||
5. **Runtime error classes** (9): unique, foreign_key ×{child,parent},
|
||||
not_null, check, type_mismatch, not_found, already_exists, generic,
|
||||
invalid_value.
|
||||
6. **`diagnostic.*` classes** (~33): arity/type/unknown-table-column/etc.
|
||||
|
||||
Each block: `what` (1–2 sentences), `example` (one runnable line,
|
||||
mode-correct), `concept` (the relational idea — the teaching part;
|
||||
optional only where genuinely none, e.g. `quit`).
|
||||
|
||||
## 10. Tests
|
||||
|
||||
Written test-first against the Phase-A skeleton where possible.
|
||||
|
||||
- **Tier 1 (unit, `app.rs`):**
|
||||
- trigger matrix: F1 non-empty → command block; F1 empty + recent error
|
||||
→ error block; F1 empty + none → getting-started; `hint` command +
|
||||
error → error block; `hint` + none → getting-started.
|
||||
- `last_error_hint_key` set on a failing command, cleared on the next
|
||||
success.
|
||||
- routing: a pre-submit diagnostic on the input drives the diagnostic
|
||||
`hint.err`; a runtime error drives the stored-key route.
|
||||
- `:`-strip: `: SELECT …` in Simple mode resolves to the advanced node.
|
||||
- **read-only overlay:** F1 leaves `input`, `input_cursor`, and
|
||||
`last_completion` unchanged.
|
||||
- tier-2 fallback when `hint_id`/key absent.
|
||||
- **Tier 2 (`insta`):** snapshot a representative rendered tier-3 block
|
||||
(the `insert` exemplar) so the framed layout + styling spans are locked.
|
||||
- **Tier 3 (integration, `tests/it/`):** type a partial command → F1 →
|
||||
block appears, buffer untouched; run a failing insert → `hint` → FK
|
||||
error expansion.
|
||||
- **Comprehensiveness coverage test** (enforces D6, the key one): iterate
|
||||
`REGISTRY` and assert every node has a `hint_id` resolving to a
|
||||
`hint.cmd.*` block; assert every runtime-error + `diagnostic.*` class
|
||||
has a `hint.err.*` block. **Red until Phase C completes** — enable
|
||||
(un-`ignore`) as the final gate.
|
||||
- `keys.rs` validation continues to guarantee every *referenced* key
|
||||
resolves.
|
||||
|
||||
## 11. Keybinding strip + discoverability (Phase D)
|
||||
|
||||
- The ADR-0051 bottom strip advertises **F1 = hint** in the editing/
|
||||
typing state (and on the empty-input state, since F1 still does
|
||||
something there). Re-accept the affected full-panel snapshots.
|
||||
|
||||
## 12. ADR / docs
|
||||
|
||||
- ADR-0053 is committed (`e16ad50`). On completion, flip its Status from
|
||||
"implementation pending" to implemented (with date), and update the
|
||||
README index entry + `requirements.md` **H2 → [x]** and **A1 → [x]**
|
||||
(A1 closes when `hint` lands).
|
||||
|
||||
## 13. Risks / watch-list
|
||||
|
||||
- **Command-identification reuse.** The lookup exists
|
||||
(`command_for_entry_word` + the mode-aware `usage_keys_for_input_in_mode`,
|
||||
`grammar/mod.rs:811`/`564`); the only new code is a thin node/`hint_id`
|
||||
variant that reuses their selection. Do **not** re-implement entry-word
|
||||
matching — mirror the existing functions.
|
||||
- **Structured-key ergonomics.** Three sub-keys per block × ~80 blocks is
|
||||
~240 catalogue keys; keep the `keys.rs` registration generation tidy
|
||||
(consider a helper that registers the `{what,example,concept}` triple
|
||||
for an id).
|
||||
- **Content voice drift across batches.** Re-check each batch against the
|
||||
approved exemplars; the `concept` line is where drift (too terse / too
|
||||
advanced) creeps in. Pedagogy wins ties.
|
||||
- **F1 terminal capture.** A few terminals intercept F1; acceptable
|
||||
(it's the convention) but note it if testing surfaces it.
|
||||
- **Snapshot churn.** The strip change re-accepts ADR-0051 snapshots;
|
||||
keep that diff isolated.
|
||||
- **Coverage-test timing.** It is red through Phases A–C; gate it so CI
|
||||
isn't broken mid-stream (e.g. `#[ignore]` until the final batch), then
|
||||
make passing it the completion criterion.
|
||||
```
|
||||
+14
-3
@@ -147,11 +147,19 @@ since ADR-0027.)
|
||||
cursor editing and is complete on its own terms; the separate
|
||||
**multi-line** entry goal is tracked under I1, which is
|
||||
genuinely not started.)*
|
||||
- [ ] **I1b** Readline-style cursor shortcuts: Ctrl-A / Ctrl-E
|
||||
- [x] **I1b** Readline-style cursor shortcuts: Ctrl-A / Ctrl-E
|
||||
as aliases for Home / End for users on keyboards without those
|
||||
keys (and for ergonomics in command-driven workflows). Likely
|
||||
followed by Ctrl-W (delete previous word), Ctrl-K (delete to
|
||||
end), Ctrl-U (delete to start). Pending.
|
||||
end), Ctrl-U (delete to start).
|
||||
*(Done 2026-06-12 — ADR-0049, issue #29: the full set —
|
||||
Esc-clear + Ctrl-A/E/W/K/U — wired in `App::handle_key`
|
||||
(`src/app.rs`) with helpers `clear_input` / `delete_prev_word`
|
||||
/ `kill_to_end` / `kill_to_start`; Esc clears only when no
|
||||
completion memo is alive (the memo wins first, ADR-0022);
|
||||
cursor-only keys leave history navigation intact, kill keys
|
||||
end it; 22 Tier-1 tests. On-screen advertisement of these keys
|
||||
is issue #27's bottom-status-line work.)*
|
||||
- [x] **I2** Persistent navigable input history (project-scoped).
|
||||
*(Implemented across Iterations 2 + 6: per-command append to
|
||||
`history.log` (Iter 2); on project open, the in-memory
|
||||
@@ -696,7 +704,10 @@ since ADR-0027.)
|
||||
`Generator`, and full completion / highlight / validity / help /
|
||||
parse-error-pedagogy wiring. Deferred SD2 increments:
|
||||
user-defined custom generators, NULL injection, multi-locale,
|
||||
recursive parent auto-seed.)*
|
||||
recursive parent auto-seed. Later catalogue refinements:
|
||||
**#33** year-as-int (`year`/`*_year`/`published`/`founded`) and
|
||||
**#34** conventional choice sets (`priority`/`severity`/`rating`,
|
||||
`status` excluded) — ADR-0048 Amendment 1.)*
|
||||
|
||||
## Query analysis
|
||||
|
||||
|
||||
Reference in New Issue
Block a user