- AGENTS.md: "Three files" is now "These files"; the table lists five. - Digests 43 and 45 regain the traps the review found missing. - protocol.md and formula-and-io.md cite file-inventory.md for the Wasm appendix and the inventory, which moved there. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
29 KiB
Repository Map (LLM Reference)
Terminal pivot-table app. Rust 2024, Ratatui TUI, command/effect architecture.
Apache-2.0. Root binary+lib improvise; sub-crates under crates/:
improvise-core—Model,View,Workbook, number formatting. No UI/IO.improvise-formula— formula parser, AST,parse_formula.improvise-io—.improvsave/load, CSV/JSON import,Storagebackends. No UI/commands.improvise-protocol— the wire vocabulary and the portable session slice:Command,Projection,ViewState, the per-widget caches,reduce_view, key types. Depends only onimprovise-core+ serde. No IO, no command layer, no terminal.improvise-command— command/effect layer + semanticAppstate (app.rs, effect.rs, cat_tree.rs, viewport.rs). Depends onimprovise-protocol. No ratatui/crossterm.gp-tree— problem-agnostic genetic-programming regression trees (Tree,Spec,breed,simplify,render). Knows nothing about improvise.improvise-gp— the one seam between the two: emits a fittedgp_tree::Treeas improvise formula text (formula_text/fitted_formula). Depends ongp-tree,improvise-formulaandimprovise-core—dataset.rsbuilds a feature matrix from aView's records (improvise-4s8) andfitted_formulatakes the&Modelit must resolve against (improvise-8xhy). Nothing depends on it yet.improvise-ws-server— the host: tokio + tungstenite, aRegistryofHosts keyed byDocumentId(multi-document, ADR 001),Sessionsfan-out, and the socket framing (wire.rs). Native only.improvise-worker-server— the other host: the sameApp,SessionsandHostas the socket server, behindself.onmessage/self.postMessagewith OPFS underneath. wasm32 only in anger;worker.rs/vfs.rsare ordinary Rust and are where every decision and every test is. Nothing depends on it.improvise-wasm-client— the browser client: the fold, the key classifier, the socket framing it mirrors (socket.rs), and the render model the DOM paints (render.rs). Non-dev deps areimprovise-protocol+ serde + wasm-bindgen and nothing else. The page itself isweb/.
The main crate (and improvise-command) re-export the sub-crates as crate::{model, view, workbook, format, formula, persistence, import} so consumer paths stay stable when crates shuffle. The main crate also re-exports improvise_command::command as crate::command and improvise_command::ui::{app, cat_tree, effect, viewport} under crate::ui. ui::app re-exports the protocol's AppMode, ViewState, ViewCursor, ViewFrame, Panel, MinibufferConfig, MinibufColor and HELP_PAGE_COUNT, and command::key re-exports the protocol's key types, so every pre-existing path still resolves.
Architectural intent lives in context/design-principles.md — read that for the "why". This doc is the "where".
How to Find Things
| I need to... | Look in |
|---|---|
| Add a keybinding | improvise-protocol/src/keymap.rs → Keymaps::default_keymaps() (the table; the browser looks up the same bindings). Resolving a bound name into effects is improvise-command/src/command/keymap.rs. |
| Add a user command | command/cmd/<submodule>.rs, register in registry.rs |
| Add a state mutation | ui/effect.rs → implement Effect |
| Change formula eval | model/types.rs → eval_formula / eval_expr |
| Change cell storage / lookup | model/cell.rs → DataStore |
| Change category/item behavior | model/category.rs → Category |
| Change view axis logic | view/types.rs → View |
| Change an axis label or cycle | view/axis.rs → impl Axis. An axis's colour is Face::Axis(axis) in improvise-protocol/src/face.rs, resolved by the Theme and turned into a style by draw.rs::face_style (ADR 004) |
| Ask "what data is visible" vs "what can the user page" | View::filtering_cats() vs categories_on(Axis::Page) |
| Change grid layout | view/layout.rs → GridLayout |
Change .improv format |
persistence/improv.pest + persistence/{format,parse}.rs |
| Change where documents are stored | storage.rs → Storage impls (opfs.rs for wasm) |
| Change number display | format.rs → format_f64 |
| Change CLI args | main.rs (clap) |
| Change import logic | import/wizard.rs → ImportPipeline |
| Change how a gp-tree fit searches | gp-tree/src/fit.rs → FitConfig / fit |
| Change gp-tree CSV preprocessing | gp-tree/src/data.rs → preprocess |
| Change how a name is quoted anywhere | improvise-formula/src/quote.rs — the one home, both halves |
| Change import-wizard keys/steps | command/cmd/wizard.rs (keys) + import/wizard.rs (steps) |
| Change frame layout | draw.rs → draw() |
| Change how wide the TUI grid is beside a side panel | improvise-command/src/ui/viewport.rs → grid_width (reached through App::grid_width); draw.rs::draw_content splits on it and App::cmd_context counts visible_cols on it (improvise-gtn7.1) |
| Change what the browser page draws | improvise-wasm-client/src/render.rs → RenderModel |
| Change a browser↔server frame | improvise-protocol/src/frame.rs → ClientFrame/ServerFrame. The one pair; both ends pub use it and neither defines one (improvise-xks6) |
| Change the worker's message loop | improvise-worker-server/src/worker.rs → Worker::on_frame |
| Change the session loop (either host) | improvise-command/src/ui/host.rs → Host::handle |
| Change how the browser opens or seeds a document | improvise-worker-server/src/vfs.rs → open / documents |
| Change what a browser resolves without the host | improvise-protocol/src/resolve.rs → resolve. Not wasm-client/src/keys.rs, which holds only BrowserKey (KeyboardEvent → improvise key) — this row named it until 2026-09-21 and that pointer is how a false claim reached context/ (improvise-e5pf) |
| Change the browser page itself | web/app.js (thin), web/index.html, web/style.css |
| Host another document | improvise-ws-server/src/registry.rs → Registry, ClientId |
| Change app state / modes | ui/app.rs → App, AppMode |
| Write a test for model logic | model/types.rs → mod tests / formula_tests |
| Write a test for a command | command/cmd/<module>.rs → colocated mod tests; helpers in cmd/mod.rs::test_helpers |
Central Pattern: Cmd → Effect
keypress → Keymap lookup → Cmd::execute(&CmdContext) → Vec<Box<dyn Effect>> → Effect::apply(&mut App)
(pure, read-only ctx) (list of mutations) (only mutation site)
pub trait Cmd: Debug + Send + Sync {
fn name(&self) -> &'static str;
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>>;
}
pub trait Effect: Debug {
fn apply(&self, app: &mut App);
fn kind(&self) -> EffectKind; // NO default — the compiler makes you classify
}
// EffectKind::{ViewOnly, Model(ModelChange)}
// ModelChange::{CellData, LayoutGeometry, WholeWorkbook}
CmdContext (see command/cmd/core.rs) holds immutable refs to Model, Workbook, View, GridLayout, the CmdRegistry, AppMode, cursor/offsets, buffers, yanked cell, key code, expanded cats, panel/tile cursors, the opened editor, the view back/forward stacks, visible_rows/visible_cols and the cursor cell's display_value.
Add a command: implement Cmd in a command/cmd/ submodule, register in registry.rs, bind in default_keymaps(). Simple wrappers go through the effect_cmd! macro in effect_cmds.rs.
Add an effect: implement Effect in ui/effect.rs, add a constructor fn if it helps composition.
Key Types (skim; read the source for fields/methods)
Model (model/types.rs): categories: IndexMap<String, Category>, data: DataStore, formulas: Vec<Formula>, views: IndexMap<String, View>, active_view, measure_agg. MAX_CATEGORIES = 24 (regular only) — pub, re-exported as improvise_core::model::MAX_CATEGORIES, and the one home: the import wizard reads it rather than keeping a copy (improvise-felo.1). Virtual categories _Index, _Dim, _Measure always exist. Use regular_category_names() for user-facing pickers.
CellKey (model/cell.rs): Vec<(cat, item)> — always sorted by category. Build with CellKey::new(coords) / with(cat, item) / without(cat). Never construct the inner Vec directly.
CellValue: Number(f64) | Text(String) | Error(String). Errors surface from formula eval (circular, div/0, etc.).
DataStore (model/cell.rs): interned symbols + secondary index (Symbol, Symbol) → {InternedKey}. Hot-path lookups go through matching_values / matching_cells.
Formula AST (improvise-formula/src/ast.rs):
Expr { Number, Ref, BinOp, UnaryMinus, Agg, If }BinOp { Add, Sub, Mul, Div, Pow, Eq, Ne, Lt, Gt, Le, Ge }AggFunc { Sum, Avg, Min, Max, Count }Formula { raw, target, target_category, expr, filter }
View (view/types.rs): category_axes: IndexMap<cat, Axis>, page selections, hidden items, collapsed groups, number_format, prune_empty. It carries no cursor — selected/row_offset/col_offset moved to ViewState::cursors (improvise-8uz). Axis { Row, Column, Page, Filter, None }. cycle_axis rotates Row → Column → Page → None; Filter is outside the rotation (see gotcha 27).
GridLayout (view/layout.rs): pure function of Model + View. cell_key(r,c), cell_value(r,c), drill_records(r,c). Records mode auto-detects when _Index is on Row and _Dim is on Column.
AppMode (ui/app.rs): 17 variants (Normal, Editing, FormulaEdit, FormulaPanel, CategoryPanel, ViewPanel, TileSelect, CategoryAdd, ItemAdd, ExportPrompt, SavePrompt, CommandMode, ImportWizard, Help, Quit, RecordsNormal, RecordsEditing). Eight text-entry variants carry a MinibufferConfig. SearchMode is Normal + search_mode: bool, not its own variant. Per-variant UI facts live on the enum as exhaustive-match methods — label(), hint_text(), panel(), minibuffer(), is_editing(), is_records() (improvise-2hi); add a variant and the compiler names every one you must fill in. A mode's colour is a Face (Face::Mode*, Face::of_minibuffer), turned into a ratatui Style only by src/draw.rs::face_style. App state is split into ModelState (workbook, file_path, dirty) and ViewState (mode, cursor/offsets, buffers, panels, search) — see improvise-x2c/ew0. wizard and drill_state live on App, not ViewState (improvise-h0b) — see gotcha 24.
Keymap (improvise-protocol/src/keymap.rs; dispatch and KeymapSet in improvise-command/src/command/keymap.rs): Binding { Cmd | Prefix(Arc<Keymap>) | Sequence(Vec<…>) }. Lookup fallback: exact(key,mods) → Char(NONE) → AnyChar → Any → parent. 17 mode keymaps, one per ModeKey, built by Keymaps::default_keymaps(); mode resolved via ModeKey::from_app_mode(). ModeKey and AppMode are two different enumerations that happen to be the same size — SearchMode is a ModeKey and not an AppMode; Quit is an AppMode with no keymap — so do not "fix" one count to match the other.
.improv File Format
Plain-text, markdown-like, defined by persistence/improv.pest. Parsed by pest — the grammar is the single source of truth (the grammar-walking test generator reads it via pest_meta).
v2025-04-09
# Model Name
Initial View: Default
## View: Default
Region: row
Measure: column
|Time Period|: page, Q1
hidden: Region/Internal
collapsed: |Time Period|/|2024|
format: ,.2f
## Formulas
- Profit = Revenue - Cost # defaults to [_Measure]
- Tax = Revenue * 0.1 [CustomCat]
## Category: Region
- North, South, East, West # bare items, comma-separated
- Coastal_East[Coastal] # grouped item, one per line
> Coastal # group definition
## Data
Region=East, Measure=Revenue = 1200
Region=West, Measure=Revenue = |pending| # pipe-quoted text
- Name quoting: bare
[A-Za-z_][A-Za-z0-9_-]*, else CL-style|…|with escapes\|,\\,\n. Same convention in the formula tokenizer. - Write order: Views → Formulas → Categories → Data. Parser tolerates any order.
- Gzip:
.improv.gz(same text, gzipped). - Legacy JSON: auto-detected by leading
{; never written. - Axis keywords:
row,column,page,filter,none;page/filtercarry the selected item after a comma. - Virtual categories:
_Index/_Dimnever persist;_Measurepersists only non-formula items (formula targets are rebuilt from the## Formulassection). - Virtual views:
_Records/_Drill(view::VIRTUAL_VIEW_NAMES) are never written and are dropped on load if an older file carries them; saving from records/drill mode writes the first real view asInitial View. Their commands rebuild them on demand.
Gotchas (digest — read before editing)
One or two lines each: the rule, and the trap. The numbers are stable and cited across the repo; a
citation of gotcha N means entry N in context/gotchas.md, which holds the full argument,
the incidents and the ticket ids. Read the long form of every gotcha your diff touches. Counts and
"closed/open" claims in the long form are dated snapshots.
- Records mode is wide format. A row is a coordinate tuple; columns are
[coords…, +cat, raw measures…, +]. Editing a category column moves every measure cell in the row._Dimcarries a column index, resolved byGridLayout::record_col.GridLayout::newdoes not prune;with_frozen_records(m, v, None)does — production code wants the latter.prune_axis_itemstakes a predicate and re-marks survivors. - Effects never touch
std::fs. Documents go throughApp::storage: Box<dyn Storage>. Autosave and the headless final write still bypass it (improvise-31oe.11). - Every effect declares an
EffectKind; there is no default.ViewOnlynever reaches other clients;Model(CellData | LayoutGeometry | WholeWorkbook)does. The view lives inside the workbook, so axis/page/format areLayoutGeometryand cursor/scroll/SwitchViewareViewOnly;Add/RemoveCategoryareLayoutGeometry.EffectKindis too coarse to check theModelCommandmapping —every_model_command_names_the_effect_of_the_same_namedoes. - Commands never mutate.
&CmdContextis read-only; return effects. CellKeyis always sorted. Use the constructors.- No
Model::add_item. Use theWorkbookfacade (wb.add_item) or go through the category. - Virtual categories.
_Measureitems = explicit ∪ formula targets (measure_item_names()). Cell aggregation (apply_agg) and formulaSUM/AVG(eval_agg) differ on empty input; non-numeric values reduce by unanimity, elseError("mixed")— never dropped.evaluate_aggregatedtakesAggregation::{Exact, OverHidden}. - Display rounding is view-only.
format_f64only in rendering; never feed a rounded value back into eval. - Formula eval is fixed-point.
recompute_formulasiterates to stability; cycles becomeError("circular"). - Drilling a formula cell strips the
_Measurecoordinate so the raw feeding records come back. - Minibuffer keymaps come from
TextEntry(eight text-entry modes); the buffer a prompt names and the keys edit are one string by construction. Read/write a runtime-named buffer withread_buffer/write_buffer.SearchModeis hand-written. - Panel keymaps come from
PanelKeymap;Panel::ALL/name()/key()drive the toggle keys. - A prefix key is stored as an address (
ViewState::transient_prefix), resolved byKeymaps::prefix_keymap; prefixes must stay flat (prefix_keymaps_are_flat).ViewStatestays free ofimprovise-commandtypes. - Keybindings are per-mode (
ModeKey::from_app_mode); minibuffer Enter/Esc areBinding::Sequence.every_hint_key_resolves_in_its_modechecks which keys a hint names, not what they do. - Registry names come from prototypes, not strings — except the
Moveliterals and four aliases, guarded by tests; a wrong-variant prototype shows up as a duplicate registration. effect_cmd!/simple_effect!wrap one effect;simple_effect!'s kind argument is mandatory. Six wrappers returnunless_unknown(...)(gotcha 51). Mode entry isenter-mode <name>; no per-mode shim commands.- One float epsilon (
FLOAT_EQ_EPSILON = 1e-10) for=/!=and the div/0 guard, in both evaluators. - Order is data.
IndexMaporder is persisted; an index ofHashSets is for lookup, never iteration (DataStore::matching_*sort to store order;analyze_recordsis first-seen order). A group's fold reads through the same rule (spanned_positions). - Cell-edit commit paths split (
commit_regular_cell_value/commit_plain_records_edit); keep both in sync.cmd::core::uneditableis the one decline, asked at open and commit.CellId::NonItemis a stored value naming no item. - Minibuffer commits go through
commit-buffer <formula|category|item>; the add effect is emitted last so its failure message wins. - An abort stops the chain, never the bookkeeping.
Save/SaveAs/AddCategorysetabort_effects; tidy-up (mode restore, buffer clear) goes at the head of a batch. An abort is not a rollback (gotcha 47). - The import wizard's back path is a history stack. Preconditions live in
validation_error();ResolveDuplicatesappears only whencollisions()is non-empty; headless defaults toDuplicatePolicy::Refuse. A collision is a cell written by two records. app.layoutgoes stale mid-batch — put aRebuildLayouteffect before any effect that reads the layout after a grid change. The rebuild is skipped when nothing it reads moved (felo.13); a writer bypassing effects must callrebuild_layout().- The cursor is per-client, keyed by view (
ViewState::cursors; useApp::cursor()/cursor_mut()). Replacing the document clears the map and bumpsdocument_generation. Cursor invalidation is observed from the grid frame (cursor_epoch), not called by effects; two arms (LayoutGeometryhashes the active view,CellDataevery view). ViewStateand its field types deriveSerialize/Deserialize— no borrowed-'staticfields anywhere it reaches.- Axis presentation lives on
Axis; colour is aFaceresolved by theTheme;draw.rs::face_styleis the one translation to ratatui. Axis::Filterconstrains without being navigable. Visible data =filtering_cats(); user-actionable =categories_on(Page)/tileable_cats(). What a virtual view must be is model-side (records_steps,drill_steps).- The browser backend is sync on purpose, with an async seam (
OpfsFs;open/attach/refresh/commit_deletes). It needs a dedicated worker —createSyncAccessHandleis absent in aSharedWorker. - "Active view" is stored twice and may differ:
Workbook::active_viewis the default,ViewState::active_viewis authoritative per client.App::resolve_viewis the one fallback;Sessions::adopt_deleted_viewshandles a deleted view. - Two tests gate
ViewStatemembership: portable (noRc/Arc) and snapshot-safe.wizardanddrill_statelive onAppas session-local residue. :commands settle their mode by observation (ReturnToNormalUnlessMoved), not by a per-effect constant.- There is no aggregate render cache. One cache per widget (
WidgetCache), aProjectionis one update to one widget; bootstrap is a compaction (Snapshot { view, seed }). The wizard has a cache but deliberately no projection. - The grid's labels frame its cells; the fold is not commutative. A
Cellsupdate that overtakes its frame is dropped;applyinvalidates relabelled positions itself; every arm touching labels or widths re-fits. Don't coalesce a batch. - A formula target is unquoted; pipe quoting has one home (
improvise_formula::quote). The keyword guard is canonical.header_nameis total; duplicate## Category:sections are one category;WHEREneedssp+. Dirtyover-reports on purpose — a redundant projection is cheap, a missed one leaves a client wrong forever.LayoutGeometryincludesformulas.- A client's command has one door:
Sessions::apply_checked(app, id, Checked). Assert theBroadcastitself, and seat a bystander where a wrong relay can move it. gp-treereturns data and never prints; a fit needs seed andFitConfig. A scorer takesRow<T>slices.fitted_formularefuses features the document cannot name; categories are forcedCategorical(zv8t).- The wire vocabulary is derived twice (
Commandview half,ModelCommandmodel half). Completeness is checked (vocabulary_is_complete.rs).ApplyAndClearDrillandAcceptWizardModelare deliberately off the wire (session-local operands). Recount, don't cite. Appliedis an enum (Diff | Rebased | Rejected) because a document swap is not a diff; the trigger is the generation moving, not the variant. A swap fixture must keep the view name.- A batch's basis is three tiers (
document_generation,layout_version,record_versions); the unit is the record; lower tiers are observed, not read off the effect. Storageis not a VFS; SQLite was declined on measurement (a second serialization is a second grammar).- Browser render decisions are made in Rust (
RenderModel:editablefrom theCellIdvariant, selection reconciled with offsets,flatten_cat_tree).improvise_protocol::frameis the one envelope pair for both deployments. - A batch carries its basis, and the server publishes it (
Projection::Basis).Basishas noDefaultor public constructor;Checked/CheckedBatchmake "ignoring the basis" unrepresentable;RejectionkeepsRecordGone/RecordMoved/RecordUnstatedapart. Anti-staleness, not security. The check is a property of the session door (apply_commanddoes not check), and only two of the three tiers hoist per frame — the record tier is asked per command (records_named) against the frame's basis. Read gotcha 43 before touching it. - The TUI grid draws from
GridCache, which frames it too (AxisFrame; width ruleheader_level_widths). Use the one-pass accessors, not per-position scans.RecordHashis document-scoped,AggregateHashlayout-scoped. - One session loop (
Host), two transports. Saving is a request frame (ClientFrame::Save); dirtiness is declared forWholeWorkbookand observed forCellData/LayoutGeometry(MarkDirtyis gone).impl Storage for Rc<T>lives inimprovise-io.harness/runsOpfsFsfor real; Safari is unseen. - The page picks a peer:
?ws=→ socket, else a moduleWorker.#statuscomposes two facts; the first-frame watchdog is per-peer-agnostic. The demo model is built so every cell is aRecord. - Keys become commands in the browser; the server must not know the keymap.
GridExtent,set_viewport,Commands(Vec),resolve::NEEDS_THE_HOSTand the five client rules (projected_at,opened,refused_here,Step::Declined,notify). - The browser JS is partly executed (
harness/tests/shell.spec.js); changes toweb/*.jsbelong there. Serialize frames on one tail promise; disarm, don't pre-empt, a watchdog. Evidence headings may be module-qualified (and must be when the leaf collides). - A websocket handshake is exempt from same-origin policy —
src/origin.rsrefuses the upgrade (loopback and absent allowed, two refused). Not authentication. - A composed TUI row measures in cells and names which side yields (
fill_line+Keeps). Keep whichever key table is derived from what it describes. Scrolls are measured where drawn (grid_width). - The command language reads pipes, refuses surplus, and checks names when applied (
only_when_omitted;RequireKnown/unless_unknown). - A date column imports as one hierarchical category;
--split-daterestores flat.Plan::spelling_clashrefuses what would silently merge;--strictrefuses partial skips.
File Map
Per-crate entry points. Per-file notes, test counts and a dated line-count snapshot are in
context/file-inventory.md; re-derive sizes with wc -l.
| Area | Where | Entry points |
|---|---|---|
| Core model | crates/improvise-core/src/ |
model/{types,cell,record,category,id}.rs, view/{layout,types,axis}.rs, workbook.rs, format.rs |
| Formula | crates/improvise-formula/src/ |
parser.rs, formula.pest, ast.rs, quote.rs |
| I/O | crates/improvise-io/src/ |
storage.rs, opfs.rs, persistence/{improv.pest,format,parse,export,mod}.rs, import/{wizard,analyzer,csv_parser}.rs |
| Protocol | crates/improvise-protocol/src/ |
lib.rs (ViewState), reduce.rs, render.rs, keymap.rs, face.rs, frame.rs, basis.rs, resolve.rs, client.rs, viewmodel.rs, help.rs |
| Command layer | crates/improvise-command/src/command/ |
cmd/{core,registry,navigation,cell,commit,effect_cmds,fit,grid,mode,panel,search,text_buffer,tile,wizard}.rs, keymap.rs, parse.rs |
| Semantic UI state | crates/improvise-command/src/ui/ |
effect.rs, app.rs, project.rs, reduce.rs, host.rs, viewport.rs, cat_tree.rs |
| TUI + CLI | src/ |
draw.rs, main.rs, ui/{grid,help,import_wizard_ui,text}.rs; integration tests in tests/ |
| GP | crates/{gp-tree,improvise-gp}/ |
gp-tree/src/{lib,data,fit}.rs, improvise-gp/src/{lib,dataset,fit}.rs |
| Socket host | crates/improvise-ws-server/src/ |
registry.rs, origin.rs, transport.rs, main.rs; host.rs is a re-export shim |
| Worker host | crates/improvise-worker-server/src/ |
worker.rs, vfs.rs, bindings.rs (wasm32 shim) |
| Browser client | crates/improvise-wasm-client/src/, web/ |
lib.rs, keys.rs, render.rs, socket.rs; web/{app,shell,worker,serial}.js, index.html, demo.improv, build.sh, assemble.sh |
| Browser harness | harness/ |
Playwright (tests/shell.spec.js, tests/opfs.spec.js), opfs-contract/, build.sh — not a workspace member; its own CI job |
| Examples | examples/, crates/*/examples/ |
gen-grammar.rs, pretty-print.rs, category_width_bench.rs, fit_csv.rs |
| Scripts | scripts/ |
checkers.sh (every *-test.sh suite), check-comment-runs.py, check-ci-graph.py, bd-audit-ids, kb, pr-watch, pr-reviews, build-doc-site.mjs |
| Context docs | context/ |
design-principles.md, gotchas.md, file-inventory.md, code/<area>.md (arguments), test-evidence/ (bug evidence by test name), adr/ (001–004, 007), cloud-agents.md; prolog-sessions/kb.pl is the KB; docs/design-notes.md is product vision |
Wasm
The browser bundle is improvise-{core,formula,io,protocol,command,wasm-client,worker-server}; all seven
cargo check --target wasm32-unknown-unknown clean with no feature flags, and the wasm CI job also gates
improvise-gp (a candidate, not a member). improvise-ws-server and the root crate are native-only.
Check new bundle dependencies with cargo tree --edges normal (the default output includes dev edges),
per crate. cargo check cannot see runtime hazards: Instant::now() panics on wasm (web-time), and
std::fs compiles but fails in a browser. Long form: context/file-inventory.md, appendix.
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>
(flat date split; default is one hierarchical category), --strict (refuse any skipped record), and
--on-duplicate refuse|sum|separate (default refuse). Details: context/code/cli.md, gotchas 51–52.
Testing — the short version
Full guidance: context/design-principles.md §7.
- No single test binary over 2 seconds; the workspace wall clock is the sum. Re-measure with
cargo test --workspace(afinished inper binary) and compare the slowest binary to the next few — a ratio, not seconds, and not a ratio to the median (it is zero). - Commands: build a
CmdContext, callexecute, assert on the returned effects. No terminal. - Property tests (
proptest, default 256 cases) for invariants and round-trips; a grammar-walking strategy must read its pest grammar once (OnceLock). - Bug-fix workflow: write the failing test first. Record the bug in
context/test-evidence/, keyed by test name — not in a doc comment (two-line cap,scripts/check-comment-runs.py). - Coverage target ~80% 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, flate2, csv, enum_dispatch. Dev: proptest, tempfile, pest_meta.