Commit Graph

41 Commits

Author SHA1 Message Date
Edward Langley 0947001b70 Each crate's tests compile alone; resolve.rs folds what it sends through the send door (improvise-a61f, improvise-aopa)
a61f: `cargo test -p improvise-wasm-client --lib` failed with E0277,
because `FormulaEntry: From<&str>` exists only under
improvise-protocol's `test-support` feature, which that crate's
dev-dependencies never asked for. `--workspace` hid it: feature
unification pulled the feature in through improvise-command's dev-dep.
A sweep of every member with `cargo test -p <crate> --no-run` found
the same failure in the root `improvise` crate
(tests/bug_hunt_reproductions.rs). Both now request the feature. The
normal wasm tree still carries no `test-support`.

aopa: eight helpers in resolve.rs's tests folded client-composed
commands through `fold`, the host's door, so a composed Command::Model
was never awaited in `in_flight`. All eight use `fold_outgoing`, one
frame per key, as web/app.js does.
`a_folded_browser_commit_waits_for_the_host` pins press_folded; the
rival that folds through `fold` again fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:35:27 -07:00
Edward Langley 7fb6960cf8 WIP: drop the serde derives only the legacy loader used; docs (improvise-tt22) 2026-09-23 07:50:26 -07:00
Edward Langley 95a35e8cce Every JSON parser reads numbers exactly, and the check derives its crates (improvise-6yvg, improvise-dz8c)
improvise-6yvg reproduced. A JSON import in improvise-command and a legacy
JSON .improv load in improvise-io stored 0.24815714285714285 as
0.24815714285714283 in each crate's own test build. The root binary's
`improvise import` used the same lossy parser. Now float_roundtrip is on every
normal serde_json dependency in the workspace: root, improvise-command,
improvise-io, and the optional test-support dependency of improvise-protocol.

improvise-dz8c: the hand-listed cargo-tree check over three frame-parsing
crates is replaced by tests/json_numbers.rs. It reads
`cargo metadata --no-deps` and names every package, including target-specific
entries, whose normal serde_json dependency lacks the feature. The rule and why
it beats "parses a frame", the build graph, and enabling the feature once in
the protocol crate are in context/code/protocol.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:19:05 -07:00
Edward Langley 487f0b6fef improvise-up8g: a headless run's extent is fixed, not read off the terminal
`run_headless_commands` read `crossterm::terminal::size()`, and the page size
decides where `page-scroll` lands, so one script wrote a different record in a
different window — and CI (no tty, 80x24 default) disagreed with a shell. Pin
it to 80x24 in `HEADLESS_EXTENT`. The test drives two sizes through LINES/
COLUMNS with the child detached by `setsid`, and asserts the same record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:18:25 -07:00
Edward Langley d0f2bf9789 chore: update cargo deps 2026-09-22 20:18:13 -07:00
Edward Langley 585f25bf78 Invalidate the cursor by observing the grid's frame, not by a per-effect table
improvise-ni6k. Twelve effects called `app.reset_cursor()`; that table is
the shape gotcha 24 names as the disease, and it had a hole the day it
was written. The observation replaces it: `view_frames` fingerprints
every view's drawn frame, `apply_effects` diffs it per model effect, and
the cursor resets when the view it is in reframes. No effect declares
anything, and `adopt_cursor_epoch` reads the same set.

FOUR THINGS THE TICKET DID NOT PREDICT, each one a fixture or a design
question rather than a line:

1. The frame fingerprint and `layout_version` answer DIFFERENT questions,
   so they are two observations and not one. `AddCategory` of an empty
   category moves no row or column but does change the coordinates a
   client's CellKey hangs off — layout_version must bump, the cursor need
   not. `view_shape`/`last_reshaped_views` (config) is untouched and still
   drives `layout_version`; `view_frame`/`last_reframed_views` (drawn) is
   new and drives the cursor. Unifying them broke two tests in both
   directions, which is how the distinction was found.

