#+TITLE: Improvise Roadmap #+AUTHOR: Edward Langley #+TODO: TODO DOING WAIT | DONE #+STARTUP: overview #+TAGS: epic standalone P0 P1 P2 P3 P4 task feature bug #+PROPERTY: COOKIE_DATA todo recursive Generated from ~bd list --json~. 50 open issues organised as 27 inverted dep-trees: each root is a goal that nothing else depends on; its children are the deps blocking it. Diamonds in the DAG are duplicated so each tree stands alone. * TODO Malformed numbers in .improv file silently become 0.0 (standalone) :standalone:P1:bug: :PROPERTIES: :ID: improvise-4yc :TYPE: bug :PRIORITY: P1 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Ed L :CREATED: 2026-04-16 18:18:17 :UPDATED: 2026-04-16 18:18:17 :KIND: standalone :END: ** Details *** Description crates/improvise-io/src/persistence/mod.rs:434 uses value_pair.as_str().parse().unwrap_or(0.0). Malformed number literals (e.g. '3.14.15', '1e999', '--5') become CellValue::Number(0.0) with no error. This is the only production unwrap/unwrap_or in the parse path and silently corrupts data. Fix: return a parse error with line context, or at minimum log a warning and use CellValue::Error. * TODO Pest parse errors lack file:line context for users (standalone) :standalone:P1:bug: :PROPERTIES: :ID: improvise-6kj :TYPE: bug :PRIORITY: P1 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Ed L :CREATED: 2026-04-16 18:18:13 :UPDATED: 2026-04-16 18:18:13 :KIND: standalone :END: ** Details *** Description persistence/mod.rs:294 wraps pest errors as 'Parse error: {e}' which shows Pair offsets like 'Pair(6..12), expected "=", found ","' instead of a line preview. Users debugging a malformed .improv file have no way to find the offending line. Fix: extract (line, col) from pest::error::Error via line_col(), include the line contents and surrounding context in the wrapped error. * TODO CyclePanelFocus does not actually cycle between panels (standalone) :standalone:P1:bug: :PROPERTIES: :ID: improvise-dqn :TYPE: bug :PRIORITY: P1 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Ed L :CREATED: 2026-04-16 18:18:07 :UPDATED: 2026-04-16 18:18:07 :KIND: standalone :END: ** Details *** Description The CyclePanelFocus command at src/command/cmd/panel.rs:299-313 is documented as 'Tab through open panels' but the if/else if chain always resolves to the first open panel (formula > category > view). Pressing the bound key repeatedly never rotates. Test at panel.rs:84 only asserts a panel mode is entered, not rotation. Fix: either implement real rotation using ctx.mode to determine 'next' open panel, or rename to FocusFirstOpenPanel. Also replace the three bool fields with an enum of open panel states. * TODO CSV import silently drops columns when row widths mismatch (standalone) :standalone:P1:bug: :PROPERTIES: :ID: improvise-k8i :TYPE: bug :PRIORITY: P1 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Ed L :CREATED: 2026-04-16 18:18:15 :UPDATED: 2026-04-16 18:18:15 :KIND: standalone :END: ** Details *** Description crates/improvise-io/src/import/csv_parser.rs:14-20 uses ReaderBuilder with .flexible(true). Rows with fewer columns than the header are truncated with no warning. User data is silently lost on import. Fix: either enforce .flexible(false) for strict RFC 4180 parsing, or detect mismatched row widths and surface a warning to the import wizard. * TODO Inconsistent float-equality tolerance in formula eval (standalone) :standalone:P2:bug: :PROPERTIES: :ID: improvise-0bf :TYPE: bug :PRIORITY: P2 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Ed L :CREATED: 2026-04-16 18:18:35 :UPDATED: 2026-04-16 18:18:35 :KIND: standalone :END: ** Details *** Description crates/improvise-core/src/model/types.rs uses different float-equality strategies: division checks 'rv == 0.0' exactly (:674), while = and != comparisons use '1e-10' epsilon (:555, :749). A formula like IF(x = 0, ..., y/x) with x = 1e-11 treats x as zero in the condition but divides without error. Either unify on a single FLOAT_EQ_EPSILON constant or document the design choice explicitly in comments. * TODO Migrate name-string comparisons to CategoryKind enum checks (standalone) :standalone:P2:task: :PROPERTIES: :ID: improvise-2lh :TYPE: task :PRIORITY: P2 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Ed L :CREATED: 2026-04-16 19:01:53 :UPDATED: 2026-04-16 19:01:53 :KIND: standalone :END: ** Details *** Description 8 sites compare category names as strings against virtual-category prefixes ('_Measure', '_Index', '_Dim') instead of checking the typed CategoryKind enum. Adding a new CategoryKind variant won't produce a compile error at any of these sites.\n\nSites:\n- crates/improvise-core/src/model/types.rs:215, 226, 429, 636 (target_category == '_Measure', cat_name == '_Measure')\n- crates/improvise-io/src/persistence/mod.rs:213, 226 (formula target comparison)\n- crates/improvise-core/src/view/layout.rs:108 (c == '_Index' && c == '_Dim')\n- src/command/cmd/grid.rs:306 (target_category == '_Measure')\n\nFix: add methods on CategoryKind: is_virtual(), is_measure(), is_persisted(), is_index_or_dim(). Look up the Category by name once, dispatch on its .kind.\n\nCorrectness win: the compiler flags every site when a new variant is added. Also removes the hardcoded-name dependency — if the underscore prefix convention ever changes, one place to edit. * TODO Extract panel mode keymap template (standalone) :standalone:P2:task: :PROPERTIES: :ID: improvise-4pg :TYPE: task :PRIORITY: P2 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Ed L :CREATED: 2026-04-16 18:53:24 :UPDATED: 2026-04-16 18:53:24 :KIND: standalone :END: ** Details *** Description command/keymap.rs:554-713 has three nearly-identical panel keymaps (FormulaPanel, CategoryPanel, ViewPanel) sharing Esc/Tab/j/k/F/C/V/: bindings. Only panel name varies.\n\nAbstraction: fn panel_keymap(panel: Panel) -> Keymap\n\nCorrectness win: adding a 4th panel becomes a one-line call; prevents per-panel binding drift. Also helps keep CyclePanelFocus keybinding consistent once improvise-dqn is fixed.\n\n~80 LOC collapsed. * TODO Virtual views _Records and _Drill should not be persisted to .improv files (standalone) :standalone:P2:task: :PROPERTIES: :ID: improvise-60z :TYPE: task :PRIORITY: P2 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: Edward Langley :CREATED: 2026-04-15 11:13:29 :UPDATED: 2026-04-15 11:13:29 :KIND: standalone :END: * TODO Epic: Browser frontend via synchronized Redux-style stores (epic) [/] :epic:P2:feature: :PROPERTIES: :ID: improvise-6jk :TYPE: feature :PRIORITY: P2 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: spot :CREATED: 2026-04-14 07:21:21 :UPDATED: 2026-04-14 07:21:21 :KIND: epic :END: ** Details *** Description Build a browser frontend for improvise by treating the client as a thin Redux-style peer of the existing server. Commands are the wire format; each side runs a reducer over its own state slice. Server is structurally unchanged (still runs today's full App with ratatui session, effect pipeline, formula eval). Client is a new thin peer: ViewState + render cache + reduce_view + keymap + DOM renderer. Commands flow both directions — upstream (user intent resolved client-side) and downstream (projection commands emitted by the server after model-touching effects). See child issues for sequenced steps. *** Design Client holds ViewState (mode, cursor, scroll, minibuffer, search, yanked, expanded cats, drill buffer, panel cursors) plus a render cache (visible cells, labels, col widths). Server holds full App as today. Wire type: Command enum, serde over websocket. Upstream: user-initiated commands resolved by client-side keymap. Downstream: projection commands (CellsUpdated, ColumnLabelsChanged, etc.) emitted by server after effect application. Client reduce_view pattern-matches commands and applies view-slice effects; client never runs formula eval or touches Model. Server emits projections by walking the effect list after each command and computing per-viewport deltas per connected client. Effect split (model vs view) is a server-internal tag driving projection emission — never crosses the wire. Key→command resolution is client-side (keymap in wasm bundle) for local responsiveness. *** Notes Prereqs from crate-split epic (improvise-xgl): step 2 (improvise-core, improvise-36h) for shared types; step 5 (improvise-command split, improvise-3mm) so wasm client can import keymap + Command + reduce_view without ratatui. Step 4 (Effect enum, improvise-45v) useful but not strictly required. Target bundle: 200-500 KB compressed — formula layer stays server-side. ** DOING Split AppState into ModelState + ViewState (standalone refactor) (standalone) :standalone:P2:feature:@Edward_Langley: :PROPERTIES: :ID: improvise-vb4 :TYPE: feature :PRIORITY: P2 :STATUS: in_progress :ASSIGNEE: Edward Langley :OWNER: el-github@elangley.org :CREATED_BY: spot :CREATED: 2026-04-14 07:21:59 :UPDATED: 2026-04-16 23:02:18 :KIND: standalone :END: *** Details **** Description Refactor the current App struct to make the model/view slice boundary explicit in types. Today App mixes Model with session state (mode, cursor, scroll, buffers, etc.); split them so that ModelState holds document state and ViewState holds per-session UI state. Server continues to own both, but as distinct fields. Valuable independently of the browser epic: cleaner persistence (only model slice serializes), cleaner undo (undo walks model effects only), better test boundaries, and it's the foundational slice separation every downstream issue depends on. **** Design pub struct ModelState { model: Model, file_path: Option, dirty: bool }. pub struct ViewState { mode: AppMode, selected, row_offset, col_offset, search_query, search_mode, yanked, buffers, expanded_cats, help_page, drill_state_session_part, tile_cursor, panel_cursors }. App becomes { model_state: ModelState, view_state: ViewState, layout: GridLayout, ... }. Audit every App field and assign it. Gray zones: Model.views stays in Model (document state). drill_state staging map is session (View); commit flushes to Model. yanked is View. file_path/dirty are Model. **** Acceptance Criteria (1) App contains ModelState and ViewState as typed subfields. (2) No App field lives outside exactly one slice (or is explicitly called out as derived, like layout cache). (3) All existing tests pass. (4) Effect apply methods updated to take &mut the correct slice where obvious, or &mut App where cross-cutting. (5) Audit report in issue notes: every field classified. **** Notes Additional implementation note merged from superseded improvise-dwe: a small number of effects today read App-level/layout-derived state at apply time (e.g. EnterEditAtCursor reads display_value from layout). For these, pre-compute the derived value in the command's Cmd::execute (where CmdContext has layout access) and bake it into the effect struct's own fields. Apply bodies then become pure model/view state writes. Example: EnterEditAtCursor { target_mode, initial_value: String } with initial_value computed in execute and consumed by apply. ** TODO Browser frontend MVP: end-to-end working demo (epic) [/] :epic:P2:feature: :PROPERTIES: :ID: improvise-1ey :TYPE: feature :PRIORITY: P2 :STATUS: open :OWNER: el-github@elangley.org :CREATED_BY: spot :CREATED: 2026-04-14 07:24:44 :UPDATED: 2026-04-14 07:24:44 :KIND: epic :END: *** Details **** Description Wire everything together into a working browser demo: start the ws-server, open the HTML shell in a browser, it loads the wasm client, connects to the websocket, receives the initial snapshot, renders the grid, accepts keystrokes, and round-trips commands to the server. This is the milestone where the architecture proves itself end-to-end. Scope: open an existing .improv file server-side, view it in the browser, type a number into a cell, see the server-side value update and the browser's projection arrive with the new value. **** Design HTML shell: minimal index.html with a
for the grid and a