docs(context): condense repo-map to a digest; move gotchas and inventory to their own pages

repo-map.md was 173 KB (~43k tokens), too long to be part of every new
agent's context. The full gotcha text moves verbatim to context/gotchas.md
and the per-file inventory (with the old Wasm/CLI/Testing sections) to
context/file-inventory.md; the map keeps a one-or-two-line digest per
gotcha under the same numbers, so every `gotcha N` citation still resolves.

The doc-site manifest publishes the two new pages, and `gotchas` gets the
same gotcha-N anchors as `repo-map`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Edward Langley
2026-10-09 21:13:55 -07:00
parent b0ae354f53
commit f0119a437d
5 changed files with 671 additions and 531 deletions
+8 -6
View File
@@ -43,7 +43,7 @@ Not published:
will meet, and nobody visiting a website is about to read a PR thread.
None of the three is referenced by any published document except as a row in
`repo-map.md`'s file inventory, which is inside a code fence, so leaving them
the file inventory (`file-inventory.md`), which is inside a code fence, so leaving them
out creates no dangling link. That was checked rather than assumed.
**And "not published" is now derived rather than assumed** (improvise-res9).
@@ -233,11 +233,13 @@ recorded as a decision rather than left to be rediscovered as a bug.
## 6. Navigation
`repo-map.md` is one long document whose gotcha list is numbered 1 to 48, and
splitting it per gotcha was rejected: the gotchas cross-reference each other
constantly ("see gotcha 35", "for the reason gotcha 39 watches…"), and a reader
following those wants a scroll, not 48 page loads. It stays one page, with an
id on every list item and a compact strip of numbered links under the heading.
`repo-map.md` carries a digest of the numbered gotchas and `gotchas.md` the long
form; the map was condensed because it was too long to put in every new agent's
context. Splitting per gotcha was rejected: the gotchas cross-reference each
other constantly ("see gotcha 35", "for the reason gotcha 39 watches…"), and a
reader following those wants a scroll, not one page load per number. Each of the
two stays one page, with an id on every list item and a compact strip of
numbered links under the heading.
The ids come from the list item's **own number**, not from a counter, so an
inserted gotcha renumbers the anchors exactly as it renumbers the prose that
cites them.
+428
View File
@@ -0,0 +1,428 @@
# 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 This file
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.
+139
View File
File diff suppressed because one or more lines are too long
+93 -524
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -34,6 +34,8 @@ const SECTIONS = [
["docs/design-notes.md", "design-notes", "Design notes"],
["context/design-principles.md", "design-principles", "Design principles"],
["context/repo-map.md", "repo-map", "Repository map"],
["context/gotchas.md", "gotchas", "Gotchas (long form)"],
["context/file-inventory.md", "file-inventory", "File inventory (long form)"],
],
},
{
@@ -613,7 +615,7 @@ for (const section of SECTIONS) {
for (const [src, slug, title] of section.docs) {
const depth = slug.split("/").length - 1;
const ctx = { depth, cited, unresolved, anchors: new Set() };
if (slug === "repo-map") ctx.listItemId = (n) => (n >= 1 && n <= 200 ? `gotcha-${n}` : null);
if (slug === "repo-map" || slug === "gotchas") ctx.listItemId = (n) => (n >= 1 && n <= 200 ? `gotcha-${n}` : null);
const md = readFileSync(join(root, src), "utf8");
const { html, toc } = renderMarkdown(md, ctx);