2. The observation must run INSIDE the effect loop, not after it. At
   end-of-batch it clobbered deliberate cursor placement in the same batch
   — `[AddItem(measure), <place cursor>]` sent the cursor home, failing
   three records-mode tests. In-loop, a later effect wins, which is the
   same ordering property gotcha 45 gives `document_config`.

3. `GridLayout::new` does NOT apply `prune_empty`; `with_frozen_records`
   does. A frame hash built on `new` cannot see a pruned row, which is
   exactly the class improvise-7kvw is in.

4. Two per-effect no-op guards were carrying the whole weight and are now
   orphaned: `effective_items` and `item_is_drawn` are deleted, along with
   six inline `moved`/`drawn`/`real`/`existed` predicates.

COST, measured as the ticket required, `benches/reshape_observation.rs`
(new), one SetCell batch, same fixtures as `grid_projection`:

    typical   400 cells   172µs -> 206µs   (+20%)
    large    9.6k cells  3.70ms -> 4.17ms  (+13%)
    18k cells            7.09ms -> 8.41ms  (+19%)
    pathological 90k    35.2ms -> 39.8ms   (+13%)

The batch is dominated by `rebuild_layout`/`recompute_formulas`, not by
the fingerprint.

WHAT CLOSES AS A CONSEQUENCE: improvise-7kvw, asserted by
`paging_across_a_prune_that_drops_a_row_invalidates_the_cursor`.
AddFormula/RemoveFormula join the shared `reshaping_commands` table (a
formula target is a `_Measure` item, so it is a column — gotcha 7), each
proved reshaping by the table's own `assert_ne!` on the grid shape.

