Commit Graph

173 Commits

Author SHA1 Message Date
Claude (felo13) 49ea84ca5f fix(docs): register the layout evidence pages; test the two gaps the review found (improvise-felo.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-02 19:18:47 -07:00
Claude (felo13) 14cef311dd docs(evidence): layout cost evidence and measurements; fmt the merged bench (improvise-felo.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-02 19:01:46 -07:00
Claude (felo13) 3b168a2ec8 perf(grid): keep the whole-grid summary per layout and read a window over it; exact-key probe in DataStore (improvise-felo.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-02 18:54:47 -07:00
Claude (felo13) 2f05f6b62d perf(layout): index a data item by position, not by walking the axis (improvise-felo.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-02 18:46:18 -07:00
Claude (felo13) 311ae96218 test(layout): a lookup and a cursor key do work independent of grid size (improvise-felo.13)
Two of the three fail on purpose: cell_key is linear in the row index and a
cursor key rebuilds the layout. The third guards the invalidation the fix adds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-02 18:44:06 -07:00
Claude (agent-adr007) 3116d524fb feat(core,protocol): a display beside the key, painted by the three panels (improvise-cq1q.3)
- `Category`, `Item` and `View` each gain a private `display` that starts equal
  to the key and changes only through `set_display` / `set_item_display`.
- `CategoryRow`, `TileEntry` and `ViewSummary` carry `display` beside their key
  field. The key stays what commands are composed from.
- The category-panel headers, the tile bar and the view list paint the display
  in the TUI and the browser. The active-view marker still compares keys.
- No production code sets a display yet, so behaviour is unchanged.
- This is the first slice of ADR 007 step 2. The grid, page bar, records labels,
  item rows and CSV export are improvise-cq1q.12; messages and the view title
  are improvise-cq1q.13.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-25 12:12:59 -07:00
Claude (agent-adr007) 7ddc50ac62 Merge commit '62066e1e1c972218b099a36f3f10440eb93d430e' into adr007/plan
# Conflicts:
#	context/code/core.md
#	context/test-evidence/core.md
2026-09-25 10:05:02 -07:00
Ed Langley 7dc34e8458 Merge branch 'main' into feat/nn4s-faces 2026-09-25 00:01:32 -07:00
Claude (agent-adr007) 6c652c2ed3 fix(core): walk taken suffixes with a loop, not recursion (improvise-cq1q.2)
IdAllocator::suffixed recursed once per taken candidate, and the call was not
in tail position. A document claiming item-1 through item-N aborted the process
on a stack overflow at N of about 11,000 in debug. This answers the Codex P2 on
PR #318. The loop still advances the counter on every candidate it tries.

Three tests are added:
- a long claimed run (20,000), which aborted before the fix;
- claim refusing lookalike virtual ids (_indexer, _measure-1), which the
  review's prefix-match rival passed before;
- a display with leading spaces that must not slug into the reserved `_` space.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 23:41:16 -07:00
Claude (agent-hier) 8d3230e685 fix(core): an expanded fold's depth is non-zero by type, so every fold the API builds reads back (improvise-felo.6)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 22:29:02 -07:00
Claude (agent-faces) f7d2730736 style(faces): rustfmt the face migration (improvise-nn4s.2)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 22:05:24 -07:00
Claude (agent-adr007) e56148fa64 feat(core): slugify and the gensym id allocator for ADR 007 (improvise-cq1q.2)
model::id adds slugify, which case folds, turns each run of spaces into one
`_`, strips everything outside a-z, 0-9, `-` and `_`, then drops leading
underscores. It also adds IdAllocator, which keeps one increment-only counter
per document plus every id seen this session; claim admits hand-written ids
verbatim. Full case folding comes from the unicase crate. Nothing calls it yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 22:02:32 -07:00
Claude (agent-faces) 12d976b432 feat(faces): the TUI draws axis, wizard-field, mode and minibuffer colours through face_style (improvise-nn4s.2)
axis_fg, minibuf_fg, field_kind_fg and mode_style are gone, and AxisColor
leaves improvise-core. The category-add prompt turns green, ADR 004
clause 8's one declared exception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 21:58:01 -07:00
Claude (agent-faces) abff562360 feat(faces): Face, Theme, resolve and the shipped theme in improvise-protocol (improvise-nn4s.1)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 21:54:11 -07:00
Claude (agent-hier) e497aef110 feat: nested group runs reach the wire, and views persist expanded(N) and collapsed folds (improvise-felo.6)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 21:43:55 -07:00
Claude (agent-hier) 8dad4b7476 feat(core): groups nest within a category and start collapsed (improvise-felo.6)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 21:22:16 -07:00
Edward Langley 7b9e868bd2 test+docs: derive #304's cap fixtures from MAX_CATEGORIES; record the coupling found (improvise-felo.1)
The duplicate step's cap check already reads the one constant after the
merge; its tests filled 12 categories and now fill to the cap. An
injected literal-12 check fails keeping_duplicates_separate_is_refused_at_the_category_cap.
core.md section 9 gains the inventory of what degrades as categories grow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 19:26:41 -07:00
Edward Langley 7d1f90dbf9 feat(core): raise MAX_CATEGORIES from 12 to 24, with one home (improvise-felo.1)
The wizard's private copy of the limit is gone; it reads the model's
now-public constant. Tests that fill to the cap derive it. Why 12 was
chosen and what 24 costs: context/code/core.md section 9.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 19:19:39 -07:00
Edward Langley 5e8e838f23 Merge #221 (c7b5b825, improvise-fdk1 coordinate rename) into the editor integration branch
Resolutions:
- CellId: #221 gives Coordinate fields { record, category }; main (#292)
  adds NonItem { category, item }. Every exhaustive match now names both
  (render.rs, core.rs x2, project.rs, resolve.rs x3, wasm-client lib.rs and
  render.rs); project.rs's basis arm keeps #221's Coordinate-carries-a-record.
- resolve.rs edit_or_drill: #221 drops the "renaming a coordinate" decline
  (Coordinate opens an editor); main's NonItem decline arm is kept.
  coordinate_at_cursor's None arm gains NonItem. reopened_at keeps #295's
  declined_at shape; declined_at's editable arm names Coordinate { .. }.
- client.rs: kept main's status_seen/status_owner and #221's opened +
  OpenedCoordinate; every settle site calls both settle_opened and
  settle_status. Main's new rebase() also gets settle_opened, since it
  rebuilds the view exactly as apply() does.
- view/mod.rs: re-export both GridEval (main) and record_row (#221).
- context/code/command.md and context/test-evidence/clients.md: both sides
  appended independent sections; kept both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 12:13:42 -07:00
Edward Langley 46896a7512 Merge PR #250 (3e417729) into the 2026-09-24 integration wave
Conflicts with #294 in render.rs and project.rs resolved as tested:
#250's side of every hunk (levelled GroupEnd { level }, groups()/group_of,
axis_frame's carried headers, the try_from level check), then #294's shape
(Item { tuple, .. }, FrameEntry::item(..), the DataItem { tuple, collapsed }
arm, and GroupEnd { .. } in folds). Evidence EOF appends keep both sides.
2026-09-24 02:49:32 -07:00
Edward Langley 903d824e38 Merge PR #254 (3c513540) into the 2026-09-24 integration wave
# Conflicts:
#	context/test-evidence/command.md
#	context/test-evidence/core.md
#	crates/improvise-command/src/ui/app.rs
2026-09-24 02:48:03 -07:00
Edward Langley 88598c6f7e Merge PR #292 (3f98a17c) into the 2026-09-24 integration wave
# Conflicts:
#	context/test-evidence/command.md
#	crates/improvise-command/src/ui/app.rs
#	tests/shipped_documents_load.rs
2026-09-24 02:46:37 -07:00
Edward Langley 5b73e67caf Merge PR #223 (e1453797) into the 2026-09-24 integration wave 2026-09-24 02:44:11 -07:00
Edward Langley 24778cf284 Merge PR #242 (1c6600c3) into the 2026-09-24 integration wave
# Conflicts:
#	context/test-evidence/command.md
2026-09-24 02:43:51 -07:00
Edward Langley cfa8bf1e0a Refuse a frame whose level names no category; lend the open-run stack
Two ChatGPT Codex findings on the #250 stack.

- #250, P2 (improvise-e265): a FrameGroup.level from the wire sized the
  open-run stack in AxisFrame::groups, so a huge level drove a huge
  allocation. AxisFrame now deserializes through UncheckedAxisFrame and
  refuses any group or GroupEnd level not below cats.len(), naming it.
  context/code/protocol.md §3b.
- #273, P3 (improvise-0r01): the lazy open_runs still cloned a stack per
  skipped row under .nth(idx). walk_open_runs lends one stack to a visitor
  that can stop the walk, so a lookup copies nothing for earlier rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 23:41:48 -07:00
Edward Langley e44b1491ca Merge gh/main (cc95cfbf) into fix/e265-levelled-groups
# Conflicts:
#	context/test-evidence/command.md
#	context/test-evidence/core.md
2026-09-23 23:26:55 -07:00
Edward Langley 1629944eac Merge gh/main into fix/gk51-non-item-record-projection (#292)
Conflicts:
- cmd/core.rs committable_cell_key: took main's form, which asks
  `uneditable` (#277), and gave `uneditable` a declining NonItem arm.
  That keeps this branch's refusal of a commit on a non-item cell.
- resolve.rs reopened_at (auto-merged, non-exhaustive after the merge):
  a declining NonItem arm, the sentence `i` already says.
  The #277 review asked for both arms to decline, never to open.
- tests/shipped_documents_load.rs: kept both, this branch's
  every_record_a_shipped_document_projects_is_writable and main's git()
  helper.
- context/test-evidence/{command,core}.md: both sides appended at EOF; kept
  both, the branch's first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 23:19:46 -07:00
Edward Langley 86afdb0a36 Merge gh/main (cc95cfbf) into test/4h8a-sfwy-guards
# Conflicts:
#	context/test-evidence/command.md
#	context/test-evidence/core.md
2026-09-23 23:19:23 -07:00
Edward Langley cc5e2635d5 Merge gh/main (666de199) into wave/tej1-browser-delete-formula
context/test-evidence/command.md: both sides inserted a block at the
same point, before the "improvise-i3o5: four declines" subsection. Both
kept: this branch's remove_formula_of_a_missing_formula_is_a_refusal,
then gh/main's totals-strip section (improvise-wm20).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 23:00:13 -07:00
Edward Langley c7f93515ba Merge gh/main (666de199) into fix/oplp-followups
context/test-evidence/command.md: both sides appended at EOF; both kept
(this branch's two drill tile-select entries, then gh/main's
Host::mutating_frame abort-arm section).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 22:49:52 -07:00
Edward Langley 516ad66af2 Merge gh/main (666de199) into fix/fdk1-coordinate-rename
Conflicts:
- cmd/core.rs committable_cell_key: take main's `uneditable` door; its
  pattern names fdk1's struct `CellId::Coordinate { .. }`.
- resolve.rs reopened_at (main, clean text): same pattern fix, a
  compile-only semantic conflict.
- context/test-evidence/{clients,command}.md: both sides' entries kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 22:47:10 -07:00
Edward Langley 5b7e0223aa Merge fold-memo/pntu-0jw5 (fa3350ac) into fold-memo/metf-opbo
Carries #268 onto #258's new head, which now sits on #247's re-merge
(12f206fb) and so on #239's spanned_values and &Expansion signatures.
Code auto-merged: GridLayout::evaluator / GridEval stays the only public
path to a FoldMemo, and prune_empty still shares one memo. The one
conflict was benches/grid_projection.rs's main(): both new sections kept,
the pruned block kept directly after the folded chain it calls "the same".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 19:08:07 -07:00
Edward Langley fa3350acfb Merge collapse-fu/h6z1 (12f206fb) into fold-memo/pntu-0jw5
Carries #258 onto #247's re-merged head, which now has #239's
spanned_values/spanned_positions and the &Expansion fold signatures.
Code auto-merged: FoldMemo::new(agg, covers) still owns both, and
fold_stored reads through spanned_values. The only conflicts were the
append-at-EOF evidence files (command.md, core.md); both sides kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 19:02:39 -07:00
Edward Langley 12f206fb0a Merge collapse-fu/wm20 (500d4609) into collapse-fu/h6z1
# Conflicts:
#	benches/grid_projection.rs
2026-09-23 18:33:53 -07:00
Edward Langley 500d46092b improvise-jq0m: pin both narrowings against the per-member definition; record the bench and the evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:32:33 -07:00
Edward Langley 08f84c25e2 improvise-jq0m: a small span narrows per covered item, not by the whole category; grouped bench cases
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:16:29 -07:00
Edward Langley 519c8ddefd Model::is_item: membership without building the item list (improvise-4daa)
`first_non_item` asked `effective_item_names(cat).contains(name)` per
coordinate per projected cell, cloning every item name each time, and the
TUI projects the whole grid every frame. `is_item` asks `contains_key`,
plus the formula targets for a measure category, and is pinned equal to
the list it replaces. #252's write path already goes through
`first_non_item`, so both callers get it.

Measured on an Apple M4 Max, interleaved 5x, minimum per build: pivot
18k cells 193 ms -> 136 ms (base 131 ms); records with a 2000-item label
145 ms -> 2.8 ms (base 1.9 ms).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:11:18 -07:00
Edward Langley 0c3ec183d3 Pin the column toggle's cursor-follow; a group lookup stops at its row (improvise-pb30, improvise-0r01)
pb30: gz puts the cursor on the collapsed group through landing and
place_cursor, and their column arms were untested. Three rivals passed
all 1948 tests: the arm swapped, landing returning None for columns, and
landing reading col_tuple(0). A new test uses two column levels
(Region over Month) with the cursor at (1, 7), South/May. It requires
the cursor to land on South's Q2 at (1, 5). Each rival fails it and
nothing else. The col_tuple(0) rival needs the two column levels, since
with one level the prefix is empty everywhere.

0r01 (Codex's P2 on #263): innermost_group built open_runs for the
whole axis, cloning a level stack per row, to answer for one row on
every z. open_runs is now a lazy iterator, still the one reader, and
the lookup takes .nth(idx). a_lookup_does_not_read_past_its_row gives
it an unreadable header after row 0 (level usize::MAX). The eager rival
panics with capacity overflow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:49:05 -07:00
Edward Langley be99391818 Merge collapse-fu/wm20 (f668ddc9) into collapse-fu/h6z1
# Conflicts:
#	context/code/command.md
#	context/test-evidence/core.md
#	crates/improvise-core/src/model/types.rs
2026-09-23 17:34:11 -07:00
Edward Langley d7dceef549 a grid evaluates through its own evaluator, never another's memo (opbo)
GridLayout::evaluate_in and column_total took any &FoldMemo, so a memo
from a layout showing Feb, handed to one hiding it, gave 5.0 where 2.0
was right. GridLayout::evaluator(model) now returns a GridEval that owns
a memo its layout built, with cell() and column_total(); the memo-taking
methods and the public fold_memo are gone. grid_body and prune_empty use
it, and the earlier rivals still fail when restated for the new API.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:30:41 -07:00
Edward Langley f668ddc94e rustfmt the yb4b/b5dp changes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:24:40 -07:00
Edward Langley fd221c6c6c prune_empty shares one fold memo per build (metf)
prune_empty evaluated every cell with a fresh memo, h6z1's shape on the
same keypress path. Measured on an M4 Max: a pruned build of the folded
chain went 705-900 ms -> ~350 ms at 18k and 6.4-6.6 s -> 3.0-5.0 s at
90k; the dense grid is unchanged within noise. A new bench line times
the pruned build. The test counts 9 evaluations where a fresh memo per
cell counts 45, and checks the pruned rows are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:24:34 -07:00
Edward Langley 1a9d73259c Merge gh/main (202e7eac) into collapse-fu/wm20
# Conflicts:
#	context/test-evidence/command.md
2026-09-23 17:19:55 -07:00
Edward Langley 076ead7c7c improvise-yb4b/b5dp: pin the scan against the per-member fold; record the ruling, the bench and the evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:18:55 -07:00
Edward Langley 3d374d8b3e improvise-gk51 WIP: one predicate for the projection and the refusal; CellId::NonItem
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:09:25 -07:00
Edward Langley 2ea2407456 Pin the column group toggle to the cursor; guard run identity in e265's enumeration (improvise-uv9e, improvise-6u6s)
uv9e: ToggleGroupAtCursor { is_row: false } (gz) had no command-level
test, and a version reading col_group_for(0) passed the workspace. A new
test seats the cursor at (0, 2) with Month on the columns and requires
Q2. Reading column 0, or reading the row half of the cursor, both fail
it. The row half was already closed: the grid tests sit on row 2, and
injecting row_group_for(0) fails five of them. The browser's z is still
Upstream with no resolver arm (improvise-uln5), so it has no cursor
reading to test.

6u6s: the e265 enumeration could not see run identity. A pruner keying
runs by (level, category, group name) instead of header position
passed. The enumeration now:
- compares every level's run name and whether each survivor shares a
  run with the previous one;
- adds three fixtures: a non-contiguous group, a level with no groups,
  and two adjacent outer groups;
- builds each layout once per collapse subset and drives
  prune_axis_items with a keep predicate per row set: 4,992 cases.

The name-keyed rival now fails at fixture 2, mask 0b101. open_runs is
the one reader of the markers. improvise-core's lib went from
0.86-0.92s to 0.17-0.23s on an Apple M4 Max under load average 39-77.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:08:02 -07:00
Edward Langley ec85401a44 improvise-yb4b: a stored fold scans the store once instead of looking up each member
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:03:05 -07:00
Edward Langley 6b3577a09e fold memo owns its agg and covers; grid_body's sharing is guarded (pntu, 0jw5)
improvise-pntu: FoldMemo was keyed by (key, span) while the aggregation
and covers were call arguments, so one memo across two covers answered
the first (5.0 where 2.0 was right). FoldMemo::new(agg, covers) now owns
both and the _in functions read them off it; GridLayout::fold_memo builds
the grid's. The mismatched call no longer compiles.

improvise-0jw5: a test in improvise-command counts fold evaluations across
one grid_body call (18) against a fresh-memo control (90). A per-cell or
per-total fresh memo fails it. The counter is exposed through a new
test-support feature on improvise-core, enabled only as a dev-dependency.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 16:57:06 -07:00
Edward Langley 67438b8f66 test: guard every field App::restore puts back; assert the records coordinate by value
improvise-4h8a: restore_puts_back_every_field_the_snapshot_took writes all
thirteen AppSnapshot fields, asserts each moved, restores, and asserts each
is back. Dropping any one assignment from App::restore, or restoring
view_state as only active_view + status_msg, fails it naming the field.

improvise-sfwy: records_mode_records_display_returns_values asserted the
Region column was non-empty; it now asserts Some("North"). A GARBAGE or
category-name fault passed the old assertion and fails the new one.

Test-only; production code is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 11:23:28 -07:00
Edward Langley d6e366dceb Group runs nest: markers name their level, pruning re-marks (improvise-e265)
On an axis with two grouped categories, runs nest: North in Upper holds
Jan in Q1, and after Q1 ends North/Mar is still in Upper. The markers
carried no level, and every reader was flat, so the nearest marker
decided. Pruning an inner header dropped its GroupEnd too, and a
surviving row changed group. The unpruned answer was lossy as well:
North/Mar was in no group while it was drawn under Upper.

The user ruled per-level membership, with z toggling the innermost group
(the one nearest the cursor). So:

- AxisEntry::GroupHeader and GroupEnd carry a level. expand_category
  reads it off its prefix, which holds one item per outer category.
  Membership is one open run per level, and a row's group is the
  innermost one (row_group_for/col_group_for, hence z).
- prune_axis_items re-marks the survivors instead of filtering the
  markers. Before each kept item it emits the markers that make the
  output's open runs equal the input's, closing deeper runs innermost-out
  first. So each survivor keeps exactly its runs, no header is kept over
  nothing, no trailing GroupEnd is left, and when nothing is pruned the
  output equals the input. This replaces the pending/close_runs_of/
  run_open machinery.
- The wire: FrameGroup gains level (an index into AxisFrame::cats), and
  FrameEntry::GroupEnd becomes GroupEnd { level }. groups()/group_of
  read the same per-level rule, and group_runs is unchanged (h0iv).
- axis_frame carries the open run at each level above a scrolled
  window, not the single nearest header, so a window opening inside
  North/Q1 still knows Upper.

Tests:
- an enumeration over every funded mask x collapse subset of a two- and
  a three-level fixture (about 4,000 cases), asserting that pruning
  moves no row's group, that no header is drawn over nothing and no axis
  ends on a marker, and that nothing-pruned is marker-for-marker equal;
- the ruling at the layout and through z;
- frame/layout agreement at every window offset;
- the nested window carry.

Rivals, each injected and restored, are in the evidence files: flat
membership, a pruner that owes no GroupEnd, ends passed through eagerly,
headers kept eagerly, nearest-only window carry, and a flat frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 11:20:03 -07:00