Files
claude@clouddev1 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.
2026-05-08 07:07:38 +00:00

457 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session handoff — 2026-05-08 (2)
This is the second handover. The first session designed and
shipped track 2's project storage end-to-end: the design ADR
(0015), file-backed projects, per-command persistence to YAML
+ CSV + history.log, rebuild-from-text on missing `.db`,
explicit `rebuild` / `save` / `save as` / `new` / `load`
commands, `--help` / in-app `help`, and a hardened cleanup of
unmodified temp projects. Iterations 5 and 6 of track 2 are
pending; the next session can pick up from this file +
`CLAUDE.md` + the linked ADRs.
## State at handoff
**Branch:** `main`. Working tree clean. The track-2 commits
since the previous handoff:
```
58a964d Harden temp-project cleanup with stacked safety guards
b7addd6 Cleanup pass: --help, in-app help, post-rebuild message,
unmodified-temp cleanup
f219827 Iteration 4b: save / save as / new / load with project
switching
ba93d3c Iteration 4a: rebuild command with confirmation modal
f0fc063 Iteration 3: existence-only load + rebuild from text on
missing .db
5410075 Persistence: empty table -> no CSV (per Iteration 2
follow-up)
5c076f6 Iteration 2: per-command write-through to project.yaml,
CSVs, history.log
601d3b6 Iteration 1: file-backed projects with auto-named temps,
lock file, and L1 CLI
4fca862 Project storage runtime: ADR-0015 + ADR-0004/0007
amendments
```
**Tests:** 345 passing (272 lib + 73 across six integration
files), 0 failing, 0 skipped.
**Clippy:** clean with `nursery` lints enabled.
**Release build:** ~6.3MB single binary (up from 5MB at the
previous handoff; the increase is `serde_yml` +
`serde` derive + `sysinfo` + `directories` + `csv` +
`base64` + `gethostname`).
The user's terminal is a real TTY; the TUI runs cleanly there
but cannot be exercised from a non-TTY environment. `cargo
test` covers everything that doesn't require a real terminal.
## What's implemented (delta vs. previous handoff)
The previous handoff covered: TUI shell, in-memory SQLite,
DSL grammar, type system, INSERT/UPDATE/DELETE, auto-show.
All of that is still in place.
**On-disk projects (Iterations 13):**
- Auto-named temp project on startup under
`<data-root>/projects/`. OS-standard data root resolved via
the `directories` crate (Linux / macOS / Windows); overridden
by `--data-dir`.
- Naming pattern: `<YYYYMMDD>-[temp]-<word>-<word>-<word>`
with a built-in 161-word list. The literal `[temp]` segment
is what marks a directory as a temp project — brackets are
rejected by `validate_user_name` so user-named projects can
never collide.
- Per-command write-through to `project.yaml`,
`data/<table>.csv`, and `history.log`. Atomic per-file
write-tmp + fsync + rename. Commit-db-last ordering inside
the SQLite transaction so a text-write failure rolls back
the db and leaves disk state recoverable.
- `is_temp` is dir-name-based via the `[temp]` segment — fast
enough for the load picker without a YAML parse per
project.
- Empty tables produce no CSV (a header-only file would
carry no information YAML doesn't already record). The
rule is enforced by `Persistence::write_table_data`,
which delegates to `delete_table_data` on an empty
snapshot.
- Existence-only load on startup. If `playground.db` is
missing, the runtime rebuilds from `project.yaml` +
`data/` before the event loop starts; the result is
surfaced as a system message in the output panel rather
than a silent reconstruction.
- CSV reader is hand-rolled to preserve the NULL-vs-empty
distinction (the `csv` crate doesn't expose whether a
field was syntactically quoted).
**Lifecycle commands (Iteration 4):**
- `rebuild` — confirmation modal with a summary ("3 tables
and 47 rows will be reconstructed; the existing
playground.db will be replaced"). Y/N/Esc. The worker's
`do_rebuild_from_text` wipes existing schema + metadata
before reloading from text, so it works on populated as
well as empty databases.
- `save` — for a temp project, opens a path-entry modal
(acts as save as). For a named project, prints
"already auto-saved; use `save as` to copy to a different
location".
- `save as` — always prompts for a target. Relative names
resolve under `<data-root>/projects/`; absolute paths used
as-is. `copy_project` is a recursive copy that excludes
the per-process lock file (a fresh one is acquired on
open) and preserves everything else including
`playground.db`.
- `new` — closes current project, creates a fresh
auto-named temp, switches.
- `load` — opens an in-TUI picker. List mode shows projects
in the active data root sorted newest-first by
`project.yaml` mtime, with `[TEMP]` prefixes for temp
projects. Arrow keys navigate; Enter loads; `b` switches
to a path-entry sub-mode for projects elsewhere on disk;
Esc cancels. Empty data root jumps straight to path
entry.
- Modal infrastructure: `App.modal: Option<Modal>` +
per-modal key routing; renderer draws a centred overlay.
**Project switching at runtime:**
- The runtime owns a `Session` with `Option<Project>` +
`Option<Database>` + `data_root`. `perform_switch` handles
Load / SaveAs / NewTemp uniformly. The `take()` pattern
drops the old project (releasing its lock) before opening
the new one — required for the "load my own current
project" case.
**CLI / app polish:**
- `--help` / `-h` prints a usage banner (options +
app-level commands + DSL grammar reference) and exits.
Parse errors also print the banner.
- `help` in-app command notes the same listing to the
output panel — a stand-in for the H3 help system that
stays in sync with what's actually wired up.
- `[TEMP] ` prefix in the bottom status bar when the
current project is temp.
**Unmodified temp cleanup (with stacked safety guards):**
- On project switch and on quit, if the current project is
an unmodified temp (kind=Temp, `project.yaml` parses with
empty `tables` and `relationships`, AND `data/` is empty),
it is deleted.
- Deletion is gated by `safely_delete_temp_project` which
refuses unless ALL of: path is not a symlink; path is a
directory; canonical path is under
`<active-data-root>/projects/`; basename contains the
literal `[temp]` segment; 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.
Anything unexpected → refuse; never delete the wrong
thing.
- 7 dedicated tests cover each guard, including a `#[cfg(unix)]`
symlink-rejection test.
**Persistence module (`src/persistence/`):**
- `mod.rs``Persistence` handle (project path), atomic
write primitive, public types
(`SchemaSnapshot` / `TableSnapshot` / `CellValue` etc.).
- `yaml.rs` — hand-rolled writer, `serde_yml`-backed
reader. Emits `project.created_at` (writer) and parses
it (reader). Action keywords (no_action, restrict,
set_null, cascade — note: `set_default` is still
unsupported per ADR-0014).
- `csv_io.rs` — hand-rolled writer + reader. Per-type
encoding from ADR-0015 §4. Hand-rolled because the `csv`
crate strips the was-quoted bit we need for NULL-vs-empty
distinction. Bool stored as `INTEGER` in SQLite, written
as `true`/`false` in CSV.
- `history.rs` — append-only `history.log` writer. Format:
`<ISO-8601 Z>|ok|<source text>` with `\n` and `\r`
escaped inside the source.
## ADR index (read these before touching the related areas)
```
0000 Record architecture decisions (process)
0001 Language and TUI framework (Rust + Ratatui)
0002 Database engine (SQLite STRICT)
0003 Input modes and command dispatch
0004 Project file format
— amended by 0015 (derived-artifact framing,
rebuild-with-confirmation semantics)
0005 Column type vocabulary (ten types)
0006 Undo snapshots and replay log (deferred)
0007 Sharing and export
— amended by 0015 (history.log not in export zip,
but ALSO not in .gitignore — user decides)
0008 Testing approach (four tiers)
0009 DSL command syntax conventions
0010 Database access via worker thread
0011 FK column type compatibility
0012 Internal metadata for user-facing column types
0013 Relationships, naming, and rebuild-table strategy
0014 Data operations, value literals, and auto-show
0015 Project storage runtime (track 2 — implemented through
Iteration 4 + cleanup; Iterations 5 and 6 pending)
```
## What's pending — proposed next moves (in order)
### 1. Iteration 5 — `export` / `import` / `.gitignore` template
Per ADR-0015 §11 and ADR-0007:
- `export` — produces a zip containing `project.yaml` +
`data/`, **excluding** `playground.db` and (per ADR-0007
amendment 1) `history.log`. Default filename
`YYYYMMDD-<projectname>-export-NN.zip` with a non-clobbering
two-digit sequence. The user picks the output directory
(modal path entry).
- `import` — accepts an exported zip, unpacks it into a named
project at a chosen location, runs `rebuild` on open. The
zip lacks `.db` and `history.log`, so a fresh `playground.db`
is created from YAML+CSV and `history.log` starts empty. If
the chosen location already exists (per the §2 collision
rule from naming), `import` refuses with a friendly error.
- `.gitignore` template — currently the only items inside it
are `/playground.db`, `/.rdbms-playground.lock`,
`/project.yaml.v*.bak`. The amendment to ADR-0007 left
`history.log` out of the gitignore (user choice). That part
is already implemented (`Project::initialize_skeleton`); the
Iteration 5 work is just adding `export` / `import`
themselves.
Will need a zip dep — `zip` crate is the standard choice. Add
two new modal types (`ExportPathEntry`, `ImportPathEntry`) or
reuse `Modal::PathEntry` with new `PathEntryPurpose` variants.
Estimated scope: moderate — ~400600 lines + tests.
### 2. Iteration 6 — `--resume` + persistent input history + migration scaffold
The remaining ADR-0015 §1 / §7 / §9 / §12 items:
- `--resume` CLI flag opens the most recently used project
(path tracked in `<data-root>/last_project`). Errors cleanly
if missing; mutually exclusive with a positional path.
- Persistent input history (I2): on project open, hydrate the
in-memory navigable history from `history.log`'s most recent
entries. New successful commands keep appending as today.
- Migration framework (F3): scaffold only — register no
migrators yet, but the load path checks `version`, copies
`project.yaml` to `project.yaml.v<N>.bak`, runs the
registered migrators in order, writes back at the latest
version. Exercised the moment a v2 ever lands.
Estimated scope: small/moderate — ~300500 lines + tests.
### 3. Other deferred items (still untouched, in order of likely priority)
- **Complex WHERE expressions (C5a)** — AND/OR/comparison/LIKE
in UPDATE/DELETE/show-data filters. Bridge from DSL to real
SQL.
- **Indexes (C3 partial) + EXPLAIN QUERY PLAN (QA1)** — strong
teaching demo.
- **B2 column drops/renames/type changes** — the
`rebuild_table` primitive already exists.
- **Friendly error layer (H1)** — translate SQLite messages to
learner-friendly ones.
- **Session log + Markdown export (V4)** — bigger UX project.
- **m:n convenience (C4)** — auto-generates a junction table.
- **CI (TT5)** — the test infrastructure exists; the workflow
file does not.
## Sharp edges and subtleties (delta vs. previous handoff)
The previous handoff's sharp edges (sync `update`, worker
thread, metadata transactions, rebuild-table primitive,
wrap-aware scroll math) all still apply. New ones:
- **`Action::ExecuteDsl` is a struct variant.** Tests can no
longer pattern-match it via `vec![Action::ExecuteDsl(...)]`.
The walking-skeleton tests use `assert_one_execute_dsl` which
compares only the parsed `Command`, ignoring the source
string.
- **All public `Database` methods take `source: Option<String>`
as a final argument.** `None` skips the `history.log` append
(used for tests and internal calls); `Some(text)` records the
user-typed line. The Python-script bulk-update from
Iteration 2 added `, None` to every existing test call site.
- **`Persistence` is wired ONLY when calling
`Database::open_with_persistence`.** Tests that use
`Database::open(":memory:")` get no YAML/CSV/history.log
writes — that's intentional and lets the SQLite layer be
exercised in isolation.
- **`do_rebuild_from_text` wipes existing user tables and
metadata before reloading.** Works for the silent on-load
case (empty db: wipe is a no-op) and the explicit `rebuild`
case (replaces whatever was there).
- **The runtime's `Session` holds `Option<Project>` +
`Option<Database>`.** Project switches `take()` the old
values (releasing the lock and stopping the worker) BEFORE
opening the new ones — required for the
load-my-own-current-project case where the new open would
otherwise see a self-held lock.
- **`safely_delete_temp_project` is the ONLY way the runtime
ever removes a project directory.** Don't reach for
`std::fs::remove_dir_all` directly — use the helper, which
stacks containment / symlink-rejection / `[temp]`-marker /
contents-allowlist guards. Refusal is non-fatal; the
directory just stays put.
- **Modal state lives in `App.modal: Option<Modal>` and
routes ALL keys when active.** New modals plug into
`handle_modal_key` and the `render_modal` dispatcher in
`ui.rs`. A modal's submission yields one or more `Action`s
for the runtime to enact.
- **`AppEvent::RebuildSucceeded` is reused for both the
explicit `rebuild` command and the silent on-load
rebuild.** The handler is kind enough to be a no-op on
`modal = None`, so the dual use works without a special
case.
- **`is_unmodified_temp` requires BOTH empty schema (from
YAML) AND empty `data/` directory.** The combined check is
the authoritative signal; the `safely_delete` helper adds
defence-in-depth on the path side.
## Repository layout (delta vs. previous handoff)
```
src/
action.rs — Action enum (Quit / ExecuteDsl /
PrepareRebuild / Rebuild /
OpenLoadPicker / LoadProject /
SaveAs / NewProject)
app.rs — App state + Modal infrastructure
+ per-modal key handlers
cli.rs — Args + HELP_TEXT
db.rs — worker, do_rebuild_from_text,
finalize_persistence (+ all the
previous DDL/DML)
dsl/ — unchanged
event.rs — AppEvent (incl. RebuildPrepared,
RebuildSucceeded, RebuildFailed,
PersistenceFatal, LoadPickerReady,
ProjectSwitched, ProjectSwitchFailed)
lib.rs — re-exports incl. persistence + project
logging.rs — unchanged
main.rs — handles --help before booting
mode.rs — unchanged
persistence/ — new module (Iteration 2+)
mod.rs — Persistence handle, atomic write,
public types
yaml.rs — writer (hand-rolled) + reader
(serde_yml)
csv_io.rs — writer + reader (both hand-rolled)
history.rs — history.log appender
project/ — unchanged location, expanded:
mod.rs — Project, ProjectKind, lifecycle,
list_projects, copy_project,
safely_delete_temp_project,
is_unmodified_temp
naming.rs — slug generator with [temp] marker,
is_temp_dirname helper
prettifier.rs — strips date prefix AND [temp]-
lock.rs — unchanged from Iteration 1
wordlist.txt — 161 words
runtime.rs — Session, perform_switch,
spawn_prepare_rebuild,
spawn_rebuild
snapshots/ — insta snapshots (incl. new
rebuild_confirm_modal_dark)
theme.rs — unchanged
ui.rs — modal renderers
(render_rebuild_confirm,
render_path_entry,
render_load_picker)
+ status-bar [TEMP] prefix
tests/
walking_skeleton.rs — Tier-3 (existing)
project_lifecycle.rs — Iteration 1
iteration2_persistence.rs — Iteration 2 + the empty-CSV rule
iteration3_rebuild.rs — Iteration 3
iteration4a_rebuild_command.rs — Iteration 4a
iteration4b_lifecycle_commands.rs — Iteration 4b + safety
guards + cleanup tests
```
## How to take over
1. Read this file.
2. Read `CLAUDE.md` for the working-style rules and current
layout.
3. Read `docs/requirements.md` for granular progress.
4. Skim `docs/adr/README.md`; read ADR-0015 in full if you'll
touch the project storage runtime.
5. Run `cargo test` to confirm the 345-test green baseline.
6. `cargo run --release` to see the app — try the smoke test
below.
### End-to-end smoke test
Verifies temp-project lifecycle, persistence, switching, and
the [TEMP] / `[TEMP]` cleanup convention:
```
# Launch — should land in a temp project named like
# 20260508-[temp]-<word>-<word>-<word>; status bar shows
# "Project: [TEMP] <Display>".
$ rdbms-playground
# Inside the app:
help -- prints command list
create table Customers with pk id:serial
add column Customers: Name (text)
insert into Customers ('Alice')
insert into Customers ('Bob')
show data Customers -- two rows
save -- prompts for a name
-- type "MyOrders" + Enter
-- status bar updates;
-- [TEMP] disappears
# Confirm on disk: the OLD temp dir is deleted (was unmodified
# at the point of save... no wait, save HAS modifications;
# it stays). Test the unmodified-temp cleanup separately:
new -- creates fresh temp
-- previous (named) project
-- stays put
load -- picker shows MyOrders +
-- the new fresh temp
-- arrow-down to MyOrders,
-- Enter
-- the fresh temp gets
-- auto-deleted (was
-- unmodified)
# Now rebuild from text:
rebuild -- modal: Y
-- "[ok] rebuild — 1 table,
-- 2 rows reconstructed"
delete from Customers --all-rows
show data Customers -- empty
quit
```
If anything in that sequence fails, something is wrong. The
sequence exercises auto-temp creation, schema + DML
persistence (yaml + csv + history.log), `save` (temp →
named), `new` (close + create new temp), `load` picker (with
unmodified-temp cleanup of the just-created fresh temp), and
explicit `rebuild`.
### Manual spot-checks worth running
- `--help` and `-h` both print the usage banner.
- A project with a table and data: delete `playground.db`,
reopen — system message "[ok] rebuild — 1 table, 2 rows
reconstructed" appears in the output panel, data is
intact.
- Two `rdbms-playground` instances on the same project — the
second refuses with a clear lock-held error.
- `save` then `save as` from a named project — `save` says
"already auto-saved", `save as` prompts.
- `load` with `b` to switch to path-entry submode.
- After `quit` from a fresh-launch (untouched) temp, the
temp directory is gone from `<data-root>/projects/`.
- Add a `notes.md` to a temp project's directory; the
cleanup refuses (warn in tracing log) and the directory
stays.