WHAT DOES NOT CLOSE, and this is the honest part: `SortData`. A pure
record reorder changes no label and no count, and records-mode `row_items`
are positional, so a row_items fingerprint cannot see it. I wrote the
mechanism (hashing `layout.records`' coords) and then REMOVED it, because
I could not build a fixture where `SortData` actually reorders anything —
records came back in key order whatever order the cells went in. Shipping
a confident comment over a mechanism I could not demonstrate is this
repo's named defect, so it is reported instead. See the hand-back.

Negative arms unchanged and still passing, including the refused-command
arm and `SetNumberFormat`; disabling the observation fails the whole
reshaping table, which is the proof it is what does the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 20:45:34 -07:00
Edward Langley 8a4ee06570 bench: measure grid_of's whole-grid cost, and decline the narrowing it was for
improvise-i5tk's acceptance criterion 1 was a benchmark, on the ground that "this
can dominate TUI rendering" is exactly as unevidenced as "it is fine".
benches/grid_projection.rs is that benchmark, and it says no.

One grid_of call, dense pivot grids, this machine:

  typical                  100 x  4 =   400 cells  whole-grid   1.902ms  ratio 1.2x  col_widths 45%
  large                   1200 x  8 =  9600 cells  whole-grid 185.644ms  ratio 1.3x  col_widths 64%
  18k cells (gotcha 41)   3000 x  6 = 18000 cells  whole-grid 402.435ms  ratio 1.5x  col_widths 42%
  pathological            9000 x 10 = 90000 cells  whole-grid   5.235s   ratio 1.5x  col_widths 44%

ratio is whole-grid over a 45x12 terminal window: 1.2-1.5x, against a fix whose
stated cost is changing what a cache index MEANS in the TUI and putting every
scroll site in scope. Wrong trade, so i5tk is closed on the measurement.

Why the ratio is so low is the bigger finding. Two of the three whole-grid passes
a frame performs are viewport-independent by construction:
ui/viewport.rs::compute_col_widths takes no viewport and walks every (row, col)
twice -- once for cell text, once for the column totals it must also size -- and a
column total must sum every row including scrolled-past ones (gotcha 44).
grid_of's own cell_display loop is the third pass and the only one windowing would
touch. So the whole grid is evaluated about three times per frame, every result
discarded, and at 18,000 cells a keypress costs four tenths of a second. Filed as
improvise-jmjz; its fix is caching or one fused pass, which this benchmark has now
distinguished from the windowing it rules out.

No criterion crate: plain Instant, and test = false so cargo test does not run a
target that takes seconds by design.

Closes improvise-i5tk

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:07:30 -07:00
Edward Langley 7013fbc398 test: the two front ends draw the same grid frame
The check gotcha 44's divergence never had. Each front end is driven the way
it is really driven — the widget into a ratatui buffer, the browser client
through a `Snapshot` of that same cache's `seed()` — and they are compared on
what they *draw*, not on what they were handed; comparing the caches would be
vacuous, since it is one cache.

Injected both faults and watched them fail before keeping it:
`labels: vec![rows.item(r).join("/")]` in the client's `render_model`, and the
same join in the widget's row header. Each names the row and both spellings.

`improvise-wasm-client` becomes a dev-dependency of the root crate for this.
It builds natively — that is how its own tests already run — and being a dev
dependency the terminal binary does not grow a wasm client.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 22:43:52 -07:00
Edward Langley bdce1636bb feat(worker-server): the whole App in a Web Worker, over postMessage + OPFS
improvise-9x6: the server half of the standalone static-web deployment
(epic improvise-tm6). `improvise-worker-server` hosts the authoritative
`App`, the command pipeline, the projection layer and VFS persistence behind
`self.onmessage`/`self.postMessage`.

It is small because everything it needs already existed: the frames are
`improvise_protocol::frame`'s (improvise-xks6), the session loop is
`Host` (previous commit), the basis check is `Sessions::apply`/`Checked`
(improvise-ondj), and persistence is `Storage` + `OpfsFs` (improvise-6mq,
i34). What is genuinely new is three things.

`vfs.rs` — how a document gets INTO a browser's empty filesystem. Loading is
unchanged: it is `persistence::load_from` on a `Storage`, the same call the
native binary makes. Seeding is the new problem, and it is a
`Storage::write`, so a fetched demo, an upload and a saved document take the
same parse path and a seed never clobbers an edit. `list` is the document
picker a browser has no OS file manager for.

`worker.rs` — the loop: one port, one client, the `Viewport` handshake
(dropped rather than closed, since there is no connection to close), and the
filter from `Addressed`. Every decision is here and every test points at it.

`bindings.rs` — `cfg(wasm32)`, decision-free, and the only part no test here
can reach: it awaits `OpfsFs::open`/`attach`/`refresh`/`commit_deletes` and
forwards strings.

Two gaps found and reported rather than papered over. No client on any
transport can ask its host to save — `effect::Save` is `ViewOnly`, so the
wire vocabulary derived from the model effects cannot name it — and a wire
`ModelCommand::SetCell` never sets `model_state.dirty`, because dirtiness is
raised by a separate `MarkDirty` effect the key path emits. So the worker
persists on its own behalf, keyed off the command variant rather than off a
flag that is never set; `touches_document` carries the argument.

`ClientFrame::Key` is still answered with nothing. A worker CAN call
`reduce_key` — it holds the `App` — but nothing in the projection stream
carries the host's own `ViewState`, so a half-wired arm diverges rather than
under-delivers. `a_key_resolved_on_the_host_moves_state_no_client_can_hear`
demonstrates it against a real `App` and `Sessions` (improvise-bpq2).

22 tests, all host-side: 16 in the crate and 6 in `tests/post_message.rs`,
which drives the worker with nothing but the JSON a `MessagePort` carries.
Bundle: 1,334,892 bytes raw, 496,965 gzipped — 485 KB against a 3 MB budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 19:08:19 -07:00
Edward Langley 4b643a6940 Merge branch 'review' 2026-09-19 17:52:00 -07:00
Edward Langley 8e40757065 chore: upgrade deps 2026-09-19 17:50:53 -07:00
Edward Langley 114680744e Merge: improvise-ws-server — the websocket host (improvise-q08)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH

# Conflicts:
#	Cargo.lock
#	Cargo.toml
2026-09-19 16:45:25 -07:00
Edward Langley df746173bf feat(ws-server): websocket host for one document (improvise-q08)
A new crate `improvise-ws-server`: a testable session loop plus a thin
tokio/tungstenite shell around it.

The ticket predates the session machinery and describes building it by
hand; most of what it asks for now exists, so what this adds is the
transport and the loop (doctrine(check_the_ticket_premise)):

- `Host` is one authoritative `App`, one `Sessions`, and every client
  attached to them — the unit of authority is the *document*, not the
  connection (the ticket's premise) and not the server. A multi-document
  registry is a map of `Host`s and is deliberately not built yet; the
  seam is in `transport`, where `serve` takes the document it hosts.
- `handle(id, msg) -> Vec<Addressed>` makes every decision and is tested
  without a socket. `Applied::Rebased` sends every client a fresh
  `Snapshot`; `Applied::Diff.broadcast` is forwarded to the *other*
  clients and never back to the asker.
- The wire adds exactly one thing to the shared vocabulary: the extent a
  client can display, which no `Command` carries by construction and
  which doubles as the handshake, since the bootstrap `Snapshot` is
  built at it.

No auth, no TLS, loopback only: a stated MVP limit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 16:44:32 -07:00
Edward Langley 30c4bb6629 feat(wasm-client): the browser-side fold and key classifier (improvise-gsw)
A new crate `improvise-wasm-client`: a `ViewState` plus one widget cache
each, moved by folding the same `Command` stream the host folds, with a
wasm-bindgen surface over JSON.

The ticket's premise does not survive contact with the architecture
(`doctrine(check_the_ticket_premise)`). It asks `on_key_event` to
"resolve via the keymap and dispatch through reduce_view", but a binding
names an *intent* resolved against a `CmdContext` — a `Model`, a
`GridLayout`, an `App` — while a `Command` is the *resolved mutation*
(gotcha 38, improvise-gxi). A thin client holds none of that, so keys
travel upstream unresolved and `improvise-command` is a dev-dependency
rather than a dependency: the keymap could not be used here even if the
bundle could afford `App`, `Workbook`, persistence and the wizard.

The one exception is minibuffer text entry, whose target buffer is
`AppMode::minibuffer().buffer_key` — the same field the host's
`TextEntry` builder reads (gotcha 11), so the local rule is derived from
the same fact rather than copied. `tests/lockstep.rs` drives the
authoritative host and the client over the same keypress in all seven
text-entry modes and asserts the two `ViewState`s agree.

Rejected for local resolution, with reasons in `keys.rs`: cursor
movement (`GridCache` is viewport-indexed, so a client cannot clamp),
panel cursors (the flattening depends on `expanded_cats`), anything
modified (`AnyChar` matches under any modifier on the host), and
CommandMode's Backspace (the one `TextEntry::rebind`, which leaves the
mode).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 16:35:04 -07:00
Edward Langley 55ee619ae6 Merge: gp-tree formula emission, library preprocessing, seeded RNG (improvise-izk, 0yr, lzn)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 13:38:02 -07:00
Edward Langley 2a0b937447 feat(improvise-gp): emit a fitted gp-tree Tree as improvise formula text
improvise-izk. New crate `improvise-gp` — the seam between gp-tree and the
formula language. It cannot live in gp-tree (that inverts the dependency) and
it cannot live in improvise-formula (which must not know GP exists).

`formula_text(tree, spec, target)` turns Split{feat,thresh,lo,hi} into
`IF(<name> < <thresh>, <lo>, <hi>)` and a Leaf into its number;
`fitted_formula` hands back the parsed Formula, so the parser is the emitter's
own contract check.

Two things the emitter owes its caller:

* Names round-trip. Feature names come from user data — spaces, operators,
  pipes, keyword collisions, the one-hot encoder's own `col=value` labels —
  so every name goes through CL-style pipe quoting, and a keyword is quoted
  even though it would parse bare.
* Thresholds route identically. A threshold is emitted as the f32's *exact*
  decimal, not its shortest round-trip form. `0.7f32` is 0.699999988…, and
  "0.7" parses as an f64 strictly greater than it, so a row whose feature
  value equals the threshold would fall on the other side of the split in
  formula evaluation than it does in `gp_tree::eval`. There is a test for
  that boundary, and it fails against the naive rendering.

Tests: parse round-trip for leaf/nested/awkward-name/awkward-target cases, the
threshold-boundary regression, and the acceptance property — over random trees
and random feature vectors, `Model::evaluate` of the emitted formula equals
`gp_tree::eval`, exactly (f32→f64 is lossless, so no tolerance is needed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 13:34:01 -07:00
Edward Langley 7c5a3f6332 chore: drop unused root deps (pest/pest_derive/dirs), ignore tags+graph artifacts
improvise-v9w: the root `improvise` crate's [dependencies] listed pest,
pest_derive and dirs, none of which appear anywhere under src/. pest and
pest_derive remain genuine dependencies of improvise-formula and
improvise-io (persistence/improv.pest); examples/gen-grammar.rs reaches
pest_meta through [dev-dependencies], which examples do get, so the
examples still build. Verified with cargo build --workspace, cargo build
--examples, cargo test --workspace and the wasm check. Cargo.lock loses
only dirs and its transitive deps.

improvise-wg1: add the GNU Global tag files (GTAGS/GRTAGS/GPATH/GSYMS) and
the root graphviz artifacts (/graph.dot, /graph.html, /graph.svg) to
.gitignore. Named exactly rather than globbed so no checked-in asset is
swallowed; `git ls-files | git check-ignore --stdin` prints nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 13:31:38 -07:00
Edward Langley d25f3187d3 feat(protocol): extract improvise-protocol crate
The crate the browser epic hangs off: Command (the wire format),
ViewState, RenderCache/CellDisplay/Snapshot, and reduce_view. It depends
on improvise-core and serde and nothing else -- no IO, no command layer,
no terminal -- and improvise-command now depends on it.

ViewState, ViewCursor, ViewFrame, AppMode, Panel, MinibufferConfig,
MinibufColor, HELP_PAGE_COUNT and the key types move here. Everything
that moved is re-exported from ui::app and command::key, so no existing
path changed. The three tickets before this one are what made the move
mechanical: h0b evicted the unportable fields, a2x added the derives,
8uz took the cursor out of the shared View.

Command's client->server half is not invented. It is exactly the set of
EffectKind::ViewOnly effects whose target is the portable slice -- h0b
had already sorted the portable state from the host-local, so the
enumeration was done. Effects touching the wizard, staged drill edits,
storage or the layout cache are absent by construction. The other half
carries projections: already-formatted display values and their labels.

One thing had to be added rather than moved. reduce_view runs on a client
that holds no Workbook, and ViewState::cursors is keyed by view name, so
ViewState needs its own active_view. That makes two copies of "which view
is active"; App::switch_to_view and App::install_workbook are the only
writers, and the_two_active_view_names_never_diverge guards it. The 8uz
characterization test caught the desync the moment it appeared, because
it was calling workbook.switch_view directly.

Whether the two are *allowed* to diverge -- two clients on different
views -- is improvise-g0f, deliberately left open. It changes the shape
of improvise-cqq, and nothing here forecloses either answer.

reduce_view has 12 tests including a determinism check: folding the same
stream twice gives the same state, which is what stops the server's
mirror drifting from the client it mirrors. Every Command round-trips
through JSON. The crate is in the CI wasm job.

Closes improvise-cqi

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V69PGQfMJuGRmtSWpfVAqn
2026-09-01 02:03:45 -07:00
Edward Langley 7afe6ef430 chore: update versions in cargo 2026-08-13 17:36:16 -07:00
Edward Langley 0f03982be8 chore: arrange deps 2026-08-01 16:23:15 -07:00
Edward Langley 43404173b7 feat: add gp-tree 2026-08-01 10:12:26 -07:00
Edward Langley a85e6ca28c refactor(workspace): extract improvise-command crate (improvise-3mm)
Command layer + semantic App state move below the TUI: command/ (cmd,
keymap, parse, key), ui/{app,effect,cat_tree}.rs, and the pure viewport
helpers now live in crates/improvise-command, which depends on core +
formula + io and provably not on ratatui/crossterm (cargo tree clean).
The root crate remains the TUI binary and re-exports improvise_command
under the old crate::command / crate::ui::{app,effect,cat_tree} paths,
so widgets, draw.rs, and main.rs are unchanged. App moves whole (per
the improvise-ltq design note) rather than splitting an AppState out —
its fields are all command-safe after the key/color decoupling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:11:43 -07:00
Edward Langley bd17aed169 refactor(io): scaffold empty improvise-io sub-crate
Lay groundwork for Phase 3 of the workspace split (improvise-8zh): a new
`crates/improvise-io/` sub-crate that will house the persistence and
import layers once the files move in the next commit.

Only scaffolding here:
- New `crates/improvise-io/Cargo.toml` declares deps on improvise-core,
  improvise-formula, and the external crates persistence+import already
  use: anyhow, chrono, csv, flate2, indexmap, pest, pest_derive, serde,
  serde_json. Dev-deps: pest_meta, proptest, tempfile.
- New `crates/improvise-io/src/lib.rs` re-exports the core modules under
  their conventional names (`format`, `model`, `view`, `workbook`,
  `formula`) so in the next commit the moved code's `crate::model::*`,
  `crate::view::*`, `crate::workbook::*`, `crate::format::*`, and
  `crate::formula::*` paths resolve unchanged.
- Root `Cargo.toml` adds the new crate to workspace members and the main
  crate's `[dependencies]`, ready to receive the move.

No source files change yet; cargo check --workspace still compiles as
before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:05:17 -07:00
Edward Langley 0d75c7bd0b refactor(core): scaffold empty improvise-core sub-crate
Lay the groundwork for Phase B of improvise-36h: a new
`crates/improvise-core/` sub-crate that will house the pure-data core
(model, view, workbook, format) once the files move in the next commit.

Only scaffolding here:
- New `crates/improvise-core/Cargo.toml` mirroring improvise-formula's
  structure; declares deps on improvise-formula, anyhow, indexmap, serde.
- New `crates/improvise-core/src/lib.rs` with the single re-export
  `pub use improvise_formula as formula;` so that in the next commit the
  moved code's `crate::formula::*` paths resolve unchanged.
- Root `Cargo.toml` adds the new crate to the workspace members and the
  main crate's `[dependencies]`, ready to receive the move.

No source files change yet; cargo check --workspace still compiles as
before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:21:58 -07:00
Edward Langley f02d905aac refactor(formula): extract formula parser into separate crate
Extract the formula AST and parser into a dedicated `improvise-formula`
crate and convert the project into a Cargo workspace.

The root crate now re-exports `improvise-formula` as `crate::formula` to
maintain backward compatibility for internal callers. The repository map is
updated to reflect the new crate structure.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-31B-it-UD-Q4_K_XL.gguf)
2026-04-14 01:49:35 -07:00
Edward Langley 04058a48f9 chore: v0.1.0-rc2
Release / plan (push) Has been cancelled
Release / build-local-artifacts (${{ join(matrix.targets, ', ') }}) (push) Has been cancelled
Release / build-global-artifacts (push) Has been cancelled
Release / host (push) Has been cancelled
Release / announce (push) Has been cancelled
2026-04-11 00:47:36 -07:00
Edward Langley 5e90fbe0f7 chore(updates): last cargo updates 2026-04-11 00:45:41 -07:00
Edward Langley 1350e875c5 chore(update): dep updates for cargo 2026-04-11 00:42:20 -07:00
Edward Langley d4f6efd1c0 fix(upgrade): errors from Rust 2024 edition
- gen is a reserved keyword (→ renamed module to generator)
- ref in patterns with implicit borrowing (→ removed ref)
- & pattern with implicit borrowing (→ added & to outer pattern)
2026-04-11 00:38:30 -07:00
Edward Langley ee6739158e chore: update cargo files more
Release / plan (push) Has been cancelled
Release / build-local-artifacts (${{ join(matrix.targets, ', ') }}) (push) Has been cancelled
Release / build-global-artifacts (push) Has been cancelled
Release / host (push) Has been cancelled
Release / announce (push) Has been cancelled
2026-04-11 00:31:43 -07:00
Edward Langley 537819577a chore: update version 2026-04-11 00:28:00 -07:00
Edward Langley b2d30d5a20 feat: cargo dist info 2026-04-11 00:15:42 -07:00
Edward Langley e6c93a24d9 chore: some dep updates 2026-04-11 00:07:59 -07:00
Edward Langley 861b142dec chore: change license to Apache-2.0
Update both README and Cargo.toml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:07:57 -07:00
Edward Langley 35ed6a13bf fix: add missing Cargo.toml package metadata
Add repository, homepage, readme, keywords, and categories fields.
Update description to match project vision. Use GitHub URLs for
public discoverability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:06:51 -07:00
Edward Langley cb24cce1f0 feat: use enum_dispatch for command dispatch and add Open command
Replace manual dynamic dispatch using Box <dyn Runnable> with enum_dispatch
for improved performance and cleaner code.

Add a new Open command to allow opening the TUI as the default behavior
when no subcommand is provided.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
2026-04-11 00:06:48 -07:00
Edward Langley ab5f3a5a86 feat(build): add profiling profile configuration
Add a new [profile.profiling] section to Cargo.toml.

This profile inherits from release but with:
- strip = false: Keep debug symbols for profiling
- debug = 2: Full debug information for analysis

Useful for generating profiling data with symbol information.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
2026-04-05 01:09:17 -07:00
Edward Langley 6647be30fa refactor: switch to clap with subcommands for CLI parsing
Replace hand-rolled arg parser with clap derive. Restructure as
subcommands: import, cmd, script. Import subcommand supports
--category, --measure, --time, --skip, --extract, --axis, --formula,
--name, --no-wizard, and --output flags for configurable imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:43:16 -07:00
Edward Langley 23e26f0e06 Add CSV import functionality
- Use csv crate for robust CSV parsing (handles quoted fields, empty values, \r\n)
- Extend --import command to auto-detect format by file extension (.csv or .json)
- Reuse existing ImportPipeline and analyzer for field type detection
- Categories detected automatically (string fields), measures for numeric fields
- Updated help text and welcome screen to mention CSV support

All 201 tests pass.
2026-04-01 01:37:04 -07:00
Ed L 09caf815d3 test: add proptest property-based tests
Add proptest dependency and property tests for:
- CellKey: key normalization invariants (sort order, dedup, round-trip,
  prefix non-equality, merge commutativity)
- View: axis exclusivity, set_axis, idempotency, page_selection roundtrip,
  hide/show roundtrip, toggle_group_collapse involution

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 00:10:44 -07:00
Ed L eae00522e2 Initial implementation of Improvise TUI
Multi-dimensional data modeling terminal application with:
- Core data model: categories, items, groups, sparse cell store
- Formula system: recursive-descent parser, named formulas, WHERE clauses
- View system: Row/Column/Page axes, tile-based pivot, page slicing
- JSON import wizard (interactive TUI + headless auto-mode)
- Command layer: all mutations via typed Command enum for headless replay
- TUI: Ratatui grid, tile bar, formula/category/view panels, help overlay
- Persistence: .improv (JSON), .improv.gz (gzip), CSV export, autosave
- Static binary via x86_64-unknown-linux-musl + nix flake devShell
- Headless mode: --cmd '{"op":"..."}' and --script file.jsonl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:11:55 -07:00