# 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-/{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` (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) ``` ### Sources, incremental evaluation and circuits (`crates/improvise-core/src/model/`, `crates/{improvise-incremental,zset,zset-ffi,numtower}/`) ``` improvise-core/src/model/source.rs CellSource, ChangeSource, Select, Summary/Numbers/Agreement, BadParts, SharedSource; TableSource (test-support); `Model::over` is in model/types.rs improvise-core/src/model/over_tests.rs tests of a model over a source improvise-incremental/src/lib.rs Derived trait and re-exports reading.rs, delta.rs, eager.rs Reading, CellDelta/DeltaValue/cell_delta/moved_records, Eager (the oracle) group.rs, shapes.rs, formulas.rs maintained groups (ExactSum), shapes per category set, formulas by stem incremental.rs, edit.rs, fold.rs Incremental (over/sync/apply_changes), Edit, the fold pushdown.rs, reads.rs Pushdown over a query-answering source; reads shared with Eager harness.rs differential, probes, Maintained sched.rs, staged.rs Executor/Unit/Pool (Pool native-only); the aggregate/grid example on three units (head, aggregate, grid) zset/src/{circuit,ops,trace,edge}.rs Circuit (arena, Send not Sync, begin/commit/rollback), operators, Trace, edges (EdgeSink/EdgeSource/Pipe/EdgeCodec/Stamp) zset-ffi/src/{lib,engine}.rs the C ABI; include/zset_ffi.h (zs_circuit_begin/commit/rollback, ZS_ERR_TRANSACTION, ZS_ERR_INTERRUPTED) ``` ### 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 `); 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/source.md CellSource, Model::over, Summary, ChangeSource (ADR 009 step 7) context/code/improvise-incremental.md The Derived plug: steps 3-7, Eager's math, Scheduling context/code/zset.md Circuit, transactions, threads, edges 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`, `improvise-worker-server`, and (ADR 005, ADR 009) `numtower`, `zset` and `improvise-incremental`, the last three because `improvise-protocol` and `improvise-command` depend on them. Each `cargo check --target wasm32-unknown-unknown` clean with no feature flags (improvise-ywd); `wasmCrates` in `nix/ci.nix` lists the set the CI gate checks, and it also holds `improvise-gp` (improvise-z5eg), a candidate rather than a member, because staying wasm-clean is free now and expensive to retrofit. `zset-ffi` is native only and not in that list. **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` — 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 ` (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.