a55b6a7a059d9829874cc78431269d20fd6d9d36
65 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a55b6a7a05 |
remove q quit alias
`q` was introduced in round-5 as a peer Keyword variant alongside `quit`. Per ADR-0023's "alias miss" critique, that was the wrong shape — it surfaced `q` as a standalone command in completion (only one of its kind), and required parallel parser + usage + catalog + test entries. Drops the Keyword variant entirely; if this ever needs to come back, it should arrive as an alias annotation per ADR-0023, not as a peer keyword. Tests still 769 passing. |
||
|
|
6ca297579e |
round-5 follow-up r2: migrate all thiserror Display attributes to catalog
Completes the i18n sweep started in the previous commit. All
remaining hand-rolled user-facing English strings inside
thiserror #[error(...)] attributes have been moved into the
catalog. Drops the thiserror dependency entirely.
Twelve error types migrated:
- dsl::action::UnknownAction → parse.custom.unknown_action
- dsl::parser::ParseError → parse.error_wrapper + parse.empty
- dsl::value::ValueError → value.{type_mismatch,format}
- persistence::csv_io::CsvError → persistence.csv.*
- persistence::mod::PersistenceError → persistence.{io,encode}
- persistence::yaml::YamlError → persistence.yaml.*
- persistence::migrations::MigrateError → persistence.migrate.*
- project::lock::LockError → project.lock.*
- project::naming::NamingError → project.naming.*
- project::naming::UserNameError → project.user_name.*
- project::mod::ProjectError → project.{path_not_found,...}
- project::mod::SafeDeleteError → project.safe_delete.*
- archive::ArchiveError → archive.*
- cli::ArgsError → cli.*
- db::DbError → db.error.*
Pattern per type: drop thiserror::Error derive, write manual
Display calling crate::t!(), keep #[from] semantics via
explicit From impls, override Error::source() where applicable
so #[source]-style chaining is preserved.
Why this matters (user rationale): "fine to have fallbacks for
errors that are purely technical, but lift the output to a
place where it can be localized later and where an adjustment
with friendly text is easily possible if any of them become
part of the happy path." All surface strings now live in
en-US.yaml and can be reworded or localized without touching
Rust source.
Tests: 769 passing, 0 failed, 1 ignored. Clippy clean with
-D warnings. Cargo.toml: drop thiserror = "2.0.18".
|
||
|
|
1e06490572 |
round-5 follow-up: completion + i18n sweep
Four user-reported gaps from the round-4 testing pass:
1. Empty-prompt hint reworded from "(no active hint)" to
"Type a command — press Tab for options, `help` for a
list" (6 snapshots updated to reflect 80-col truncation).
2. App-lifecycle commands (quit/q, help, rebuild, save/save as,
new, load, export, import, mode, messages) now flow through
the DSL parser:
- 15 new keywords + catalog token entries
- new Command::App(AppCommand) AST with 11 variants
- parse-first dispatch in submit() (app commands work in
both simple and advanced modes)
- pre-chumsky source-slice for `export <path>` /
`import <zip> [as <target>]` mirrors the replay precedent
- UsageEntry registry entries so parse errors surface
relevant usage templates
- `mode <bad>` / `messages <bad>` use try_map for the
friendly "unknown mode/messages" wording
3. DSL completion gaps:
- `1:n` surfaces as a composite candidate at `add `
- --all-rows / --create-fk / --force-conversion /
--dont-convert surface as new CandidateKind::Flag
candidates (coloured with tok_flag in hint panel)
- filter_clause .labelled() wrap removed so chumsky's
expected-set surfaces the constituent options
4. Hardcoded user-facing strings migrated to catalog:
- 4 parser custom errors (incl. the known "tables need at
least one column" wart)
- UnknownType Display now via parse.custom.unknown_type
- UI panel titles + mode labels (Output / Hint / SIMPLE /
ADVANCED / Advanced:)
- app.rs cascade rendering (action labels + summary)
- runtime --resume CLI stderr
- db.rs change-column diagnostic tables (7 headers + 3
wrapper summaries + force-conversion hint)
Tests: 765 → 769 passing, 0 failed, 1 ignored (same doctest
as before). Clippy clean with -D warnings.
Deferred:
- ~25 thiserror #[error] attributes still hand-rolled
(DbError, ArgsError, ArchiveError, PersistenceError,
LockError). Tracked separately.
- DSL/SQL relationship in advanced mode — clarified
implicitly via parse-first dispatch; broader ADR
amendment to follow.
- Post-complete-parse completion gap (e.g. `save ` Tab
can't offer `as` because `save` parses bare; same shape
as `--create-fk` after a complete `add relationship`).
|
||
|
|
1eb2e0d01f | handoff | ||
|
|
c247f55094 |
ADR-0022 follow-up r4: column-type completion
Round-4 user finding: typing `(de` at a column-type slot
showed the parser's "unknown type 'de'" error and Tab did
nothing — completion was blind to the type vocabulary
entirely.
Root cause: type names are NOT in the Keyword enum (ADR-0020
§2 — they remain identifiers, validated by Type::from_str),
so the keyword-iter path in candidates_at_cursor missed
them. The schema-identifier path also missed them (they're
not in the schema cache).
Fix: when the parser's expected-set contains the `"type"`
label (from `ident_inner().labelled("type")` inside
`type_keyword`), produce candidates from `Type::all()`
filtered by the partial prefix. Centralised as
`TYPE_SLOT_LABEL` constant so the parser and the completion
engine agree on the magic string.
Candidates appear in `Type::all()` declaration order
(text/int/real/decimal/bool/date/datetime/blob/serial/
shortid) — matching ADR-0005's pedagogical grouping. Coloured
as Keyword (purple) since type names are closed-set
grammar, not user content.
Verified end-to-end:
- `(de` → ["decimal"] (single match → Tab inserts with space)
- `(da` → ["date", "datetime"] (multi → cycles)
- `(sh` → ["shortid"]
- `(` → all 10 types in declaration order
- `(var` → [] (no Tab candidates; parser custom error fires on submit)
Tests: 760 passing, 0 failing, 1 ignored (755 baseline +5
new type-slot cases). Clippy clean.
|
||
|
|
22119d6a4e |
ADR-0022 follow-up r3: identifier colour, NewName hint, "Next:" wording, "type" label
Three fixes from a third round of real testing. 1. **tok_identifier vivid (round-3 #1).** The cool grey-blue from r2 was still too close to theme.fg to register as distinct. Bumped to cyan-teal (#56B6C2 dark / #0F6B76 light) — identifiers are the user's most "special" content and now read that way against keywords (purple), numbers (orange), strings (green), and flags (amber). 2. **"Type a name" hint at NewName slots (round-3 #2).** New `completion::typing_name_at_cursor(input, cursor)` returns `Some(TypingName)` when the cursor sits at — or inside — an `IdentSlot::NewName` position. It probes by substituting a single-letter placeholder identifier and re-parsing to discover what the parser would expect AFTER the name; the hint then reads "Type a name, then `(`" instead of the technical "next: `(`" that surfaces once the partial identifier has been consumed by the live parser. When the probe yields nothing useful (custom errors with empty expected, or a complete-on-substitute case), falls back to "Type a name". New catalog keys hint.ambient_typing_name and hint.ambient_typing_name_then. Wired into ambient_hint between the candidate-list and invalid-ident checks. 3. **"Next:" instead of "expected:" wording.** "Expected" read as a leaked diagnostic; "Next:" is shorter, conversational, and consistent with the action-oriented voice of "Submit with Enter" and "Type a name". Hint sentences now also start capitalised (Submit/Next/Type/No-such), per the user's Capital-T-on- "type a name" preference. 4. **type_keyword labelled "type".** Without a label, the `select_ref!` over an Identifier token produced `RichPattern::SomethingElse`, which rendered as the meaningless "something else" in the hint after `(`. Labelled now: error reads "Next: type" — terse but honest. The label is applied BEFORE try_map (not after, not via as_context) so the existing custom-error wording for unknown types ("unknown type 'varchar' (expected one of: …)") still surfaces unchanged. Tests: 755 passing, 0 failing, 1 ignored (no net change — +5 typing_name cases, -0 net since one test was reworded for capitalisation rather than added). Clippy clean. Smoke probe verifies: "add column to table T: " → "Type a name, then `(`"; "add column to table T: Name (" → "Next: type"; "show data Custp" → "No such table: `Custp`"; valid input → "Submit with Enter". Note for next testing round: parser-side custom errors (e.g. the "tables need at least one column" message that fires for `create table Customers `) still read in lowercase — they're hand-written in parser.rs source rather than via the catalog. If the lowercase "tables need…" intrusion bothers you, easy follow-up. |
||
|
|
f94a999e66 |
ADR-0022 stage 8 follow-up r2: completion UX fixes from real testing
Two concrete behaviour changes from the user's second testing
round:
1. **Single vs multi commit paths.** Previously every Tab,
even single-candidate, created a memo so Esc/Backspace could
undo. The downside: with one candidate, repeated Tab "cycled"
through the same item invisibly — looked stuck. Now:
- Single candidate → insert with trailing space, no memo.
The user can keep typing or hit Tab again to fresh-complete
at the new cursor. (Trade-off: Esc/Backspace no longer
whole-span undo for unique completions; the user accepted
this for the chained-Tab fluency.)
- Multi candidate → insert WITHOUT trailing space, create
memo for cycling. The natural commit gesture is space —
pressing it clears the memo and inserts the space normally,
producing "<chosen> " ready for the next position.
The "stuck on unique" symptom goes away, and the missing
trailing space on multi-Tab signals "you're picking; press
space when you're done" without needing modal affordances.
2. **Keyword candidates in grammar order.** Dropped the
alphabetical sort in `describe_expected` in favour of
chumsky's native source-order traversal of `or_not`/`choice`
chains — empirically this matches the canonical command
shape. Result: `add column ` now offers `to` before
`table` (as `add column [to] [table] <Table>:…` reads),
not `table` before `to` which previously suggested the
nonsensical `add column table to ...`. Identifiers still
alphabetised within their group; entry-keyword fallback
for the no-prefix case stays alphabetical (no source order
when 10 separate command branches).
Tests: 750 passing, 0 failing, 1 ignored (747 baseline →
+3 net: replaced single-candidate Esc/Backspace tests with
new multi-candidate variants; added the unique-Tab-chains-
naturally case that drove the round-2 fix; kept the
keywords-in-grammar-order test updated to assert
`to`/`table`/identifiers ordering).
|
||
|
|
bd1cce672d |
ADR-0022 stage 8 follow-up: fixes from real-app testing
Three fixes from the user's testing run, plus an investigation note on a fourth. #4 Sticky hint during cycling. The previous code recomputed candidates_at_cursor at the post-Tab cursor position, which made the panel whiplash through "what comes next at the new cursor" between cycles. ambient_hint now short-circuits to the memo's stored candidate list while the memo is alive — so Tab Tab Tab keeps showing the same list with the selection moving, then snaps to the post-Tab ambient state once any non-Tab key clears the memo. #2 Candidate ordering and kind-coloured rendering. New `Candidate { text, kind: Keyword|Identifier }` carries the classification through completion, last-completion memo, and ambient-hint payload. candidates_at_cursor now sorts keywords first (alphabetical), identifiers second (alphabetical), and the hint-panel renderer colours keywords in `tok_keyword` and identifiers in `tok_identifier`. Keyword-vs-identifier name collisions resolve in favour of the keyword (rare; the user can still address their table via different syntax). #3 tok_identifier no longer matches theme.fg. Identifiers in the input pane now render in a distinct cool grey-blue (dark) / dark steel-blue (light), so they stand out from prose-like default text without competing with keyword purple. Same colour drives the identifier candidates in the hint panel for visual consistency input ↔ hint. Limitation worth knowing: "keywords first, alphabetical" is not the same as grammatical order. For "add column " the hint shows `table to` not `to table` — chumsky's expected-set doesn't preserve combinator-source order, and encoding it in the registry adds maintenance overhead the fix doesn't cleanly justify. Marked for future revisit if it bites. #1 (Tab does nothing on "add column ") — not reproduced through App::update. The internal logic works correctly: "add column " + Tab inserts "Customers ", second Tab cycles to "Orders ", third to "Thing ". The most likely explanation is a stale binary or a terminal-level event intercept (tmux focus, kitty-keyboard protocol differences, etc.) — needs user verification with a fresh build. Tests: 747 passing, 0 failing, 1 ignored (744 baseline → +3: 2 new completion-ordering cases including the keyword-wins-on-name-collision edge, plus 1 hint-mid-cycle sticky test). Clippy clean. |
||
|
|
8214e4136a |
ADR-0022 stage 8e: invalid-identifier detection + hint variant
Per the user's #5: "if our candidate selection works correctly, then entering a character that removes all matches is the same as entering an invalid token." Closes the loop between schema cache (8c/8d) and live error feedback (4). New `completion::invalid_ident_at_cursor(input, cursor, cache)` returns `Some(InvalidIdent { range, found, slot })` when: - the cursor is on a partial identifier-shaped token; - the parser's expected-set at the start of that token contains a known-set IdentSlot (TableName / Column / RelationshipName); - no schema entry across those slots prefix-matches the typed text. `render_input_runs` extended to take a `&SchemaCache` and overlay the invalid-identifier range with `tok_error` — same visual treatment as the parse-error overlay (4), unified red signal regardless of which detector fires. `ambient_hint` extended to surface `hint.ambient_invalid_ident` when invalid_ident_at_cursor returns Some — wording "no such {kind}: `{found}`" mirrors ADR-0019's engine-error voice for consistency. Catalog + KEYS_AND_PLACEHOLDERS declaration added; validator passes. Render priority: candidates win over invalid-ident (if any schema match exists for the partial prefix, the state is "in-progress completion" not "invalid"). Falls through to the existing parse-error/incomplete/Valid framings otherwise. NewName slots are filtered out at the source — typing into a "user invents this name" position is never invalid (per `IdentSlot::completes_from_schema`). Tests: 744 passing, 0 failing, 1 ignored (738 baseline → +6: 5 invalid_ident_at_cursor cases covering unknown-prefix-fires, prefix-match-doesn't-fire, NewName-immune, no-cursor-token, keyword-slot-immune; plus 1 ambient_hint integration test). Clippy clean. This closes ADR-0022. Stages 1-8e together deliver the ambient-typing-assistance feature: token highlighting, error overlay, hint panel ambient, hint panel multi- candidate display with scroll markers, Tab/Shift-Tab cycling with one-keystroke Esc/Backspace undo, schema-aware identifier completion, and invalid-identifier live feedback. Total stage-8 footprint: 5 commits, ~1600 lines. |
||
|
|
7a32c13bd5 |
ADR-0022 stage 8d: schema cache refresh wiring
New `AppEvent::SchemaCacheRefreshed(SchemaCache)` event + App handler that stores it on `app.schema_cache`. Runtime helper `refresh_schema_cache(database, event_tx)` fetches table / column / relationship names via the `list_names_for` worker request (added in stage 7) and posts the assembled cache. Wired into every site that already posts `TablesRefreshed`: - `seed_initial_tables` (initial project load). - Project-switch path in `handle_project_switch`. - `RebuildSucceeded` path. - Post-DDL path (`spawn_command`). - Post-replay path. Result: schema-aware identifier completion (added in 8c) becomes live — Tab on `show data ` offers the actual table names from the current project, `drop column from T: ` (or similar) offers existing columns, etc. The cache stays fresh across DDL and rebuild without per-keystroke worker round-trips (one refresh per schema-mutating action is amortised across many subsequent keystrokes). Best-effort: a failed `list_names_for` for any individual slot kind leaves that field empty in the cache rather than suppressing the whole refresh — partial completion beats no completion. Tests: 738 passing, 0 failing, 1 ignored (unchanged total — this stage is wiring, not new test surface; the synthetic-cache tests from stage 8c remain the regression net for the completion logic itself). Clippy clean. |
||
|
|
51a8d9ac44 |
ADR-0022 stage 8c: IdentSlot propagation + SchemaCache API
`IdentSlot` gains `expected_label()` and the round-trip
`from_expected_label()`. The four slot kinds map to the
user-facing labels "identifier" (NewName), "table name",
"column name", "relationship name".
`ident_ctx(slot)` now actually applies `slot.expected_label()`
as the chumsky label (was documentation-only after stage 6).
Parser errors and the hint panel's "expected: …" prose now
read with the slot-specific name: "expected table name"
instead of the generic "expected identifier". One parser
test updated accordingly; the four catalog `parse.token.*`
keys are unaffected (the slot labels are a parallel surface).
New `completion::SchemaCache { tables, columns,
relationships }` struct + `for_slot(slot) -> &[String]`
accessor. Empty by default; runtime wiring lands in a
follow-on substage. NewName slots return `&[]`
unconditionally.
`candidates_at_cursor` extended to accept `&SchemaCache`:
when the parser's expected-set includes a slot label,
schema candidates from the cache are added alongside the
keyword candidates. Both sources are then prefix-filtered,
combined, sorted, deduplicated. App::schema_cache field
threaded into both the App-side completion paths and the
ambient_hint computation in ui.
Tests: 738 passing, 0 failing, 1 ignored (730 baseline →
+8: 2 IdentSlot label round-trip tests, 6 completion-with-cache
cases covering table/column/relationship slots, prefix
filtering, empty cache, and NewName-no-candidates).
Clippy clean.
User-visible: identifier completion infrastructure is in
place but the cache is always empty — runtime wiring (the
next substage) will populate it on project load and after
successful DDL, at which point Tab on identifier slots
starts offering schema names.
|
||
|
|
faebeed588 |
ADR-0022 stage 8b: hint panel candidate list with scroll markers
Refactor `ambient_hint` to return a richer enum:
- `Prose(String)` — the existing single-line hint (Valid /
incomplete-with-no-keywords / definite-error states);
- `Candidates { items, selected }` — multi-candidate (or
single-candidate) keyword completion at the cursor.
When `candidates_at_cursor` returns Some, the new
`Candidates` variant wins over the prose framing — the
candidate list is more actionable than "expected: `data` or
`table`". `selected` tracks the live `LastCompletion` memo's
selection_idx for the renderer to highlight.
`render_candidate_line` (new helper in ui.rs):
- All items fit → render space-separated; selected item
rendered bold + theme.fg, others theme.muted.
- Overflow → window centred on the selected item (or
item 0 with no selection); `< ` / ` >` markers at the
edges (per the user's #2). Window expands right-first
then left-first to use available width.
- Returns `Line<'static>` (items cloned into spans) so the
caller doesn't fight lifetimes between the
AmbientHint::Candidates payload and the rendered Line.
Updated callers in ui.rs and input_render tests for the new
signature. Added `ambient_hint_with_memo_carries_selected_index`
test asserting the renderer-side `selected` plumbing.
Tests: 730 passing, 0 failing, 1 ignored (728 baseline →
+2 net: -3 reworked + 5 new candidate-related cases).
Clippy clean.
Stage 8c will plumb identifier completion (schema cache +
candidate fetch from worker on demand or pre-cache) and add
the invalid-identifier hint variant.
|
||
|
|
06e8d1e769 |
ADR-0022 stage 8a: non-modal keyword completion + Esc/Backspace undo
Per the user's framing decision: there is no "completion
mode." Tab is just an action that consumes whatever is
expected at the cursor, and the existing always-on hint
panel (stage 5) tells the user what's available.
New `completion` module: `candidates_at_cursor(input,
cursor)` returns a `Completion { replaced_range,
partial_prefix, candidates }` based on the parser's
expected-token set at the cursor position. Filters to bare
keyword candidates only (no punctuation, no descriptive
labels), narrowed by the typed prefix (case-insensitive).
`LastCompletion` memo struct on `App::last_completion`
carries the cycle state: inserted_range, original_text,
candidates, selection_idx. Wrap-around forward/backward
indices.
App key handling (added before the existing matcher):
- Tab → cycle forward if memo present; else insert first
candidate; create / advance memo.
- Shift-Tab → cycle backward if memo present; else
insert last candidate (alphabetically) so the user can
jump to the end without cycling through everything.
- Esc / Backspace while memo alive → restore
original_text in inserted_range, place cursor at the
pre-Tab position, clear memo.
- Any other key → clear memo, then process normally.
The user's symmetry preference was load-bearing here:
"insert with one keystroke, remove with one keystroke."
Both Esc and Backspace honour that — multiple Tab cycles
collapse into one undo. Documented inline.
A single-candidate completion still creates a memo so
Esc/Backspace can undo it. Multiple Tabs in a row cycle
through the candidate list with wrap-around at both ends
(per the user's #2).
Tests: 728 passing, 0 failing, 1 ignored (705 baseline →
+23: 13 completion module + 10 app integration tests
covering Tab, Shift-Tab, cycling, wrap-around, Esc-undo,
Backspace-undo, multi-Tab-then-Esc, memo invalidation by
typing or cursor movement). Clippy clean.
Stage 8b will add multi-candidate hint-panel rendering
with scroll markers (`<` `>`) per the user's #2. Stage 8c
will plumb in identifier completion + invalid-identifier
detection.
|
||
|
|
aea3224da2 |
ADR-0022 stage 7/8: schema query plumbing
Add `Request::ListNamesFor { slot, reply }` and the public
`Database::list_names_for(slot)` method. The completion
engine in stage 8 calls this on Tab when the cursor sits
on an identifier-typed slot.
Worker dispatch:
- TableName → user tables (filters __rdbms_*).
- Column → distinct column names across all user tables
(v1 simplification per the stage 6 IdentSlot note: no
table-context binding; the schema-completion engine in
stage 8 may refine).
- RelationshipName → relationship names from the
__rdbms_playground_relationships metadata table.
- NewName → short-circuited at the public method (no
worker round-trip).
Names are returned alphabetised + deduplicated. Filters
respect ADR-0002 — internal __rdbms_* tables never reach
the completion menu (covered by a regression test).
Tests: 705 passing, 0 failing, 1 ignored (700 baseline →
+5 list_names_for cases). Clippy clean.
Stage 8 wires this into the App as a Tab-triggered
completion mode. Note for the next session: stage 8 is by
far the largest of the eight stages — it touches App state
(completion mode), event routing (Tab/arrow/Enter/Esc/letter
behaviour while in completion mode), hint-panel render
variant, candidate filtering, integration tests. Several
fine-grained UX decisions (cursor position after accept,
panel height when candidate list overflows, what closes
the mode) want explicit user input rather than agent
guesswork. See "Stage 8 open questions" in the next
handoff for the list.
|
||
|
|
6845df1475 |
ADR-0022 stage 6/8: IdentSlot taxonomy + parser audit
New `dsl::ident_slot` module: IdentSlot enum with four variants — NewName (user invents), TableName (existing), Column (existing), RelationshipName (existing). Plus `completes_from_schema()` accessor for the completion engine in stage 8. Deliberate v1 simplification vs. ADR-0022 §8: no TableRef binding for Column. The completion engine in stage 8 will either union all columns or determine the table from the consumed prefix heuristically. The TableRef wrinkle returns if/when stage 8 needs it. Parser audit: renamed bare `ident()` → `ident_inner()` (now private-by-convention) and introduced `ident_ctx(slot)` wrapper. Every command parser combinator was audited and each `ident()` call site replaced with the appropriate `ident_ctx(IdentSlot::…)`: - create_table table-name → NewName - drop_table → TableName - add_column → TableName + NewName - drop_column → TableName + Column - rename_column → TableName + Column + NewName - change_column → TableName + Column - show_data / show_table → TableName - insert column-list → Column; insert table → TableName - update set-LHS → Column; update target → TableName - delete target → TableName - where-clause LHS → Column - relationship `as <name>` → NewName - drop relationship by name → RelationshipName - qualified_column → TableName + Column - with_pk_clause spec name → NewName The slot tag is currently documentation-only — the wrapper ignores it and returns ident_inner() unchanged. The audit's value is ensuring every call site has explicit intent recorded co-located with the parser combinator. The completion engine in stage 8 will start consuming the slots either by re-parsing with awareness or by an explicit parser-side propagation refactor. Tests: 700 passing, 0 failing, 1 ignored (698 baseline → +2 IdentSlot enum tests). Clippy clean. Stage 7 plumbs schema queries through the worker thread (ListNamesFor) so stage 8's completion engine has data. |
||
|
|
9c4857eb50 |
ADR-0022 stage 5/8: hint panel ambient typing assistance
ParseError::Invalid gains an `expected: Vec<String>` field —
the human-rendered names of the patterns chumsky was looking
for at the failure point (`\`create\``, `identifier`, etc.).
Empty for custom errors, which have no expected-set framing.
Populated by a new `describe_expected()` helper in parser.rs
that humanise() also delegates to (eliminates duplication).
`input_render::ambient_hint(input) -> Option<String>` returns
the hint-panel content per ADR-0022 §6:
- empty input → None (caller falls back to panel.hint_empty);
- Valid → t!("hint.ambient_complete") ("submit with Enter");
- IncompleteAtEof → t!("hint.ambient_expected", expected = …)
listing the parser's expected next tokens, oxford-joined;
- DefiniteErrorAt → t!("hint.ambient_error_with_usage", …)
composing the parse-error message with the matching
parse.usage.* template if a known entry keyword was
consumed, else the bare message.
Catalog gains the three hint.ambient_* keys + validator
declarations.
ui::render_hint_panel resolution order:
1. explicit app.hint (modal contexts) wins;
2. simple-mode + non-empty input → ambient_hint;
3. fallback to panel.hint_empty.
Advanced mode (persistent + one-shot `:`) bypasses ambient
hinting per ADR-0022 §12.
Snapshot: highlighted_input_all_token_classes rebaselined
because the hint panel now displays an ambient hint instead
of the empty placeholder when input is non-empty.
Tests: 698 passing, 0 failing, 1 ignored (693 baseline →
+5 ambient_hint cases). Clippy clean.
Stage 6 introduces the IdentSlot taxonomy + parser audit so
identifier-typed slots can yield schema-aware completion
candidates in stage 8.
|
||
|
|
313d4f8346 |
ADR-0022 stage 4/8: render-time parse + error overlay
Add `classify_input(&str) -> InputState` that returns one of
{Empty, Valid, IncompleteAtEof, DefiniteErrorAt(byte)}.
The renderer uses this to overlay tok_error on the failing
token of mid-typed input that can never be valid.
ParseError::Invalid gains an `at_eof: bool` field populated
by `into_parse_error`:
- structural failures: at_eof = found.is_none()
(chumsky's own "ran out of input" discriminator);
- custom errors from try_map: at_eof = true,
conservatively.
The conservative custom-error classification is a deliberate
under-highlighting bias. It means three classes of error
currently DO NOT get a live red overlay (only on submit):
- "tables need at least one column" (correct: this is
genuinely an incomplete state — adding `with pk ...` fixes it);
- "unknown type 'varchar'" (sub-optimal: should overlay);
- "--force-conversion and --dont-convert are mutually
exclusive" (sub-optimal: should overlay).
The trade-off is documented inline on the at_eof field. A
future refinement could carry an explicit definite/incomplete
tag through Custom errors (would change RichReason::Custom's
payload from String to a typed value).
render_input_runs now applies the overlay on the failing
token's run before injecting the cursor. Tokens after the
error keep their lex-class colour — fixes one thing at a
time per ADR-0022 §4. Lex errors continue to render in
tok_error from stage 2.
Pattern-matches on ParseError::Invalid throughout the
codebase use `..` and are unaffected; only the two
constructions in parser.rs needed updating.
Tests: 693 passing, 0 failing, 1 ignored (683 baseline →
+10: 7 classify + overlay tests, +1 adapted full-command
test, +2 valid-vs-incomplete coverage). Clippy clean.
Stage 5 lights up the hint panel as the verbose-feedback
surface — needs the InputState classifier from this stage.
|
||
|
|
39da399add |
ADR-0022 stage 3/8: simple-mode echo lines highlighted
Lift `dsl::ECHO_PREFIX = "running: "` as a public const,
with a unit test asserting `t!("dsl.running", input = "")`
matches it. The catalog template is now contracted to equal
`format!("{ECHO_PREFIX}{input}")` — a translator changing
the prefix breaks the test.
Add `input_render::lex_to_runs(input, theme)` — a
cursor-less variant of `render_input_runs` for use cases
(echo lines, future hint panel) that need token-class
colouring without an inverted cursor.
ui::render_output_line: when the line is an Echo submitted
in Simple mode, peel the prefix and re-tokenise the rest
through lex_to_runs, rendering each token at its class
colour. Advanced-mode echoes and any echo whose body
unexpectedly lacks the prefix fall through to the plain
rendering.
Tests: 683 passing, 0 failing, 1 ignored (682 baseline →
+1 echo_prefix_matches_catalog_template). Clippy clean
(uses let-chain to keep the if condition flat).
Stage 4 adds render-time parse + error overlay so the
failing token in mid-typed input lights up in the error
colour.
|
||
|
|
cafc455c8a |
ADR-0022 stage 2/8: input panel — token-class highlighting
New `input_render` module with `render_input_runs(input, cursor_byte, theme) -> Vec<StyledRun>`. Lexes the input, assigns each token its `theme.token_color`, preserves whitespace gaps as `theme.fg` runs, and injects the cursor by splitting the run that contains it into before/under/after sub-spans (under marked Modifier::REVERSED). End-of-input cursor is an empty-range sentinel rendered as an inverted space. ui::render_input_panel switches over EffectiveMode: simple mode goes through render_input_runs + a small runs_to_spans helper that borrows from the input string; advanced modes (persistent + one-shot `:`) keep the previous plain before/under/after rendering since the DSL lexer doesn't speak SQL (ADR-0022 §12). Multi-byte UTF-8 in string literals is handled by walking to the next char boundary when splitting the cursor run, mirroring the previous renderer. Tests: 682 passing, 0 failing, 1 ignored (672 baseline → +10: 9 input_render unit tests covering each token class, cursor placements, multi-byte, full-command shape; +1 new "all token classes" UI snapshot). Clippy clean. Caveat (noted inline in the new snapshot test): the TestBackend/render_to_string path records text symbols only, not ratatui style. The new snapshot is therefore a text-layout regression net; the unit tests in input_render::tests are the authoritative regression net for colour mappings. Stage 3 wires the same colouring into simple-mode echo lines in the output panel. |
||
|
|
00c9deaf6f |
ADR-0022 stage 1/8: theme token-class colour fields
Add seven `tok_*` Color fields to Theme — keyword, identifier, number, string, punct, flag, error — populated in both dark and light themes. WCAG-AA contrast against each theme's bg. Identifier and punct sit close to fg/muted so dominant content reads quietly; literals + flags get warm accent tones; keyword takes a cool accent (purple) distinct from the mode-banner blue. tok_error reuses the existing error palette so lex-error tokens read consistently with [error] lines elsewhere. New helper Theme::token_color(&TokenKind) -> Color maps each token kind to its display colour. Tests: 672 passing, 0 failing, 1 ignored (668 baseline → +4 theme tests). Clippy clean. Pure addition; no existing render path uses these yet. Stage 2 wires them into the input panel. |
||
|
|
f0632af8af |
ADR-0022: ambient typing assistance (unifies I3 + I4)
Replaces the originally-planned separate ADRs for syntax highlighting (I4) and tab completion (I3) with a single unified design. The framing: colour, hint panel, and Tab are three answers to the same question — what does the user need to know mid-typing? — and planning them separately produces three loose pieces that drift apart. Three mid-typing states (valid-so-far / definite-error / incomplete-but-plausible) drive four layered channels: token-class colour and parse-error overlay (silent, always on), hint panel ambient and Tab-triggered completion mode (verbose, in the existing hint panel — no floating popups). Schema-aware from day one via an IdentSlot taxonomy in the parser (NewName / TableName / ColumnIn(TableRef::Earlier(N)) / RelationshipName); every existing ident() call gets audited and tagged. Completion candidates come from chumsky's expected-token-set for keyword slots and from a new worker request (ListNamesFor) for identifier slots. Implementation lands in 8 green-after-each commits: theme colours; input panel highlighting; echo line highlighting; render-time parse + error overlay; hint panel ambient; identifier-slot taxonomy + parser audit; schema query plumbing; completion mode + key bindings. Estimated 1500-2500 lines across the eight stages. Out of scope (deliberately): inline ghost text (could return as a "most-likely" affordance later — fish-shell style), fuzzy matching, punctuation completion, user-customisable keybindings, SQL highlighting in advanced mode (waits on Q4). |
||
|
|
11071ae164 |
ADR-0021 implementation: per-command usage templates in parse errors
New `dsl::usage` module: registry pairing each command's
entry-keyword with a `parse.usage.*` catalog key.
`matched_entry()` resolves the entry keyword from the
consumed token prefix; multi-entry families (add, drop,
show) return all matching keys.
Catalog: new `parse.usage.<command>` keys (one per command),
`parse.token.{keyword,punct,...}` vocabulary (one per
Keyword/Punct variant + token-class labels + LexError
kinds), and `parse.available_commands` for the no-prefix
fallback. Catalog grows ~60 entries.
Validator: extended KEYS_AND_PLACEHOLDERS; new completeness
test asserts every Keyword and Punct variant has its
`parse.token.*` entry.
`app::dispatch_dsl` rewritten to compose three blocks per
ADR-0021 §2: caret + structural/custom error + usage block
(or available-commands fallback per §5). Caret math fixed
to use original-input byte position rather than
trimmed-input position (the lexer no longer trims before
lexing). Three pre-existing app tests adjusted to look
across all error lines instead of `output.back()` (the
usage block is now the last line).
`dsl::usage::matched_entry` uses `<=` rather than `<` for
position comparison so custom errors raised by `try_map`
(whose span starts at the first consumed token) still
resolve to the entry keyword.
Tests: 668 passing, 0 failing, 1 ignored (650 baseline →
+18: 8 usage + 1 token-vocab completeness + 9 new
integration tests in tests/parse_error_pedagogy.rs
covering create/add/drop/show/frobulate/update/insert
cases). Clippy clean.
|
||
|
|
fdaf7e3e0e |
ADR-0020 implementation: lexer + parser refactor over &[Token]
New `dsl::keyword` module: macro-driven Keyword and Punct enums (single source of truth — enum, lex-side mapping, catalog-key derivation generated from one declaration). New `dsl::lexer` module: tokenizer producing a span-tagged Vec<Token>. Always succeeds; lex-shape errors (unterminated string, unrecognised character, malformed flag) embed as TokenKind::Error tokens so I4 can highlight invalid input uniformly. Parser refactored from `Parser<'a, &'a str, ...>` to `Parser<'a, &'a [Token], ...>`. All 50+ existing parser unit tests ported and passing; aggregation across `choice` now works as designed (e.g. `add` → "expected `1` or `column`", `drop` → "expected `column`, `relationship`, or `table`", `frobulate Customers` lists all ten command-entry keywords). Custom `try_map` content errors (unknown type, mutually-exclusive flags, "with pk needs at least one column", "specified twice") preserved. `replay` bare-path UX kept via the source-slice special case from ADR-0020 §6 (~10 lines, documented inline). Tests: 650 passing, 0 failing, 1 ignored (610 baseline + 40 new lexer/keyword tests). Clippy clean. |
||
|
|
857ee753f2 |
ADR-0020 + ADR-0021: tokenization layer and parse-error pedagogy (H1a)
ADR-0020 amends ADR-0001 with a two-phase parse: a lexer
producing a span-tagged token stream, then chumsky over
&[Token]. Single source of truth for keywords and punct via
a define_keywords!/define_punct! macro pattern. Parser
contract committed for I3 (queryable expected-token-set)
and I4 (lexer always succeeds, Error tokens for invalid
input). Includes an honest history note: the no-lexer shape
in dsl/parser.rs arose incrementally without ADR-level
deliberation against the known H1a/I3/I4 requirements; this
ADR corrects that.
ADR-0021 builds on ADR-0020 to close the H1a gap: a
per-command UsageEntry registry keyed off entry-keyword,
with parse errors rendered as caret + structural error +
matching usage template(s). Multi-entry families (add,
drop, show) render together. New catalog sections under
parse.usage.* (per-command grammar) and parse.token.*
(single-token vocabulary). Zero-prefix case ("frobulate
Customers") falls back to an "available commands:" framing.
Anchor-phrase compliance preserved.
|
||
|
|
47601f7c85 |
Handoff doc for end of 2026-05-09 (#6)
Documents this session's work and the recommended next move:
## Session totals
- 11 commits since handoff-5
- 534 → 610 tests passing (+76)
- Release binary 7.2 → 7.8 MB
## What landed
- All four non-CI items from handoff-5's Independent Work
list: B2 (int→bool tests), B1 (help update), A2 (engine-
vocabulary audit), A3 (replay command)
- ADR-0019 fully implemented end-to-end:
- Friendly-error layer + i18n catalog (~170 entries
across 16 categories)
- §6 runtime row-pinpoint enrichment with
schema-resolved facts
- §9 migration sweep — every user-visible literal in
src/ now flows through the catalog (caught a ui.rs
gap during the post-sweep manual sanity check, folded
it in as sweep 3/3)
## Recommended next move
Parser-as-source-of-truth ADR + H1a implementation. The
friendly-error layer made engine errors much better;
parse-error wording is now the visibly-weakest user
surface. User explicitly surfaced the gap during manual
testing this session ("typing `create` should illustrate
the expectation"). Bounded scope, high pedagogical value,
unblocks I3/I4 in passing.
A1 (CI workflow) noted as the easy alternative for a
quick win first.
## Sharp edges captured
- New i18n workflow: catalog + keys.rs + t!() at every
use site, validator catches drift
- TranslateContext is owned (no lifetime); App combines
runtime FailureContext with verbosity
- Anchor phrases load-bearing per ADR-0019 §10
- `running: ` prefix coupled to caret-padding math
- main.rs initialises catalog before args parsing
- Several alignment-coupled strings deliberately left out
of the catalog (echo prefix tags, mode labels)
|
||
|
|
a6fd26d15a |
ADR-0019 §9 sweep (3/3): ui.rs prose strings (caught in manual sanity)
Surprise gap from the post-sweep sanity check — `ui.rs` had a substantial set of TUI-rendered strings that the previous two sweep passes didn't cover. Caught by grepping for capitalised literals in `ui.rs` after running the binary smoke check. ## Migrated - **modal.*** — load picker title / empty state / path prompt; rebuild confirm title / "Continue?" prompt. (modal.path_entry's title comes from `save.*` since it's the save / save-as dialog.) - **save.*** — `save` no-op hint, modal titles for Save / Save as, modal prompt body. - **status.*** — status bar `Project:` label and the `(no project)` placeholder. - **panel.*** — `Tables` panel title, `(none yet)` placeholder for empty tables, `(no active hint)` placeholder for the hint panel. - **shortcut.*** — the bottom-bar keyboard hint labels (submit, confirm, cancel, yes, no, load, select, browse_path, back_to_list, switch, advanced_once, cancel_one_shot, quit). Each is a translatable label paired with a key name (Enter / Esc / Ctrl-C / etc.) at the call site. Keystroke names are deliberately left as literals — translating them would mean retraining users away from what their keyboard says. The `push_shortcut` closure's parameter type changed from `&'static str` to `&str` so it accepts the catalog-returned String. ## Deliberately left - **Echo prefix tags**: `[simple] `, `[advanced] `, `[system] `, `[error] `. Their column widths are hardcoded into the wrap-width calculation in `render_output_panel`; translating them would silently break alignment. Worth a follow-up pass if a future locale needs different prefixes (would need `mode.label()` and the echo-tag widths to live behind a single locale-aware function). - **Mode labels**: `SIMPLE` / `ADVANCED` / `Advanced:` rendered in the input panel border. Same alignment reasoning as the echo tags — also they're keywords (the user types `mode simple` to switch), so translating the display label without translating the command word would be confusing. Left as is. - **Visual decoration**: `[Y]`, `[N]`, `[TEMP] `, `>` cursor markers, `█` cursor block, `↑↓` arrow glyph, `›` selection marker. Universal symbols / labels rather than translatable prose. ## Catalog totals The catalog now has ~170 entries across 16 categories. `tests/engine_vocabulary_audit` passes — no engine vocabulary leaks anywhere user-reachable. ## Tally 610 tests passing (no change — pure refactor with identical-output catalog substitutions). Clippy clean with nursery lints. Release builds at 7.8 MB. ADR-0019 §9 is now genuinely complete. |
||
|
|
720511ef29 |
ADR-0019 §9 sweep (2/2): help blocks + modals + system notes
Final pass of the i18n migration sweep. Every user-visible
string in `src/` now flows through the catalog via `t!()`.
## Categories migrated in this commit
- **help.cli_banner** — the entire `cli::HELP_TEXT` const,
formerly a 40-line `&'static str`, is now a YAML block in
the catalog. The const is replaced by a thin
`cli::help_text() -> String` wrapper that performs the
catalog lookup. `main.rs` calls `help_text()` for both
`--help` output and the args-parse error path. The two
integration tests that referenced `HELP_TEXT` directly are
updated.
- **help.in_app_body** — the in-app `help` command's body is
one YAML block; `note_help` becomes 5 lines that iterate
the lines and emit each as its own output row (preserving
the renderer's "one logical line = one display row"
invariant for accurate scroll math).
- **modal.*** — load picker, rebuild confirm, and save-as
path-entry strings: rebuild_cancelled, load_cancelled,
generic_cancelled, load_picker_nothing,
path_entry_empty_name, path_entry_empty_path.
- **dsl.failed** — the `"<verb> <subject>" failed: <rendered>`
wrapper around the friendly-error layer's translated
message.
- **dsl.running** — the `running: <input>` echo line shown
above each command's response. (Note: the en-US prefix
"running: " is hardcoded in the parse-error caret-padding
calculation. Translators changing the prefix must keep the
width consistent — documented inline.)
- **advanced_mode.not_implemented** — the placeholder echo
shown when SQL hits the unimplemented advanced-mode path
(Q1 territory).
- **fatal.persistence** — the FATAL banner for
PersistenceFatal events (ADR-0015 §8).
- **project.{load_path_missing,saveas_target_exists,**
**import_zip_missing}** — runtime-side project-switch
validation errors that surface via ProjectSwitchFailed.
## Catalog start-up ordering
`main.rs` now calls `friendly::catalog()` at the very top
(before args parsing) so `help_text()` works in both the
success path and the args-error path. A corrupted build
artefact still fails loudly with a useful panic; the
practical risk is essentially zero since the catalog is
`include_str!`'d at compile time and validated by the unit
test before shipping.
## Remaining literals
The only `note_*` calls in `src/` that still pass plain
strings are inside `#[cfg(test)]` modules — synthetic test
fixtures, not user-visible. The codebase passes the "every
user-visible string flows through the catalog" bar.
## Tally
610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints.
## What this closes
ADR-0019 §9 (migration sweep) — done.
ADR-0019 itself is now fully implemented:
- §1-§5: catalog + translator + voice + verbosity ✓ (`eac7e5b`)
- §6: row pinpointing + schema enrichment ✓ (`431645a`)
- §9: migration sweep ✓ (this + `aff528a`)
- §10: anchor phrases preserved throughout ✓
- The five "Out of scope" items remain explicitly bounded
to future ADRs (advanced-mode SQL, settings persistence,
pluralisation, runtime locale, value formatting,
constraint management).
|
||
|
|
aff528aa3f |
ADR-0019 §9 sweep (1/2): replay/client_side/ok/mode/messages/project/parse
First half of the catalog migration sweep. Six categories of
user-visible literals moved from inline `format!` calls to the
i18n catalog via `t!()`:
- **replay.*** — `[ok] replay … N command(s) run`,
`replay … failed at line N: …`, the `> command` echo, and
the inner `could not open` / `parse error` / `nested replay`
wordings the runtime constructs inside `ReplayFailed.error`.
- **client_side.*** — the four [client-side] pedagogical notes
from ADR-0017 §6 / ADR-0018 §9 (transformed,
transformed_lossy, auto_fill_transition,
auto_fill_add_serial, auto_fill_add_shortid). The
`format_auto_fill_add_note` helper in db.rs now routes via
the catalog too.
- **ok.*** — the `[ok] {verb} {subject}` summary header
(consolidated through a new `App::note_ok_summary` helper)
plus the per-operation row-count footers
(`{count} row(s) inserted/updated/deleted`).
- **mode.*** — `mode: simple/advanced` set/show banners +
`usage: mode …` + `unknown mode '{value}' …` errors.
- **messages.*** — `messages: short/verbose` set/show + the
`unknown messages mode` error.
- **project.*** — `[ok] rebuild — {summary}`, `[ok] now
editing: {display_name}`, `[ok] export — wrote {path}`, plus
matching failure variants and the `usage: export/import`
+ `import: empty target after as` argument-parsing errors.
- **parse.*** — the `parse error: {detail}` wrapper around
chumsky's structural output, the `{padding}^` caret pointer,
and the `empty input` fallback for `ParseError::Empty`.
Catalog total: 99 lines of YAML across the new categories,
44 new entries declared in `keys.rs::KEYS_AND_PLACEHOLDERS`.
The validator (`keys_validate_against_catalog`) walks the
expanded list and confirms placeholder coverage / no format
specifiers / no engine vocabulary across every entry.
Anchor phrases (ADR-0019 §10) preserved verbatim; existing
substring assertions in the test suite hold.
## Tally
610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints. Release builds.
## Still ahead in the sweep
- Sweep 7: HELP_TEXT (CLI banner) + in-app `note_help` —
large multi-line blocks.
- Sweep 8: modal labels (load picker, rebuild confirm,
save-as path entry) + any remaining strays. Final pass.
Both shipping in a follow-up commit so this checkpoint
stays reviewable.
|
||
|
|
431645ae60 |
ADR-0019 §6: runtime enrichment + row pinpointing
Closes the placeholder-substitution gap reported during manual
testing: FK violations were rendering `<value>` and `<column>`
literally because the App had no schema awareness. With this
change the runtime resolves the schema-dependent facts before
the App ever sees the failure.
## Architecture
- **Database** gains two public methods backed by new worker
Request variants:
- `read_relationships(table)` → (outbound, inbound) FK list
(lifts the previously-private `read_relationships_*` pair
into the public surface, behind a `RelationshipsReply`
type alias).
- `find_rows_matching(table, column, value, limit)` →
`DataResult` for row pinpoint queries.
- **friendly module** gets:
- New `FailureContext` struct: schema-resolved facts the
runtime builds (table, column, value, parent_table,
parent_column, child_table, optional diagnostic_table).
- `TranslateContext` loses its lifetime parameter and gains
`parent_table` / `parent_column` fields. All string fields
are now `Option<String>` for ownership simplicity.
- `TranslateContext::from_facts(operation, verbosity, facts)`
helper.
- Translator's FK paths now use `ctx.parent_table` /
`ctx.parent_column` for child-side wording; FK Update gets
a dedicated `fk_child_side_update` arm.
- FK dispatch is enrichment-driven first
(`parent_table` set → child-side; `child_table` set →
parent-side), with operation as the tiebreaker.
- The translator forwards `ctx.diagnostic_table` onto the
`FriendlyError` so pinpointed rows render through the
existing ADR-0017 §7 bordered renderer.
- **Event** `DslFailed` carries `(command, error, facts)`.
The runtime populates `facts` via `enrich_dsl_failure`
before posting the event.
- **Runtime** `enrich_dsl_failure(database, command, error)`
classifies and resolves:
- UNIQUE INSERT/UPDATE: parses `T.col` from engine message,
finds the user's attempted value (with schema fallback
for natural-order multi-value INSERT — including the
serial/shortid auto-skip rule from `do_insert`), pinpoints
the existing conflicting row(s) via `find_rows_matching`
and renders as a `DiagnosticTable`.
- NOT NULL INSERT/UPDATE: parses `T.col`; no value
(definitionally null) and no pinpoint (engine doesn't
identify the row).
- FK INSERT/UPDATE: outbound relationship lookup picks the
FK column the user is touching; resolves
`parent_table`/`parent_column`/`value`. UPDATE falls back
to inbound (parent-side) when no outbound match.
- FK DELETE: inbound relationship lookup picks a child_table
that references this row.
- **App** drops its old `attempted_value_for` /
`column_from_qualified_target` helpers (their work moved to
runtime where the Database is in scope).
`build_translate_context` combines the runtime-supplied
facts with the operation derived from the Command and the
App's verbosity.
## Manual-test fixes folded in
Two issues surfaced during manual testing of the initial
implementation, both fixed:
1. Natural-order multi-value INSERT
(`insert into Orders values (4, 11.99)`) skipped FK
enrichment because `user_value_for_column` only knew the
single-value short form. The schema-aware lookup
(`user_value_for_column_with_schema`) now mirrors
`do_insert`'s position-mapping rule (auto-generated
columns skipped), so positional INSERTs onto tables with
serial/shortid PKs resolve correctly. Regression test:
`enrich_fk_insert_natural_order_multi_value_resolves_via_schema`.
2. The arity error on INSERT now lists the columns it
expected — `expected 3 value(s) for (id, Name, Email), got 2`
instead of the bare count. Surfaces what the user needs
to fix without making them go check the schema.
## Tests
`tests/friendly_enrichment.rs` (+8 integration tests):
- UNIQUE INSERT with explicit columns: facts.{table, column,
value, diagnostic_table} all resolved; pinpoint shows
conflicting row.
- UNIQUE INSERT natural-order short form: schema fallback
resolves the value.
- UNIQUE UPDATE: value pulled from assignments.
- NOT NULL INSERT: table+column resolved, value None
(correct), no pinpoint.
- FK INSERT: parent_table, parent_column, value all resolved
via outbound relationship lookup.
- FK INSERT natural-order multi-value: schema-aware lookup
with auto-skip resolves correctly (regression for the
manual-test bug).
- FK DELETE: child_table resolved via inbound relationship
lookup.
- DbError::Unsupported: enrichment returns default
FailureContext (no false positives).
App-level tests updated to populate `FailureContext` directly
(simulating runtime enrichment) for the verbosity / threading
checks.
## Tally
610 tests passing (was 603: +8 enrichment integration tests
minus 1 obsolete App-side helper test that the runtime
absorbed). Clippy clean with nursery lints. Release builds.
|
||
|
|
eac7e5b81d |
ADR-0019 implementation: friendly error layer + i18n catalog
All eight implementation steps from ADR-0019's §"Order of
operations":
Step 1 — `src/friendly/` module skeleton; `t!()` macro; YAML
catalog loader (`include_str!` + `serde_yml`); `{name}`
substitution helper that rejects format specifiers per §8.4.
Step 2 — `error.*` catalog populated for UNIQUE / FK /
NOT NULL / CHECK / type-mismatch / not_found / already_exists /
generic / invalid_value, with verbose hints per
pedagogical-voice rule (§5). Anchor phrases (§10) preserved
verbatim.
Step 3 — `FriendlyError { headline, hint, diagnostic_table }`
+ renderer composing the three blocks per §7.
Step 4 — `translate(&DbError, &TranslateContext) → FriendlyError`.
Classifies by `SqliteErrorKind` first, then by message text
for the constraint family. `change column` failures route to
the type-mismatch headline, subsuming the previous
`friendly_change_column_engine_error` helper.
Step 5 — `DbError::friendly_message()` delegates to the
translator with default context. Removed
`friendly_change_column_engine_error` (absorbed) and
`enrich_fk_message` (FK list moves to the deferred re-query
step). One test rewritten to assert on the engine-classified
payload rather than the removed enrichment text.
Step 6 — `messages (short|verbose)` app-level command parallel
to `mode`. `App::messages_verbosity` (default verbose)
threaded into `TranslateContext` via
`App::build_translate_context`. `AppEvent::DslFailed` now
carries the structured `DbError`, plus the App extracts the
user's attempted value from `Command::Insert` / `Update`
to fill the `{value}` placeholder for UNIQUE / NOT NULL.
Step 7 — Catalog validator (§8.6) checks for missing keys,
unused/undeclared placeholders, format specifiers, and
forbidden engine vocabulary. `main.rs` parses the embedded
catalog at startup so a corrupted build artefact fails
loudly there rather than at the first `t!()` call.
Step 8 — Anchor phrases (§10) held: existing tests asserting
on "no such table", "already exists", "cannot be converted",
etc. all pass without rewording.
## Tally
603 tests passing (was 561: +42 net). Clippy clean with
nursery lints. Release binary 7.7 MB.
## Deliberately deferred
- Schema-aware enrichment for FK violations (parent_table /
parent_column / child_table) and the multi-value
natural-order INSERT case for UNIQUE. Both need the
Database handle in scope at translation time, so they
bundle naturally with the row-pinpoint re-query work
(ADR-0019 §6) — that follow-on adds runtime-side
enrichment via a `Database` lookup and a structured
failure-context carried on `DslFailed`. Until then,
unfilled placeholders render as their `{name}` form for
visual consistency with the catalog.
- Migration sweep (§9). Only `error.*` is catalog-driven so
far; `help.*`, `ok.*`, `client_side.*`, `replay.*`,
`parse.*`, modal labels, etc. migrate per-PR.
- Settings persistence for `messages`. In-session state for
now; waits on the future settings ADR.
|
||
|
|
d4801ea52f |
ADR-0019: pluralisation is a translator concern, not deferred work
Per follow-up review: §8.5's framing read as "we'll do this properly later". Reword to make it explicit that real plural-form rules per locale (Fluent / ICU) are NOT a goal of this project. Translators handle pluralisation in their wording (`(s)` shorthand or rephrased templates) — sufficient for a teaching tool's output surface, and we're not planning to revisit it. Matching Out-of-Scope entry tightened the same way. |
||
|
|
2a8618c783 |
ADR-0019: friendly error layer (H1) and i18n message catalog
Settles the design we discussed across this session's
follow-up to the engine-vocabulary audit:
- A central `friendly` module owns translation; the existing
ad-hoc helpers (`friendly_change_column_engine_error`,
`enrich_fk_message`) absorb into it.
- Initial catalog covers UNIQUE / FK / NOT NULL / CHECK /
type-mismatch errors with operation-tailored,
pedagogically-voiced wording in verbose and short variants.
- New `messages (short|verbose)` app-level command lets
advanced learners shrink the output. In-session state for
now; persisted later when settings persistence lands.
- Row pinpointing via post-failure re-query, rendered through
ADR-0017 §7's bordered diagnostic-table renderer.
`FriendlyError` is a structured payload (headline + hint +
optional table); `output_render` composes it.
- i18n foundation: hierarchical YAML catalog, embedded via
`include_str!`, fixed locale (en-US) for now, no external
files. `{name}` plain substitution; format specifiers
explicitly rejected so a translator cannot reformat values.
Value formats stay invariant across all locales (ISO 8601
dates, `.` decimals, `true`/`false`, `NULL`) — explicitly
not a translatable concern.
- Migration sweep is required follow-on but separable: a
`t!()` macro marks call sites and lets per-category PRs
land incrementally. Anchor-phrase list (§10) limits test
churn for the most common substring assertions.
Out of scope and explicitly deferred: advanced-mode SQL
error sanitisation (waits on Q1), settings persistence for
the messages command, plural-form rules per locale, runtime
locale selection, locale-aware value formatting (rejected,
not deferred), constraint-management surface (C3 territory).
README index updated.
|
||
|
|
c4ee264636 |
replay: new replay <path> command (A3, U4)
Implements the U4 replay command per handoff §A3:
replay <path>
Reads <path> and dispatches each non-blank, non-`#`-comment
line through the same DSL pipeline as interactive input.
Aborts at the first per-line failure (parse or runtime),
reporting the line number; previously dispatched commands
stay applied (no rollback) — matches the "I'm replaying my
history" mental model where partial replay is a recoverable
state.
Architecture choices and why:
- **Parsed by the DSL parser** (Command::Replay), not as an
app-level command alongside `import` / `export`. The
handoff's implementation sketch was explicit and the
parsed-AST shape gives us a clean test surface for the
path-lexing rules. A new `path_literal` parser terminal
accepts either a single-quoted string (escape rules
mirror `string_literal` — `''` for a literal quote) or a
bare run of non-whitespace, with explicit refusal of `'`,
`(`, `)`, `;` in bare form. Empty paths fail at parse
time so file-system-layer errors aren't shadowed by
silly inputs.
- **Routed away from the worker thread.** Command::Replay
is intercepted in `App::dispatch_dsl` and emitted as
`Action::Replay` rather than `Action::ExecuteDsl`. Two
reasons: (1) the worker has no filesystem context, and
(2) the replay invocation must NOT land in
`history.log` — otherwise `replay history.log` would
re-trigger itself recursively. Only the individual
sub-commands write to history.log via the normal
per-command persistence path.
- **Inner loop separated from spawn.** `runtime::spawn_replay`
is a thin tokio::spawn wrapper around `runtime::run_replay`,
which is `pub` and returns a Vec<AppEvent>. The inner
function is what tests exercise, sidestepping mpsc plumbing.
- **Relative paths resolve under the project root** so
`replay history.log` works without ceremony from inside
any project. Absolute paths pass through unchanged.
- **Nested `replay` is refused.** Allowing `replay foo` from
inside a replay file invites infinite-loop footguns and
opens design questions (transitive composition, ordering)
we'd rather not answer right now. Refusal is explicit.
New plumbing:
- `Command::Replay { path }` AST variant + verb/target_table.
- `Action::Replay { path }` runtime action.
- `AppEvent::ReplayCompleted { path, count }` and
`AppEvent::ReplayFailed { path, line_number, command, error }`.
- `runtime::run_replay` (public) and `runtime::spawn_replay`.
- App handlers render success as
`[ok] replay <path> — N command(s) run` and failures as
`replay <path> failed at line N: <error>` with a
` > <command>` echo line for line context. Line 0 is the
"file open failed" signal — header reads
`replay <path> failed: <error>` and the echo line is
suppressed.
- In-app `help` lists the new command with a continuation
describing comment/blank handling and the relative-path
rule.
Tests (+20):
- 7 parser tests covering bare/quoted/escaped paths,
case-insensitive keyword, and refusal cases (no path,
empty quoted path).
- 9 integration tests in `tests/replay_command.rs`:
- happy 3-line replay → 3 commands run, state mutated;
- blank lines + `#` comments skipped;
- empty file + only-comments file → count 0;
- missing file → ReplayFailed line_number 0;
- parse failure mid-replay → reports correct line +
leaves earlier commands applied + does NOT run later
lines;
- runtime failure mid-replay (refers to nonexistent
table) → reports correct line;
- nested replay refused;
- history.log contains per-command entries but NOT the
`replay …` invocation itself.
- 4 App-level tests: Action::Replay dispatch (not
ExecuteDsl); ReplayCompleted rendering; ReplayFailed
rendering with and without line-number context.
541 -> 561 passing, clippy clean with nursery lints,
release build successful.
A future ADR on the parser-as-source-of-truth direction
(handoff §"Pending §3") would bring richer error reporting
for replay parse failures (currently uses the same
single-line wording as interactive parse failures, which is
adequate but not great when a script has many lines around
the failing one).
|
||
|
|
b8102dc063 |
tests: ADR-0002 engine-vocabulary audit (A2)
Verifies the user-facing posture in ADR-0002 §"User-facing posture": no occurrence of SQLite, sqlite, rusqlite, STRICT, or PRAGMA may appear in any user-reachable string. The audit's mechanical sweep of `src/` confirmed the codebase already conforms — every appearance of those tokens is in either: - code comments / module-level docstrings (allowed by ADR-0002 explicitly), - DDL strings sent to the engine (not displayed to the user), - internal field/function names like `sqlite_type` / `sqlite_strict_type` (code identifiers, not user-visible). The previous session removed the last known leak in `do_add_column`. To stop a future change from quietly re-introducing one, this commit adds a regression test file covering a representative set of user surfaces: - `cli::HELP_TEXT` (`--help` banner). - The in-app `help` command output. - DSL parse errors for a battery of failing inputs (column-name-first typo, unknown type token, mutually exclusive flags, missing clause, garbage). - `DbError::friendly_message()` for realistic Sqlite, Unsupported, and InvalidValue payloads — the surface the runtime forwards via `AppEvent::DslFailed`. The forbidden-token list lives in one place (`engine_vocabulary_audit.rs::FORBIDDEN`) so future audits can extend it. Failure messages name the leaking token and its byte offset so a regressing edit pinpoints itself. Out of scope (and called out in the handoff for separate work): the H1 friendly-error layer that translates the remaining engine error wording into pedagogical English. That needs its own ADR. The local `friendly_change_column_engine_error` wrapper (db.rs §2354) is the prototype. 537 -> 541 passing (4 new), clippy clean. |
||
|
|
3dbaedc1da |
help: surface ADR-0017/0018 auto-fill semantics (B1)
ADR-0017 added --force-conversion / --dont-convert as opt-in flags on `change column`; the help text already mentioned the flags but didn't explain when they apply. ADR-0018 generalised serial beyond PK and added auto-fill on `add column ... (serial|shortid)` for non-empty tables; none of that was reflected in user-visible help. This commit: - Annotates the `add column` line with a continuation note that adding serial/shortid to a non-empty table auto-fills existing rows. - Annotates the `change column` line with a continuation note that converting to serial/shortid auto-fills null cells. - Appends an "Auto-generated types" section explaining serial and shortid: how they auto-fill, that they imply UNIQUE outside a PK (serial) or always (shortid), and that adding/converting-to either type on a non-empty table auto-fills existing/null cells. The new test `help_describes_auto_generated_type_behaviour` pins these phrases so a future help-text edit can't silently drop the pedagogical lines. The existing `help_command_lists_supported_commands` and `help_lists_export_and_import` tests still pass — they only assert substring presence. No engine vocabulary leaks (ADR-0002 posture preserved). 536 -> 537 passing, clippy clean. |
||
|
|
0d7a7bcd49 |
db: end-to-end tests for change_column int -> bool (B2)
The (Int, Bool) entry of the ADR-0017 §3 matrix was already covered at the per-cell unit-test level in `type_change.rs`, but the end-to-end change_column path through `db.rs` had no test exercising it. This closes that gap with the two cases called out in the handoff: - `change_column_type_int_to_bool_with_zero_one_succeeds`: Rows 0/1/0 succeed, no [client-side] note. The matrix returns the same Value::Integer for 0 and 1, so is_non_identity reports false for every cell and ClientSideNote.transformed stays at 0 — the `transformed > 0 || auto_filled > 0` filter therefore drops the note. - `change_column_type_int_to_bool_refuses_other_values`: Row with 2 → Incompatible. Verified under both Default and ForceConversion modes (per ADR-0017 §5: incompatible is not lossy, --force-conversion must not advertise). No production code change; tests only. 534 -> 536 passing, clippy clean with nursery lints enabled. |
||
|
|
dcfeef5d3c | Handoff doc for end of 2026-05-08 (#5) | ||
|
|
5bb0a147f0 |
ADR-0018 implementation: auto-fill contracts for serial and shortid
Generalises serial and shortid beyond their previous restricted forms: - `serial` is no longer restricted to single-column PK. Non-PK serial columns get an emitted UNIQUE constraint and use application-side MAX(col)+1 at INSERT time (rowid alias still drives the PK case for free; per ADR-0010 worker-thread serialisation, the read-then-insert sequence is safe). - `shortid` columns auto-fill existing null cells when the column is materialised — `add column T: x (shortid)` on a non-empty table no longer leaves rows in a not-really-valid NULL state. - `int -> serial` joins the type-change matrix as always-clean identity (closes the asymmetry vs `text -> shortid`); other sources are refused with a route-via-int hint. - `change column T: x (serial|shortid)` fills null source cells with sequence / generated values in the same rebuild transaction. Internal infrastructure: - ReadColumn gains `unique: bool`; read_schema detects single- column UNIQUE indexes via pragma_index_list / pragma_index_info; schema_to_ddl emits inline UNIQUE for non-PK columns. - ColumnSchema (persistence) gains `unique: bool` so the flag survives YAML round-trip and rebuild-from-text reconstructs it faithfully — preserves the "serial -> int leaves UNIQUE in place" promise across save/load cycles. - ChangeColumnTypeResult.client_side now carries `auto_filled` + `auto_fill_kind` alongside `transformed` + `lossy`; the app handler renders separate note lines when both apply. - AddColumnResult is a new return type carrying pre-rendered [client-side] note lines for the auto-fill paths. Tests: 519 -> 534 (+15). Clippy clean. |
||
|
|
7dfa718c6e |
parser: structural error rendering, source echo, and caret pointer
The old humanise() fell back to chumsky's terse Display for non-
custom errors and appended "(near `X`)", which on top of an
already-cryptic "found 'i' expected ':'" turned the message into
a puzzle. Now humanise() reads the structured RichReason, lists
expected RichPatterns in plain prose, and prefixes the message
with the consumed context.
Before: parse error: found 'i' expected ':' (near `i`)
After: parse error: after `change column Rich`, expected `:`,
found `i`
dispatch_dsl additionally echoes the source line on parse failure
(matching the success path's "running: ...") and prints a `^` caret
under the failure position, so the user can see what got submitted
and where the parser broke without re-reading from scratch.
Known limit: keyword_ci's custom-error mismatches don't aggregate
across choice alternatives, so messages like "expected DATA or
TABLE" (bison-equivalent) aren't yet possible. That's a structural
fix to the keyword matcher, deferred to a future parser-affordances
ADR.
Tests: +2 structural-error regression tests.
|
||
|
|
00947b928c |
ADR-0017 implementation: per-cell type-change with override flags
Replaces the placeholder "trust STRICT" body of do_change_column_type
with the per-cell transformer matrix from ADR-0017. Adds:
- src/type_change.rs: CellOutcome { Clean / Lossy / Incompatible }
+ transform_cell + static_refusal covering every matrix pair
from §3 (54 unit tests).
- --force-conversion and --dont-convert flags on `change column`
(mutually exclusive at parse time per §5).
- Refined PK rule (§4.1): refused only when the column has an
inbound FK and fk_target_type would change. Outbound-FK columns
still refused outright (§4.2). PK / shortid uniqueness checked
post-transformation (§4.3).
- Bordered diagnostic tables (lossy / incompatible / collision)
via the pretty-table renderer (§7) — uses ADR-0016's primitives.
- [client-side] success note (§6) when any cell was rewritten.
- Friendly wrapper for engine-level errors under --dont-convert
so no engine vocabulary leaks (ADR-0002 user-facing posture).
ADR-0017 §3 + §7 amended in place (with user sign-off): serial->int
added explicitly to the always-clean matrix, and diagnostic rows
identify themselves by PK value(s) rather than positional indices
(SQLite returns rows unordered without ORDER BY, so positional
"row 5" is unaddressable).
Tests: 449 -> 517 (+68). Clippy clean with nursery lints.
|
||
|
|
545cbf5c0e |
Handoff doc for end of 2026-05-08 (#4)
Records the session's commits (Iter 5/6 follow-ups, pretty rendering, B2/C2 column ops, ADR-0016, ADR-0017, ADR-0002 amendment, CLAUDE.md no-engine-name rule), the 449-test baseline, and the next session's priority: implement ADR-0017 (per-cell classification, FK-target-type-aware PK precondition, uniqueness checks for PK + shortid, --force-conversion / --dont-convert flags, pretty-table rendered diagnostics). |
||
|
|
c3e5f9014f |
ADR-0017 + ADR-0002 amendment: type-change compatibility + engine-agnostic posture
Specifies the curated per-cell classification (clean / lossy / incompatible) for column type changes, the static transformer matrix (numeric chains, text↔structured types, always-clean stringifications), and the PK / shortid / uniqueness-bearing handling. Replaces the B2/C2 placeholder of "rely on engine STRICT and surface its errors" with a learner-friendly model that: * refuses incompatibles up-front, * refuses lossy conversions by default with a re-run-with- --force-conversion hint, * refines the PK refusal: an inbound-FK PK is only refused when the new type would change the FK target type (so `serial → int` and `shortid → text` on FK-referenced PKs are allowed; `int → text` etc. still refuse), * adds a post-transformation uniqueness check for PK and shortid columns, * uses the pretty-table renderer (ADR-0016) for all diagnostic row lists, * emits a `[client-side] …` note in the success summary whenever the transformer rewrote any cell. `--force-conversion` accepts loss; `--dont-convert` skips the client-side layer entirely; mutually exclusive. Forward-look: a future iteration may add resolution flags (`--default 0`, `--on-incompatible '<value>'`). Also amends ADR-0002 with a new "User-facing posture" section cementing that the database engine choice is an implementation detail and is never named in user-visible strings. Adds a corresponding bullet to CLAUDE.md's working-style rules so every session picks it up. Implementation lands as a follow-up. |
||
|
|
7b97786ab7 |
B2/C2: column drop / rename / change-type DSL commands
Closes B2 (rebuild-table reused outside relationships) and
C2 (full add/drop/rename/change-type column operations).
* drop column [from] [table] <T>: <col>
- ALTER TABLE DROP COLUMN (SQLite 3.35+) + metadata
cleanup in __rdbms_playground_columns.
- Refuses PK columns and columns involved in a declared
relationship (drop the relationship first).
* rename column [in] [table] <T>: <old> to <new>
- ALTER TABLE RENAME COLUMN (SQLite 3.25+); SQLite
cascades the rename through FK declarations on other
tables.
- Mirrors the new name into both metadata tables
(__rdbms_playground_columns, __rdbms_playground_relationships)
so describes stay accurate after a rename.
- Refuses identity rename and name collisions.
* change column [in] [table] <T>: <col> (<newtype>)
- Routes through the rebuild_table primitive (ADR-0013)
since SQLite ALTER doesn't support type changes.
INSERT INTO new SELECT FROM old; STRICT typing enforces
cell compatibility, transaction rolls back on mismatch.
- Refuses PK columns, relationship-involved columns,
`serial` target, and no-op same-type changes.
Adds 20 tests (parser + db layer); updates the in-app help
listing. Both prepositions independently optional in each
new command, matching `add column`'s grammar shape.
Total: 449 passing, 0 failing, 0 skipped (up from 429).
Clippy clean.
Known spec gap: column-type-change conversion compatibility
is not yet documented (currently relies on SQLite STRICT
errors); follow-up will close this.
|
||
|
|
41cef5399b |
parser: make to and table independently optional in add column
Previously the grammar accepted only `to table` together or neither. The user-stated convention is that bare table identifiers are accepted in unambiguous positions (matching how `add 1:n relationship from <T>.<col> to <T>.<col>` takes bare table names). Both `to` and `table` are now or_not'd independently, so all four combinations parse identically. Updates the in-app `help` listing to advertise the new shape: `add column [to] [table] <T>: <col> (<type>)`. 3 new parser tests cover the variants. |
||
|
|
1b27a0c9b1 |
runtime: suppress silent-rebuild banner for empty projects
A fresh-launch temp project enters run() with no .db, calls rebuild_from_text on an empty schema, and used to surface a "[ok] rebuild — 0 tables and 0 rows will be reconstructed" note that conveyed no information. Now we only emit the note when the project actually has tables. The explicit `rebuild` command still always reports its summary — the user asked. |
||
|
|
5b5e08d852 |
ADR-0016 + Iter 5/6 follow-up: pretty table rendering
Replaces the placeholder pipe-and-dash output with Unicode box-drawing tables for both data results and table-structure listings, per ADR-0016. * New `src/output_render.rs` module with `render_data_table` and `render_structure`. Hand-rolled to match the project's existing CSV/YAML pattern; ~300 lines. * Header-only outer-frame border style: outer ┌─┐│└─┘ box + ├─┤ header underline, no per-row separators. NULL renders as `(null)`; cell newlines/tabs/control chars become `↵`/`→`/`·` as display-only substitutions. * Type-aware column alignment: numeric types right-aligned, everything else left. `DataResult` gains a `column_types: Vec<Option<Type>>` field, populated from the existing metadata lookup at the two query sites in db.rs (no new query paths). * Structure view shows Name | Type | Constraints columns; References / Referenced-by sections retain plain-text format, leaving room for the future relationship-rendering ADR. * 18 new unit tests in output_render.rs (plus 4 insta snapshots for the canonical layouts). Existing assertions in app.rs and walking_skeleton.rs updated to match the new format. Total: 426 passing, 0 failing, 0 skipped (up from 408). Clippy clean. |
||
|
|
67d68db5f8 |
Iteration 6: --resume + persistent input history + migration scaffold
Closes out track 2's ADR-0015 backlog. * `--resume` CLI flag (L1a, ADR-0015 §7) opens the most- recently-used project, tracked in <data-root>/last_project. Mutually exclusive with a positional <project-path>; errors cleanly to stderr (above the shell prompt) on missing file or stale recorded path. last_project is rewritten on every successful project open (startup, load, new, save as, import). * Persistent input history (I2-persist, ADR-0015 §12). On project open, the in-memory navigable history is hydrated from the tail of history.log (capped at the in-memory cap). ProjectSwitched gains a `history_entries` payload field; App::seed_history is the entry point. Pipes inside source text round-trip via splitn(3); unknown escape sequences are passed through literally. * Migration framework scaffold (F3, ADR-0015 §9). New persistence::migrations module with MigratorRegistry + migrate_to_latest + ensure_project_yaml_migrated. Empty in v1 (production registry has no migrators); the loader runs through it on every project open and is exercised by tests with a fake v1→v2 migrator. Writes project.yaml.v<N>.bak before any migrator runs; verifies each step bumps the version field. Refreshes docs/requirements.md (A1 / I2 / F3 / E1 / L1a / test baseline) and adds docs/handoff/20260508-handoff-3.md covering both Iter 5 and Iter 6. Total tests: 408 passing, 0 failing, 0 skipped (up from 345 at handoff-2). Clippy clean. |
||
|
|
c6cf3df6dc |
Iteration 5: export / import commands
Implements the `export` and `import` app-level commands per ADR-0015 §11 + ADR-0007 amendment 1. - `export [<path>]` writes a zip of project.yaml + data/ to <data-root>/YYYYMMDD-<projectname>-export-NN.zip by default, preserving the project's directory name as the single top-level folder inside the archive. - `import <zip> [as <target>]` extracts an exported zip into a new named project and switches to it. Target name is derived from the zip's top-level folder by default; on collision the destination auto-suffixes -02, -03, ... up to -99 instead of refusing (deviates from §2's refuse-on- collision rule for save/save as; recorded as an amendment to ADR-0015 §11). - Excludes playground.db and history.log from the zip. - Path-traversal protection via zip::enclosed_name + post- resolution check that the extraction path stays inside the target directory. Adds the zip = "5" dep with default-features = false + features = ["deflate"] to keep the binary-size cost modest. Test baseline: 370 passing, 0 failing, 0 skipped. |
||
|
|
ca71184678 |
Handoff doc + CLAUDE.md and requirements.md refresh
Adds docs/handoff/20260508-handoff-2.md describing the state at the end of this session: ADR-0015 designed, Iterations 1-4 of track 2 shipped (file-backed projects with auto-named [temp] dirs, per-command write-through, rebuild from text on missing .db, save/save as/new/load/rebuild commands with modal dialogs and project switching), plus the cleanup pass (--help, in-app help, post-rebuild message, unmodified-temp cleanup) and the safety hardening of safely_delete_temp_project. Lists the next-up moves (Iteration 5: export/import, Iteration 6: --resume + persistent input history + migration scaffold) and an end-to-end smoke test. requirements.md: marks P1-P5, P-NAME-1/2/3, F1, F2, U3, L1 as [x] with iteration references; adds P-CLEAN-1 for the safe cleanup; updates A1, I2, H3, L1a progress notes. CLAUDE.md: updates the project-storage decisions and deferred-items entry to reflect what's now live vs. still pending. |
||
|
|
58a964da8c |
Harden temp-project cleanup with stacked safety guards
The previous remove_dir_all on a path returned by Project::path() was too trusting: an unusual CLI argument or a hand-edited project.yaml could in principle have steered cleanup into deleting the wrong directory. Replace it with safely_delete_temp_project, which refuses unless every one of the following passes: 1. Path is not a symlink (checked before canonicalize so a symlink can't smuggle a different target through). 2. Path is a directory. 3. Canonical path is under <active-data-root>/projects/ (canonical-prefix containment). 4. Directory basename contains the literal `[temp]` marker. 5. Direct children are exclusively well-known project artefacts (project.yaml, data/, history.log, playground.db, .gitignore, lock file) plus migration .bak files and atomic-write .tmp files. Any stranger file (notes.md, .git/, screenshots, etc.) makes the helper refuse. is_unmodified_temp now also requires data/ to be empty, in addition to project.yaml's tables and relationships being empty. A hand-edited yaml that drops the schema list but leaves CSVs in data/ no longer passes. Failure to delete is non-fatal -- the helper returns SafeDeleteError, the runtime logs a tracing::warn!, and the project stays on disk. Leaving an unexpected directory alone is always preferable to a wrong delete. Tests: 345 passing (272 lib + 9 + 5 + 6 + 27 + 9 + 17), 0 failing, 0 skipped. 7 new tests covering each guard, including a unix-only symlink-rejection test. |