Commit Graph

71 Commits

Author SHA1 Message Date
ef79a39721 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:32:19 -07:00
9fc3f0b5d6 refactor: synthesize previous refactors 2026-04-01 01:01:19 -07:00
3f84ba03cb Revert "refactor: mystery model 3"
This reverts commit 4b721f7543.
2026-04-01 00:46:55 -07:00
4b721f7543 refactor: mystery model 3 2026-04-01 00:46:25 -07:00
6d88de3020 Revert "refactor: mystery model #2"
This reverts commit 87fd6a1620.
2026-04-01 00:41:25 -07:00
87fd6a1620 refactor: mystery model #2 2026-04-01 00:40:22 -07:00
a57d3ed294 Revert "refactor: mystery model #1"
This reverts commit bbebc3344c.
2026-04-01 00:32:12 -07:00
bbebc3344c refactor: mystery model #1 2026-04-01 00:32:07 -07:00
ff08e3c2c2 chore: Revert refactors to give claude a clean slate 2026-04-01 00:26:55 -07:00
8c84256ebc refactor: merge using claude sonnet 2026-04-01 00:25:19 -07:00
d915908354 refactor: unsloth/Qwen3-Coder-Next-GGUF:Q5_K_M refactors the drawing helper 2026-04-01 00:20:19 -07:00
7731c7ceab Revert "refactor: unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M"
This reverts commit 98d151f345.
2026-03-31 23:11:21 -07:00
98d151f345 refactor: unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M 2026-03-31 23:10:52 -07:00
f1e6e61bca Revert "test: use gpt-oss-20b to do some minor refactoring"
This reverts commit bbd1f48b78.
2026-03-31 22:50:10 -07:00
bbd1f48b78 test: use gpt-oss-20b to do some minor refactoring 2026-03-31 22:50:07 -07:00
bbfd2dc163 refactor: TuiGuard -> TuiContext 2026-03-31 22:05:02 -07:00
85eabebd88 refactor: extract terminal setup/teardown 2026-03-31 21:27:40 -07:00
22ea265b63 refactor: inline print_usage 2026-03-31 20:59:39 -07:00
dda5571faf refactor: inline run_headless 2026-03-31 20:58:43 -07:00
c32e800128 refactor: split main code paths for clarity. 2026-03-31 20:52:28 -07:00
aae8392f46 refactor: continue simplifying entrypoint 2026-03-31 20:15:20 -07:00
6eee161f02 chore: rm vim swap file 2026-03-31 00:08:28 -07:00
183b2350f7 chore: reformat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 00:07:22 -07:00
37584670eb feat: group-aware grid rendering and hide/show item
Builds out two half-finished view features:

Group collapse:
- AxisEntry enum distinguishes GroupHeader from DataItem on grid axes
- expand_category() emits group headers and filters collapsed items
- Grid renders inline group header rows with ▼/▶ indicator
- `z` keybinding toggles collapse of nearest group above cursor

Hide/show item:
- Restore show_item() (was commented out alongside hide_item)
- Add HideItem / ShowItem commands and dispatch
- `H` keybinding hides the current row item
- `:show-item <cat> <item>` command to restore hidden items
- Restore silenced test assertions for hide/show round-trip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 00:07:11 -07:00
3cf64b40a3 chore: don't pin rust overlay nixpkgs to normal nixpkgs 2026-03-31 00:04:36 -07:00
866a6ff589 chore: more cleaning 2026-03-30 23:55:08 -07:00
af0b2c6fdd refactor: more entrypoint simplifications
initial model calculated in its own function
2026-03-30 23:37:04 -07:00
3e3ac05b05 refactor: cleanup entry point 2026-03-30 23:30:57 -07:00
4fb97c89ed fix(tests): restore coverage for toggle_group_collapse, item group, and insertion order
Rewrites three commented-out tests to access public fields directly
instead of the removed item_by_name/item_index/is_group_collapsed methods:
- add_item_in_group_sets_group: uses items.get()
- item_index_reflects_insertion_order: uses items.get_index_of()
- toggle_group_collapse_toggles_twice + involutive proptest: inspect collapsed_groups directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 23:08:06 -07:00
c5eab1f283 chore: update gitignore 2026-03-30 23:07:42 -07:00
226029bc68 fix(tests): restore persistence round-trip assertions using items.get()
The two tests were previously silenced when item_by_name was removed.
Rewrites them using category.items.get() directly, restoring coverage
of item-name and item-group serialization round-trips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 23:02:48 -07:00
a1b17dc9af refactor: remove dead code, replace sum_matching tests with evaluate()
Removes unused methods (sum_matching, get_mut, item_by_name, item_index,
top_level_groups, is_group_collapsed, show_item) and unused constants
(LABEL_THRESHOLD, MIN_COL_WIDTH).

The sum_matching tests in model.rs were bypassing the formula evaluator
entirely. Replaced them with equivalent tests that call evaluate() against
the existing Total = SUM(Revenue) formula, exercising the real aggregation
code path.

