Files
improvise/context/file-inventory.md
T

33 KiB
Raw Blame History

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, 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<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.