Commit Graph

87 Commits

Author SHA1 Message Date
Claude (dbsp-sources) 7f42a724f9 fix(core): a model put over a source drops the formula cells its own cells computed (improvise-rnak)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-10 19:32:39 -07:00
Claude (dbsp-sources) bcc773bb2a Merge dbsp/source-trait into dbsp/model-over-source
# Conflicts:
#	context/code/source.md
2026-10-10 19:31:46 -07:00
Claude (dbsp-sources) 8ba4bd0a6c fix(core): summaries are tested for exact merges, impossible parts are refused, and the bypass list is complete (improvise-qddo)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-10 19:30:22 -07:00
Claude (dbsp-sources) bebaeb2d61 fix(core): a model over a source refuses every write in every build and drops its own cells (improvise-rnak)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-10 18:57:13 -07:00
Claude (dbsp-sources) 7c5f77708c Merge dbsp/source-trait into dbsp/model-over-source
# Conflicts:
#	context/test-evidence/core.md
2026-10-10 18:54:23 -07:00
Claude (dbsp-sources) 4a49398f1a fix(core): a Summary can be built from an engine's parts, and keys stop cloning values (improvise-qddo)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-10 18:52:58 -07:00
Claude (dbsp-sources) 315e271905 feat(core): a model can be over a source, so one evaluator serves any of them (improvise-rnak)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-10 18:03:20 -07:00
Claude (dbsp-sources) 5fdf502871 refactor(core): a read-only CellSource trait, DataStore its first implementation (improvise-qddo)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-10 17:55:14 -07:00
Ed Langley cea0ff8d9a Merge pull request #339 from fiddlerwoaroof/adr007/display-2c
feat(display): status messages and the view title show displays (improvise-cq1q.13)
2026-10-10 11:16:05 -07:00
Claude (adr007-2c) a06bad119f Merge gh/main (5f8c3f8c) into adr007/display-2c 2026-10-09 23:17:56 -07:00
Claude (adr007-slug) ae398e23b2 Merge gh/main 5f8c3f8c into adr007/slugify-amendment 2026-10-09 23:17:55 -07:00
Ed Langley c57069a359 Merge pull request #336 from fiddlerwoaroof/adr007/display-2b
feat: displays in the grid frame, page bar, records labels, item rows and CSV (improvise-cq1q.12)
2026-10-09 20:42:45 -07:00
Claude (adr007-2c) 369d1e12c0 test(display): pin the browser's declined_at arm and the removal operands to keys (improvise-cq1q.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-09 13:30:42 -07:00
Claude (adr007-slug) 040b7070e3 test(core): pin the zeros tie-break, widen the id-syntax and suffix properties, refresh the amendment's built-code note (improvise-cq1q.14)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-09 13:09:18 -07:00
Claude (adr007-2c) 300b5afc7f fix(display): defer the formula-result display: the surrogate cannot find the target's category (improvise-cq1q.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-09 13:08:33 -07:00
Claude (adr007-2c) bb7fe88547 docs(context): core.md §21 third slice, test evidence, and two comment runs shortened (improvise-cq1q.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-09 12:55:41 -07:00
Claude (adr007-slug) 2e1f0f6550 docs(context): core.md section 20 and test evidence for the amended id rules (improvise-cq1q.14)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-09 12:49:09 -07:00
Claude (adr007-steward) 1190c80efe docs(adr): tag every ADR 007 change to accepted text as [Amendment] (improvise-cq1q.1)
The Status now says the tag is the complete list of proposed rules, and its
table gains the regression test's step 3. Clause 12's test keeps the
accepted "commit the unedited text" with the amendment's edit-a-non-name step
tagged; the outside-_Measure real-item rule, the formula-collision example
and the East/East_ collision sentences are tagged; the "exactly one id"
sentence in clause 2 is restored; "other entities" loses the added
"virtual"; the title drops "and groups". core.md section 20 and the id.rs
test evidence cite ADR clauses instead of answer numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-08 23:22:01 -07:00
Claude (adr007-display) 09fa78e7cd test(command,wasm): column width, row-header width and column group labels follow displays (improvise-cq1q.12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-08 22:38:16 -07:00
Claude (adr007-display) d20c5471bd fix(core): a page axis with ragged item displays is refused at deserialise (improvise-cq1q.12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-08 22:11:43 -07:00
Claude (adr007-display) 7a7ff81731 docs(context): core.md section 21 and test evidence for displays beside keys, second slice (improvise-cq1q.12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-08 21:58:46 -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-faces) 92f8ac2113 docs(faces): where faces live and what the first two steps settled (improvise-nn4s)
context/code/protocol.md gains section 10 and tui.md's colour section now
describes face_style; the pointers to axis_fg, minibuf_fg and AxisColor
elsewhere follow. Evidence for the new tests.

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) 5cf070ff98 docs(core): why slugify folds with unicase, and the evidence for each id rule (improvise-cq1q.2)
context/code/core.md §19 gives the reasoning behind model::id.
context/test-evidence/core.md records, for each test, the rival that was
injected and the test that caught it.

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:41 -07:00
Claude (agent-hier) e09ec75226 docs: group hierarchies and fold lines, with the evidence for each rival (improvise-felo.6)
The harness fixtures open their groups explicitly now that groups start closed.

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:23 -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 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 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 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 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 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 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 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
Edward Langley 4f75fffcb9 improvise-h6z1: one fold memo per grid build, keyed by fold key and span
viewport::grid_body makes one FoldMemo and passes it to every cell and
total through GridLayout::evaluate_in / column_total, so a fold row's
formula chain is evaluated once across the row (9, not 45). The bench
gains a folded five-formula chain case; stored-only grids 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 09:09:34 -07:00