Files
improvise/context/file-inventory.md
T
Edward Langley cc113c732f docs(context): answer the review of the repo-map split
- repo-stats reads the gotcha list from context/gotchas.md (the digest in
  repo-map.md no longer measures the doc-growth signal) and reports it as
  `docs.gotchas`; the test fixture and its derived expectations follow.
- AGENTS.md and the wave agent-rules protect gotchas.md and
  file-inventory.md like the repo map they were split from.
- doc-site.md's publication table lists the two new pages.
- file-inventory.md's Context docs rows describe the three files correctly.
- the code-review skill points at gotchas.md for the long form.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 21:42:24 -07:00

431 lines
33 KiB
Markdown
Raw 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.
# File Inventory (the long form)
The per-file listing that `context/repo-map.md` summarises. Moved here unchanged.
Line counts are a **dated snapshot**, not a maintained fact: measured 2026-09-24 at `c1772ca9`, and **70 of 105 had drifted past 10%** in the three days since the 2026-09-21 refresh — eleven had doubled or more (`client.rs` 255 → 943, `lockstep.rs` 1126 → 4064). Treat them as "is this file big", never as a figure to cite; re-derive with `wc -l`. The `ws-server/` and `wasm-client/` entries use a shorthand prefix, so a refresh must map it to `crates/improvise-<name>/{src,}/` by hand or they drift silently. Test counts are informational — run `cargo test --workspace` for live numbers. Files under 100 lines and render-only widgets omitted.
### `improvise-core` (`crates/improvise-core/src/`)
```
model/types.rs 3560 / 112t Model, formula eval (fixed-point), CRUD
model/cell.rs 1358 / 47t CellKey (sorted), CellValue, DataStore (interned + index),
live_records (insertion order — gotcha 18)
model/record.rs 226 / 8t RecordHash / CellId surrogates: the opaque identities a
thin client names a cell by (ADR 002)
model/category.rs 310 / 9t Category, Item, Group, CategoryKind
model/symbol.rs 89 / 3t Symbol interning
view/layout.rs 2604 / 57t GridLayout, drill, records mode, records_columns; a private
per-axis position index (`row_pos`/`col_pos`, rebuilt by `reindex`)
so a data position resolves in O(1) (improvise-felo.13)
view/types.rs 817 / 43t View config (axes, pages, hidden, collapsed, format)
model/id.rs 290 slugify + IdAllocator: how a generated id is made (ADR 007
clause 4, core.md §20). Nothing outside the file calls it yet
view/axis.rs 190 / 5t Axis enum + label/next methods (no colour: that is a Face)
workbook.rs 413 / 22t Workbook: Model + cross-view ops, item facade
format.rs 519 / 40t format_f64, parse_number_format (display only)
```
### `improvise-formula` (`crates/improvise-formula/src/`)
```
parser.rs 1394 / 79t pest grammar + tokenizer → Formula AST
quote.rs 160 / 7t quote_name / is_bare_name / pipe_quote / unescape_pipe_body:
the one home, both halves (improvise-i2m5). improvise-gp
re-exports it; improvise-io calls it from
persistence/{format,parse}.rs
formula.pest 108 PEG grammar for formula text
ast.rs 130 Expr, BinOp, AggFunc, Formula, Filter
```
### `improvise-io` (`crates/improvise-io/src/`)
```
storage.rs 334 / 13t Storage trait (read/write/list/delete), PhysicalFs, MemoryFs,
`impl Storage for Rc<T>` (improvise-hmws), and
contract::storage_contract — public, not cfg(test), because
a browser must run that sequence (harness/)
opfs.rs 289 / 0t OpfsFs: Storage over the browser's Origin Private File
System. wasm32 only. Run by harness/, not by cargo test
persistence/improv.pest 179 PEG grammar — single source of truth
persistence/mod.rs 441 / 11t document lifecycle: save/load/gzip/legacy-JSON over Storage;
facade re-exporting format_md/parse_md/export_csv*
persistence/format.rs 401 / 11t format_md: Workbook → .improv text
persistence/parse.rs 1684 / 46t parse_md: pest tree-walk → Workbook (+ grammar generator)
persistence/export.rs 243 / 5t export_csv / render_csv. Builds its layout with
with_frozen_records, NOT new — `new` skips prune_empty
(improvise-iot6)
persistence/roundtrip_tests.rs
1236 / 62t format_md ↔ parse_md characterization (tests only)
import/wizard.rs 1967 / 62t ImportPipeline + ImportWizard. `Plan::place` is the one
placement pass, shared by `collisions()` and the build so
the two cannot disagree; `DuplicatePolicy` / `Collision`
(improvise-cgqt)
import/analyzer.rs 434 / 17t Field kind detection (Category/Measure/Time/Skip);
analyze_records is first-seen order, not HashSet (gotcha 18)
import/csv_parser.rs 590 / 26t CSV parsing, multi-file merge
```
### `improvise-protocol` (`crates/improvise-protocol/src/`)
```
lib.rs 619 / 2t ViewState, ViewCursor, ViewFrame, AppMode (17), Panel,
MinibufferConfig, MinibufColor, HELP_PAGE_COUNT
reduce.rs 749 / 10t Command + ModelCommand (the wire format), HelpPage, reduce_view
render.rs 2157 / 46t CellDisplay, WidgetCache trait + one cache per widget
(GridCache/CategoryTreeCache/FormulaCache/ViewListCache/
TileBarCache), AxisFrame/FrameEntry/FrameGroup and
header_level_widths (the grid frame, gotcha 44),
GridCache::seed (the bootstrap compaction), Projection,
bootstrap-only Snapshot
keymap.rs 975 / 0t KeyPattern, Binding, Keymap, ModeKey (17), TextEntry,
PanelKeymap, Keymaps::default_keymaps/prefix_keymap — the
shared table both front ends look up. Tests live with
dispatch in improvise-command/src/command/keymap.rs
viewmodel.rs 170 / 5t flatten_cat_tree — derivations between a cache and a
renderer, called by both front ends (improvise-edp)
face.rs 682 Face (a role: Axis, WizardField, Mode*, MinibufferOf…), Theme,
AnsiSlot — appearance as data a front end applies (ADR 004,
protocol.md §10); draw.rs::face_style is the TUI's one translation
key.rs 96 / 3t KeyCode, KeyModifiers, KeyEvent (moved from command/)
frame.rs 156 / 3t ClientFrame/ServerFrame — the ONE envelope pair, both
deployments (improvise-xks6). Nothing else defines one.
ClientFrame::Save is a request, not a Command (gotcha 45)
basis.rs 257 / 6t Basis (the three tiers), Rejection, BasisCache — ADR 001
layer 1. Basis has no Default and no public constructor
help.rs 1241 / 14t HelpPage content and Hint { keys: Vec<(KeyCode, KeyModifiers)>,
command, caption }. `compact` drops a chord by its MODIFIERS
(anything beyond shift), never by parsing a label it just
formatted (improvise-thpe) — the guard is the signature
resolve.rs 4883 / 88t the browser's key resolvers: a BrowserKey to the Commands it
composes, mirroring each Cmd line for line INCLUDING where
siblings disagree (gotcha 47). The largest file in the crate
client.rs 943 / 15t ClientState: ViewState + one cache per widget, and the fold;
in_flight, `opened` (OpenedCell), notify, settle_status
(gotcha 47). EditorSession is in lib.rs beside ViewState
```
### `gp-tree` / `improvise-gp` (`crates/gp-tree/`, `crates/improvise-gp/`)
```
gp-tree/src/lib.rs 575 / 8t Tree, Spec, eval, simplify, render, breed, Population,
SplitMix64 (vendored seeded Rng; seed() keeps the seed so
a fit can be shown and recorded, pinned to the published
reference vectors)
gp-tree/src/data.rs 1000 / 21t raw string rows -> f32 feature vectors: per-column type
inference, one-hot, mean-imputation + col.missing flag,
ColumnSpec/encode_row, Dataset (incl. warnings),
Model = trees + plan + seed + FitConfig
gp-tree/src/fit.rs 961 / 32t the search: FitConfig (Default = the CLI's numbers), fit()
-> simplified champion, cv_regress/cv_classify,
permutation_importance, folds/standardize/mse.
Returns data; never prints
gp-tree/examples/fit_csv.rs 583 CLI over the above: flags, demo dataset, all printing
gp-tree/tests/scoring_known_answers.rs
250 / 6t f32 bit patterns + a next_u64 witness: the evidence any
change to the scoring paths needs (gotcha 37c)
improvise-gp/src/lib.rs 978 / 25t formula_text / fitted_formula: a Tree as IF(...) formula
text, validated back through parse_formula. A threshold is
the SHORTEST decimal that reads back as the same f64 (not
the exact expansion), so a row on a split boundary routes
the same in f64 as in f32 eval; non-finite values panic
improvise-gp/src/dataset.rs 506 / 15t a View's records -> the feature matrix a fit consumes —
the improvise-side counterpart of preprocess. Records mode
is already a feature matrix (gotcha 1)
improvise-gp/src/fit.rs 1778 / 35t one fit end to end: a View's records in, an improvise
Formula and the evidence for believing it out. Not in a
command, because none of it is terminal-specific
```
### Command layer (`crates/improvise-command/src/command/`)
```
cmd/core.rs 475 / 2t Cmd trait, CmdContext, CmdRegistry, parse helpers
cmd/registry.rs 903 / 13t default_registry() — all registrations
cmd/navigation.rs 507 / 12t Move, EnterAdvance, Page*
cmd/cell.rs 724 / 20t ClearCellCommand, YankCell, PasteCell, TransposeAxes, SaveCmd
cmd/commit.rs 1304 / 27t CommitAndAdvance, CommitBuffer(CommitKind), CommitExport,
CommitSave
cmd/effect_cmds.rs 519 / 6t effect_cmd! macro, 25+ simple wrappers, `unless_unknown` (gotcha 51)
cmd/fit.rs 293 / 8t fit-measure: evolve a tree over the active view and store it
as an ordinary `## Formulas` line — nothing GP-specific is
left in the document afterwards
cmd/grid.rs 988 / 22t ToggleGroupAtCursor, ViewNavigate, DrillIntoCell, TogglePruneEmpty
cmd/mode.rs 469 / 13t EnterMode, Quit, EditOrDrill, EnterTileSelect
cmd/panel.rs 936 / 24t Panel toggle/cycle/cursor, formula/category/view panels
cmd/search.rs 202 / 4t SearchNavigate, SearchOrCategoryAdd, ExitSearchMode
cmd/text_buffer.rs 399 / 14t AppendChar, PopChar, CommandModeBackspace, ExecuteCommand
cmd/tile.rs 170 / 6t MoveTileCursor, TileAxisOp
cmd/wizard.rs 244 / 6t HandleWizardKey — per-step import-wizard key tables
keymap.rs 1353 / 44t KeymapSet, Keymap::dispatch/dispatch_transient, effects_of,
SetTransientPrefix — what a thin client cannot have. The
table itself is improvise-protocol/src/keymap.rs
parse.rs 350 / 23t Script/command-line parser (prefix syntax)
```
### Semantic UI state (`crates/improvise-command/src/ui/`)
```
ui/effect.rs 4020 / 99t Effect trait. 63 effects: 53 `impl Effect for` blocks, 9
simple_effect! expansions, and SetTransientPrefix in
command/keymap.rs. The 10 outside the brace-match are all
ViewOnly; 28 of the 53 are Model(_) (gotcha 38, h3y7)
ui/app.rs 5558 / 134t App, ModelState, ViewState + dispatch methods, Panel, handle_key, dispatch_key, document_generation, autosave; apply_effects declares `dirty` for WholeWorkbook and observes it for CellData/LayoutGeometry (gotcha 45)
ui/project.rs 6383 / 118t Dirty, Viewport, grid_of/category_rows/formula_texts/
view_summaries/tile_entries, grid_updates (the diff),
ClientSession + Sessions (connect/apply/rebase/project_all)
ui/reduce.rs 1217 / 24t Checked / CheckedBatch / apply_command / model_effects /
reduce_key — one door from a Command or a keypress to the
authoritative App. `reduce_full` is GONE (improvise-h3y7)
ui/host.rs 3410 / 67t Host: one App + one Sessions; handle, commands, project_all,
rebase. Lives here, not in ws-server (gotcha 45)
ui/cat_tree.rs 174 / 8t Category tree flattener for panel
ui/viewport.rs 259 compute_col_widths / compute_row_header_width / visible_cols
(pure geometry; the width rule itself is the protocol's);
grid_width, SIDE_PANEL_WIDTH, MIN_GRID_WIDTH (improvise-gtn7.1);
GridSummary / grid_summary / grid_window (improvise-felo.13)
```
### TUI rendering, draw, main (`src/ui/`, `src/draw.rs`, `src/main.rs`)
```
ui/grid.rs 905 / 14t GridWidget (ratatui): no &Model, no &View — the frame comes
off GridCache (gotcha 44). Its module doc is the best-written
statement of that rule in the repo
ui/help.rs 277 5-page help overlay (render only)
ui/import_wizard_ui.rs 829 / 9t Import wizard rendering. `Panel` is the one writer, and
every write is clipped to the panel's rows and width
ui/text.rs 169 truncate and wrap, both measured in terminal cells, never bytes
draw.rs 1122 / 20t TUI event loop, frame composition; `content_tests` draws a
whole frame to check the selection beside a side panel
main.rs 428 CLI entry (clap): open, import, cmd, script
# other ui/*.rs are small panel renderers — skip unless changing layout/style
tests/grid_frame_agreement.rs
479 / 6t the two front ends draw the same grid frame (improvise-anr9)
tests/records_wide_smoke.rs 67 / 1t
tests/shipped_documents_load.rs 333 / 8t every TRACKED .improv/.improv.gz loads, and every record
it projects is writable. "Shipped" is read off `git ls-files`,
not a .gitignore reading (improvise-c0aj), and the walk's git
is scrubbed of every hostile GIT_* setting the caller may carry
tests/web_demo_model.rs 83 / 2t every visible cell in web/demo.improv is a Record
```
### Browser host and client (`crates/improvise-ws-server/src/`, `crates/improvise-wasm-client/src/`, `web/`)
```
ws-server/registry.rs 616 / 14t Registry of Hosts keyed by DocumentId; ClientId {document,
session} — a SessionId is unique only within its document.
A document is never evicted while the server runs (ADR 001)
ws-server/host.rs 4 / 0t a re-export shim only. The session loop is
improvise-command/src/ui/host.rs (gotcha 45)
ws-server/origin.rs 132 / 6t OriginPolicy: which origins may open a socket. Loopback by
default, --allow-origin widens, absent allowed, two refused
(improvise-kc7n). The only decision transport::admitted makes
ws-server/transport.rs 330 / 4t tokio + tungstenite plumbing, plus the Origin check at the
handshake — a refused upgrade, not an ignored socket
ws-server/main.rs 190 / 5t CLI entry: serve a file on a port; --bind, --allow-origin
ws-server/tests/socket.rs 389 / 6t the one test that uses a real socket: handshake, JSON
framing, SessionId routing, which document a URL asked for
wasm-client/lib.rs 455 / 8t Client: the wasm-bindgen surface (init, on_message, on_frame,
viewport/set_viewport, render_model, set_cell_at, typed_cell)
wasm-client/keys.rs 187 / 5t BrowserKey ONLY — a KeyboardEvent flattened to an improvise
key. It does NOT resolve: `resolve` and KeyOutcome live in
improvise-protocol/src/resolve.rs, shared by both front ends
wasm-client/render.rs 1249 / 21t RenderModel: editable / selected / cat tree, decided here
wasm-client/socket.rs 547 / 13t the fold (fold_frame/handle_frame), the two compositions
and typed() text→CellValue. Defines no enums
# state.rs is GONE — ClientState moved to improvise-protocol/src/client.rs
wasm-client/tests/browser_round_trip.rs 2136 / 34t improvise-1ey's scope against a real Host
wasm-client/tests/lockstep.rs 4064 / 78t host and client agree on a keypress
web/app.js 816 string calls only, no protocol knowledge, no framework.
Run by harness/tests/shell.spec.js (Chromium; the standalone half on Firefox too) — see
gotcha 48 for what that does and does not reach
web/worker.js 86 three string calls, no protocol knowledge. RUN by
shell.spec.js's no-?ws path, with bindings.rs (gotcha 45)
web/shell.js 399 everything app.js DECIDES that is not painting: no DOM,
no browser API, one `node --test` away. Owns socketUrl,
gridPlan, frameCostOf, documentTitle — so a chunk of the
browser's render decisions is NOT in the file gotcha 42
and gotcha 46 name. Reasoning: context/code/clients.md
web/serial.js 19 the one rule worker.js exists to keep: one frame at a
time, in arrival order (gotcha 48's async-listener race),
extracted so it is testable without worker globals
web/index.html, style.css the page; legibility only
web/build.sh 38 wasm-pack build → web/pkg (gitignored). Needs
wasm-opt = ["-O", "--enable-bulk-memory"] or the bundled
wasm-opt rejects LLVM's memory.copy
web/assemble.sh 112 assembles the deployable site (copies web/*.improv)
web/demo.improv 65 the document the site ships and seeds from. Every cell a
Record — asserted (gotcha 46)
```
### Browser worker host (`crates/improvise-worker-server/`)
```
improvise-worker-server/src/worker.rs 765 / 17t Worker: one port, one client; handshake, Addressed
filter, persist trigger (model_state.dirty). Every decision, every test
improvise-worker-server/src/vfs.rs 348 / 8t documents/open: seeding is a Storage::write
improvise-worker-server/src/bindings.rs 185 / 0t wasm32 shim: OpfsFs awaits + postMessage. No Rust test
reaches it, but harness/tests/shell.spec.js DOES: the
no-?ws path runs start()/OpfsFs::open under Chromium
and Firefox.
Called NEVER RUN here for months: see gotcha 45
improvise-worker-server/src/lib.rs 51 / 1t the module doc that states exactly what is and is not
covered — the one correct copy of that fact
improvise-worker-server/tests/post_message.rs 407 / 8t main-thread traffic, strings only
```
### Browser harness (`harness/`) — not a workspace member
```
harness/playwright.config.js 50 projects: chromium (every spec), firefox (opfs, plus
shell.spec's standalone half: grepInvert @mocked-peer),
webkit (opfs only, which skips on a runtime probe: no
OPFS). CI runs `--project=chromium --project=firefox` only:
nixpkgs' WebKit aborts inside the runner VMs (ci.yml)
harness/tests/shell.spec.js 345 the first execution of web/app.js. A change to web/*.js
belongs here (gotcha 48). Playwright serves harness/serve/,
which harness/build.sh ASSEMBLES: edit web/*.js, rebuild,
or the specs run the old page
harness/tests/opfs.spec.js 103 drives the OPFS contract page and reads back its result
harness/opfs-contract/src/lib.rs 470 runs improvise_io::storage::contract::storage_contract
against a real OpfsFs inside a real Web Worker
harness/www/opfs/{index.html,worker.js} the page and worker that host it
harness/build.sh 50 assembles harness/serve/ (web/assemble.sh, the fixtures,
and the contract crate's wasm-pack build) — gitignored
# gated by the `browser` job in .github/workflows/ci.yml, which can fail the build
```
### Examples
```
examples/gen-grammar.rs Grammar-walking random .improv generator (pest_meta)
examples/pretty-print.rs Parse stdin, print formatted .improv
crates/improvise-core/examples/category_width_bench.rs
The same records at 12 and at 24 regular categories
(improvise-felo.1; context/code/core.md)
```
### Context docs
```
.claude/skills/code-review/ Review skill: checks a change against the doctrine in these docs + kb.pl
scripts/check-comment-runs.py The two-line comment cap AND --check-evidence. The flag is
ADDITIVE since improvise-ijql — every invocation that can
exit 0 answers the cap — guarded by a subprocess self-test,
the only level a dispatch bug is visible at. Reasoning:
context/code/tooling.md §2
scripts/check-ci-graph.py pages-deploy must `needs` every job in ci.yml, and every
suite `checkers.sh --list` knows must run in some CI job
(improvise-dak5). It REFUSES what it cannot read rather than
parsing YAML: the values it accepts are an allowlist
(tooling.md). --self-test; run in the `test` job. README
states the rule and lists no jobs (improvise-x7cq)
scripts/checkers.sh The one entry point for every *-test.sh suite: scrubs the
env to an allowlist, runs under the bash that runs it (no
version floor since #297). `ci_suites` is the table of which
suites each CI job runs (`--ci <job>`); ci.yml names none
scripts/pr-watch Waits on a PR's checks (the pr-watch skill). Exit 5: the PR
is already MERGED or CLOSED, so there is nothing to wait for
scripts/assemble-stamp.sh The cache-bust stamp gate web/assemble.sh calls, extracted
so its refusals can be exercised without a web/pkg
scripts/assemble-stamp-test.sh Its check_the_checker, run first in the `build` CI job
context/code/tooling.md The repository's own checkers: why an evidence heading's
names come from cargo rather than from the source, and
why a leaf name is not an identity
context/test-evidence/tooling.md Evidence for the gates in scripts/ and for the tests that
hold CI's shape
scripts/nix-seed-flake-inputs.sh Seeds locked flake inputs over git so `nix develop` works in cloud sessions
prolog-sessions/kb.pl Executable knowledge base: invariants, doctrine, decisions (CI-checked)
context/adr/007-stable-identities.md Stable identities: ids unique across the document are the
keys and names are display-only; gensym suffixes apply to
generated ids only, and hand-written ids are canonical.
Accepted 2026-09-25. (There is no ADR 005 or 006 file:
improvise-l2b8 and improvise-ilvl claim those numbers.)
context/adr/004-faces-and-themes.md Faces: appearance as UI-agnostic data a front end
applies, emacs-style. Accepted 2026-09-22.
context/adr/003-live-backed-models.md A model may be backed by a live query source —
improvise as a DB client. Accepted, NOT a priority.
context/adr/002-naming-a-cell.md How a client names a cell: an opaque surrogate on
CellDisplay, whose absence means the cell is an
aggregate. Records why _Index cannot serve.
context/adr/001-concurrent-edits.md Concurrent edits between clients: record-level atomicity,
optimistic basis check per batch, advisory reservations.
Records why a causal clock was rejected.
context/cloud-agents.md Cloud-session environment: tracker sync, bd pitfalls, what is blocked
context/design-principles.md Architectural principles & testing doctrine
context/plan.md Show HN launch plan
context/repo-map.md The map: crates, how-to-find-things, the gotcha digest, a file map
context/gotchas.md The long form of every numbered gotcha (the digest is in repo-map.md)
context/file-inventory.md This file: the per-file listing the repo map summarises
docs/design-notes.md Product vision & non-goals
```
---
---
# Wasm, CLI, testing and dependencies (long form)
The three repo-map sections of these names, as they stood before the map was condensed.
## Wasm
The browser bundle is `improvise-core`, `improvise-formula`, `improvise-io`,
`improvise-protocol`, `improvise-command`, `improvise-wasm-client` and
`improvise-worker-server`. All seven
`cargo check --target wasm32-unknown-unknown` clean with no feature flags
(improvise-ywd), gated by the `wasm` job in `.github/workflows/ci.yml`. That
job gates **eight** crates, not seven: `improvise-gp` was added (improvise-z5eg)
explicitly *not* as a bundle member but as a candidate, because staying
wasm-clean is free now and expensive to retrofit — so the bundle is seven and
the gate is eight, and neither number is the other one's typo.
**A feature that adds a dependency to a bundle crate must be checked with `cargo tree --edges normal`** — the default output includes **dev** edges, so a plain `cargo tree -p improvise-protocol --target wasm32-unknown-unknown` reports a dev-only `serde_json` as *present* and reads exactly like a leak. Check per-crate too, because the `wasm` CI job invokes each crate separately and feature unification cannot reach across that (improvise-26dm added a `test-support` feature on `improvise-protocol` on those terms; it is off in every shipped build).
`improvise-ws-server` is the host and is native only — tokio, tungstenite and
`Box<dyn Storage>` — so `improvise-wasm-client` cannot depend on it in anger. It
no longer needs to: both ends `pub use` `improvise_protocol::frame` and neither
defines a pair (gotcha 42). The **dev**-dependency remains, for
`tests/browser_round_trip.rs`, which hands the client's own serialized frame to
a real `Host::handle`. The root `improvise` crate is
the terminal binary and does **not** build for wasm — it depends on
crossterm; that is correct, not a gap.
`cargo check` passing is not the same as running. Two known runtime hazards
it cannot see: `Instant::now()` panics on wasm (hence `web-time` in
`improvise-command`), and any `std::fs` call compiles but fails in a browser
— improvise-fuj closed the effects that bypassed `Storage`; autosave and the headless final write still do (improvise-31oe.11).
## CLI
```
improvise [model.improv] # open TUI (default)
improvise import data.csv [--no-wizard] [-o out] # import CSV/JSON
improvise cmd 'add-cat Region' -f model.improv # headless command(s)
improvise script setup.txt -f model.improv # run script file
```
Import flags: `--category`, `--measure`, `--time`, `--skip`, `--extract`, `--axis`, `--formula`, `--name`, `--split-date <field>` (a date column imports as ONE hierarchical category by default; this restores the flat split, and `--extract` still picks the components — improvise-8jk6.3), `--strict` (with `--no-wizard`, any skipped record is a refusal and nothing is written, where the default writes and warns with a count — improvise-8jk6.6), and `--on-duplicate refuse|sum|separate` (default `refuse`; applies with `--no-wizard`, where the wizard would otherwise ask — improvise-cgqt). A headless import also refuses a `--split-date` that names a non-date field and an `--axis` that names a category the import did not build (exit 1).
---
## Testing — the short version
Full guidance lives in `context/design-principles.md` §7. Quick reminders:
- **Two numbers, not one** — the ambiguity between them is what let the old
"suite runs in <2s" drift: **no single test binary over 2 seconds**, and the
workspace wall clock is the sum. Measured at `63a5c13`: **1464 tests over 35
binaries** (1 ignored), longest single binary **0.6–1.0s across two runs on one machine** —
the spread being its own argument for reading your own run. Excludes compilation,
and excludes `harness/` (Playwright, minutes, its own CI job). *Re-measure* rather
than re-assert — `cargo test --workspace` prints a `finished in` per binary, and
**the portable signal is the ratio, not the seconds**. **Not the ratio to the
median, though: it is degenerate.** Most binaries print `0.00s` at two decimals,
so the median is zero and every ratio to it is undefined (improvise-i5z6, 39
binaries). Compare the slowest binary to the next few slowest instead: an outlier
is one that sits well clear of them *in every run*. That is how i5z6 was read —
`improvise-formula`'s lib longest of 39 in all three runs, at 0.91–1.95s against a
runner-up's 0.50–0.92s. Both outliers so far had one cause: a generator re-parsing
its pest grammar per case. `improvise-io` (improvise-8shu, 1.42s → 0.21s) and
`improvise-formula` (improvise-i5z6, 0.84–0.98s → 0.06–0.07s) each read it once through a
`OnceLock` now. A new grammar-walking strategy should do the same from the start.
The full argument is `context/design-principles.md` §7.
- Commands: build a `CmdContext`, call `execute`, assert on returned effects. No terminal needed.
- Property tests (`proptest`, default 256 cases) cover invariants: CellKey sort, axis consistency, save/load roundtrip, `parse(format(parse(generate())))` stability.
- **Bug-fix workflow**: write a failing test *before* the fix (regression guard). Record the bug in `context/test-evidence/`, **keyed by test name** — *not* in the test's doc comment, which the two-line comment cap forbids and `scripts/check-comment-runs.py` enforces. A heading may be module-qualified and must be when its leaf is not unique.
- Coverage target ~80% line/branch on logic code; skip ratatui render paths.
---
## Key dependencies
ratatui 0.30, crossterm 0.29, clap 4.6 (derive), serde/serde_json, indexmap 2, anyhow, chrono 0.4, pest + pest_derive, flate2 (gzip), csv, enum_dispatch. Dev: proptest, tempfile, pest_meta.