Also fixes a compile error in view.rs prop_tests where View/Axis imports
and a doc comment were incorrectly commented out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 22:56:04 -07:00
15f7fbe799 chore: add rust-analyzer to nix dev shell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 22:55:44 -07:00
f58ca625cc chore: misc nix/direnv changes 2026-03-30 22:29:12 -07:00
856b5d5d41 chore: reformat 2026-03-30 22:28:45 -07:00
f19f503ab1 chore: update flake lock 2026-03-30 22:27:24 -07:00
e3078ba61d fix: make nix flake and cargo config portable across Linux and macOS
musl tooling is Linux-only, so guard it behind an isLinux check in
flake.nix and remove the hardcoded musl build target from .cargo/config.toml
(the nix devShell already sets CARGO_BUILD_TARGET on Linux).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 22:22:11 -07:00
a655078997 fix: --import now opens the wizard instead of doing a headless auto-import
Previously --import ran ImportJson headless before the TUI started,
hitting the category limit and printing the error to stderr where it
was invisible. Now it parses the JSON and opens the ImportWizard on
startup, matching :import behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 15:18:37 -07:00
acc890764b refactor: execute_command owns its own mode transitions
Removed the post-execution mode reset from the caller. execute_command
now sets mode = Normal at the top as the default; commands that open
a new mode (ImportWizard, Quit) simply override it. The caller no
longer needs a special-case exclusion list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 12:09:10 -07:00
f4978a9fd4 chore: reformat spec a bit 2026-03-24 12:09:09 -07:00
c1f4ebf5fc fix: :import command now opens the wizard instead of silently closing
execute_command set mode to ImportWizard, but the caller immediately
reset it to Normal for any non-Quit mode. Added ImportWizard to the
exclusion list so the wizard survives the reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:40:55 -07:00
1345142fe0 fix: make .improv parser order-independent via two-pass approach
Root cause: set_axis silently ignores unregistered categories, so a
view section appearing before its categories would produce wrong axis
assignments when on_category_added later ran and assigned defaults.

Fix: collect all raw data in pass 1, then build the model in the
correct dependency order in pass 2 (categories → views → data/formulas).
The file can now list sections in any order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:37:40 -07:00
b13ab13738 chore: do not use JSON for serialization 2026-03-24 10:52:31 -07:00
41f138d5c3 feat: replace JSON with human-readable markdown .improv format
New format is diffable plain text with categories, items, formulas,
data cells, and views all readable without tooling. Legacy JSON files
(detected by leading '{') still load correctly for backwards compat.

Format overview:
  # Model Name
  ## Category: Type
  - Food
  - Gas [Essentials]
  ## Data
  Month=Jan, Type=Food = 100
  ## View: Default (active)
  Type: row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 10:46:57 -07:00
cb43a8130e feat: add 't' key to transpose row/column axes
Pressing 't' swaps all Row-axis categories to Column and all
Column-axis categories to Row, leaving Page categories unchanged.
Selection and scroll offsets are reset to (0,0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 09:40:02 -07:00
c42553fa97 feat: 2D multi-level grid headers with repeat suppression
Column headers now render one row per column category instead of
joining with '/'. Row headers render one sub-column per row category.
Repeat suppression hides labels when the prefix is unchanged from
the previous row/column.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 09:32:01 -07:00
c8b88c63b3 refactor: eliminate Box<dyn Iterator> and Option sentinels in export_csv
The CSV export used Box<dyn Iterator<Item = Option<usize>>> to unify
empty and non-empty row iteration, violating the CLAUDE.md rule that
Box/Rc container management should be split from logic. Replaced with a
direct for loop over row indices, removing both the Box and the Option
sentinels used to represent "placeholder empty row/col".

Also removes unused pub use cell::CellKey re-export and an unused
import in cell.rs tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 09:00:37 -07:00
6038cb2d81 refactor: make active_view and axis_of infallible
Both functions previously returned Option despite their invariants
guaranteeing a value: active_view always names an existing view
(maintained by new/switch_view/delete_view), and axis_of only returns
None for categories never registered with the view (a programming error).

Callers no longer need to handle the impossible None case, eliminating
~15 match/if-let Option guards across app.rs, dispatch.rs, grid.rs,
tile_bar.rs, and category_panel.rs.

Also adds Model::evaluate_f64 (returns 0.0 for empty cells) and collapses
the double match-on-axis pattern in tile_bar/category_panel into a single
axis_display(Axis) helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 09:00:25 -07:00
a2e519efcc refactor: replace CellValue::Empty with Option<CellValue>
Previously CellValue had three variants: Number, Text, and Empty.
The Empty variant acted as a null sentinel, but the compiler could not
distinguish between "this is a real value" and "this might be empty".
Code that received a CellValue could use it without checking for Empty,
because there was no type-level enforcement.

Now CellValue has only Number and Text. The absence of a value is
represented as None at every API boundary:

  DataStore::get()    → Option<&CellValue>  (was &CellValue / Empty)
  Model::get_cell()   → Option<&CellValue>  (was &CellValue / Empty)
  Model::evaluate()   → Option<CellValue>   (was CellValue::Empty)
  eval_formula()      → Option<CellValue>   (was CellValue::Empty)

Model gains clear_cell() for explicit key removal; ClearCell dispatch
calls it instead of set_cell(key, CellValue::Empty).

The compiler now forces every caller of evaluate/get_cell to handle
the None case explicitly — accidental use of an empty value as if it
were real is caught at compile time rather than silently computing
wrong results.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 08:06:51 -07:00
e680b098ec refactor: remove Axis::Unassigned; axis_of returns Option<Axis>
Axis::Unassigned served two purposes that Option already covers:
  1. "this category has no assignment yet" → None
  2. "this category doesn't exist" → None

By removing the variant and changing axis_of to return Option<Axis>,
callers are forced by the compiler to handle the absent-category case
explicitly (via match or unwrap_or), rather than silently treating it
like a real axis value.

SetAxis { axis: String } also upgraded to SetAxis { axis: Axis }.
Previously, constructing SetAxis with an invalid string (e.g. "diagonal")
would compile and then silently fail at dispatch. Now the type only admits
valid axis values; the dispatch string-parser is gone.

Axis gains #[serde(rename_all = "lowercase")] so existing JSON command
files (smoke.jsonl, etc.) using "row"/"column"/"page" continue to work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 07:32:48 -07:00