3d0850c1326b8f95320cb1a5f143bd395a30033a
47 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3d0850c132 |
Merge main (d7bfcbb, PR #96) into wave/operandless-commands
Two textual conflicts, and one the merge resolved cleanly and wrongly. effect.rs: #96 extracted the LayoutGeometry list into layout_geometry_cases() shared by two tests, one of which compares it against the wire vocabulary; my side had added ViewTarget arguments and carried the base's assert_eq!(cases.len(), 13). Kept #96's structure with my arguments and dropped the count -- the list-versus-vocabulary check is strictly stronger and a count beside it would be a second answer to the same question. Verified the count came from the merge base, not from my side, before dropping it. test-evidence/command.md: both sides appended entries at the same point. Kept both. app.rs auto-merged CLEANLY and did not compile: improvise-4dpj's new a_batch_that_leaves_records_settles_out_of_records_normal constructs effect::SetAxis, which now carries a view operand. Fixed with ViewTarget::Active -- the host's own key path, which is what that test is. Checked rather than assumed, per the brief: - Test names diffed BOTH ways against the union of the two parents, not the count. Union 1510, merged 1509, exactly one name absent and it is the intended one: a_hosts_none_cats_leak_into_another_views_formula_columns, the ac2z characterization test my own branch replaced with a_formulas_columns_survive_a_host_that_aggregates_differently. Nothing else lost, nothing unexpected gained. - #96's the_enumeration_names_every_variant_the_derive_declares is still discriminating against the reshaped ModelCommand variants: dropping TransposeAxes from every_model_command() fails it by name. - The test-support feature stays off in shipped builds: cargo tree --edges normal --target wasm32-unknown-unknown finds no serde_json. And the finding the brief asked for, which is the opposite of a disagreement: #96's switching_views_across_records_ness_settles_against_ the_view_switched_to is the HOST-side twin of the client fix in this branch. Same batch, same crossing in both directions, and independently the same phrase in its message. Neither is redundant -- nothing in the projection stream carries the host's ViewState, so a host-side guard cannot see a client's mode or the reverse. Cross-referenced in context/test-evidence/clients.md so neither is deleted as a duplicate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
498582deb1 |
Bound every_model_command() with the derive (improvise-26dm, Codex P1)
The right-hand side of the LayoutGeometry completeness diff was a second hand-written list. Both this branch's own review and ChatGPT Codex found it, and the previous commit corrected the PROSE; Codex asked for the FIX. This is it. the_enumeration_names_every_variant_the_derive_declares diffs every_model_command() against variants::declared::<ModelCommand>() -- serde's derive-generated variant list, improvise-5ubs's mechanism. The chain is now derive -> every_model_command() -> model_effects -> case list, every link checked, which is what the ticket asked for. Injection is Codex's own, run end to end: a ModelCommand::ProbeGeometry whose effect declares LayoutGeometry, with the model_effects and variant_name arms the compiler forces, omitted from BOTH lists. Before: the whole workspace green. After: staged, each stage naming the probe -- every_model_command() never constructs ["ProbeGeometry"] then, with the enumeration repaired and only the case list short, the_layout_geometry_cases_are_every_wire_effect_of_that_kind right: [... "ProbeGeometry" ...] The staging is the point: the two tests answer different questions. SCOPE OVERRUN, declared. `variants` was `#[cfg(test)] pub(crate)`, not merely `pub(crate)`, and `declared()` needs serde_json, which improvise-protocol carries only as a dev-dependency -- so the authorised one-token change could not work. What it took instead, all plumbing, no behaviour change: improvise-protocol/Cargo.toml serde_json optional + a `test-support` feature improvise-protocol/src/lib.rs #[cfg(any(test, feature = "test-support"))] pub improvise-command/Cargo.toml dev-dep enables that feature The feature is OFF in every shipped build: `cargo tree --edges normal --target wasm32-unknown-unknown` shows no serde_json, and all eight wasm-gated crates still check clean. Drop this commit if the crossing is unwanted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
f6fca7ec98 |
A formula is evaluated for the view being projected (improvise-ac2z)
rebuild_layout recomputes formulas for the HOST's none_cats, and grid_in's elsewhere path built another view's GridLayout against that cache. The cache is keyed by the coordinates the recompute used, so a Total keyed without a Product coordinate is not found by a view with Product on Column, and prune_empty drops the formula's columns entirely. Absent columns, not wrong numbers. grid_in::rederived_for clones the model and recomputes for the target view's none_cats. Three conditions gate the clone -- another view, different none_cats, at least one formula -- so a client on the host's own view pays nothing and a formula-free document pays nothing ever. This is an interim and the cost is why: a whole-model clone plus a fixed-point recompute per projection on exactly the broken path. The ticket's first candidate, keying formula_cache by none_cats so one Model holds more than one view's derived values, is the durable answer and is an improvise-core change outside this wave's ground. The characterization test is replaced by the positive one, and the none_cats carve-out is dropped from a_clients_grid_does_not_depend_on_which_view_its_host_is_on -- which is the general property this is an instance of, and which fails under the same injection. Evidence: context/test-evidence/command.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
7bc26f80d5 |
Review round 1: three checks that did not check what they named
All three found by the adversarial review of
|
||
|
|
1dd6950ed3 |
A view-scoped ModelCommand names its view (improvise-lltt)
Nine ModelCommand variants configure a view and named it implicitly: the effect read App::view_mut(), the HOST's slice. Since improvise-jytm made ViewState::active_view authoritative per client, a client on another view moved the host's view instead of its own -- its grid did not change and every other client's did. Each of the nine now carries `view: String`; model_effects maps it to effect::ViewTarget::Named, and the nine effects carry a ViewTarget rather than reading an ambient one. There is no operandless form left: App::view_mut() is deleted and replaced by view_mut_of(&ViewTarget), so a future geometry effect has to say which view it writes. The host's key path passes ViewTarget::Active, which is right -- the TUI is the host and has no session to ask. A Named view the document has not got is a no-op rather than resolve_view's fallback onto the default. Second half, which the ticket did not predict: naming the view made a reframe of a view the host is NOT on reachable, and adopt_cursor_epoch could not see one -- cursor_epoch is bumped only by the host's own reset_cursor, so those clients were reset by nobody and told nothing (gotcha 24's strictly-worse-than-nothing state). App::last_reframed_elsewhere is the second trigger; its opening frames are taken lazily inside view_mut_of, the one door such a write goes through, so the held-key path still hashes one view and improvise-mcq4's declined cost is not paid. Evidence and the three injected faults: context/test-evidence/command.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
78ca25b5e0 |
Test both arms of the records-mode settle (improvise-4dpj)
settle_normal_mode matches `Normal | RecordsNormal`, and replacing that with a
one-way `Normal` left the ENTIRE workspace suite passing: the existing crossing
test drives the layout both ways, but both branches end in `enter-mode normal`,
so the settle is handed `AppMode::Normal` either way. It discriminates "settles
at all" and "settles against the ending layout", never "settles from either
mode".
Gap 1, a_batch_that_leaves_records_settles_out_of_records_normal: the
production repro -- SetAxis { "_Index", Axis::None } from a records grid.
Injection is the one-way guard; it is the only test in the workspace that fails.
Gap 2, switching_views_across_records_ness_settles_against_the_view_switched_to
(raised by ChatGPT Codex on PR #91, verified correct on main first): the batch
SwitchViewAtCursor emits, both ways. A genuinely different path -- records-ness
is auto-detected from the axes, so nothing in the batch says "records".
Injection is moving the settle back above rebuild_layout; both fail by name.
Scope note in the evidence: this drives the effects, not the view-panel key
path, which needs a CmdContext in another wave's ground this round.
Also carries the command.md half of improvise-hsoe's three duplicate headings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
d41c8b8e6f |
Merge main (1a9f45a) into wave/protocol-render-gaps
#91 and #93 landed while this wave was in flight and appended to the same four context/ files. That conflict is why CI never ran: a pull_request workflow builds refs/pull/N/merge, and a CONFLICTING PR has no merge ref, so no run is ever created. Neither the close/reopen nor the empty commit could have helped. Resolution: keep both sides. Only context/test-evidence/protocol.md conflicted textually; the other three auto-merged. Verified with --no-ext-diff that nothing of this branch was dropped -- the only removals are main's own rewrites, of the frame-basis paragraph in context/code/command.md and of the minibuffer-exit entry (renamed _to_plain_normal -> _to_records_normal) in test-evidence/command.md. NOTE the first three "nothing was dropped" checks were WORTHLESS and read as clean: an external diff tool is configured, so `git diff | grep '^-'` was grepping a side-by-side format for unified-diff markers and matched nothing whatever the content. --no-ext-diff is what actually answers the question. A check that returns empty for the wrong reason is indistinguishable from one that passes. Fixed while re-reading, and pre-dating the merge: this branch had TWO entries headed a_window_narrows_the_cells_and_neither_the_totals_nor_the_widths -- the original, describing a fixture that no longer exists and repeating the claim the PR #92 review measured false, and the superseding one. Merged into a single entry whose subject is what each of the three fixtures was blind to. The evidence checker cannot see this: it verifies that a heading names a real test, and a duplicate heading names one just as well. Merging gh/main ( |
||
|
|
902a77c174 |
review: two corrections from the #92 adversarial review, and a P2 they found
1. THE JMJZ WIDTH ASSERTION COULD NOT SEE A WINDOWED WIDTH, and the evidence
said it could. Injecting `if drawn { widen(..) }` -- widths from the window only
-- left 1476 passing, 0 failing. Two reasons, the second structural: the measure
was named `Revenue` (7 chars) against a value of `300021` (6), so the assertion
passed on the header alone; and comparing grid_body's widths against
compute_col_widths is INCAPABLE of testing windowing, because compute_col_widths
IS grid_body with an empty window. Windowing degrades both sides identically.
The same function twice is not a check, and it reads exactly like one.
Fixture is now the reviewer's shape -- a total that cancels and a one-character
header -- reordered once more so BOTH assertions discriminate: with the large
values last, the windowed row and the cancelled total were both 1.0, so the
totals assertion went blind to the row-window fault while the widths assertion
happened to catch it. Large values first:
for ri in rows.clone() -> totals [888888888.0] against [1.0]
if drawn { widen(..) } -> widths [10] against [11]
10 rather than 5 is a smaller gap and a better witness: the missing character is
the minus sign of a row outside the window.
2. THE T6DB none_cats PARAGRAPH REPORTED A NEGATIVE RESULT FROM A SETUP THAT
LACKED THE CONDITION -- and correcting it found a P2. The fixture was said to put
Product on Axis::None in one view and on Column in another. It did not:
add_category defaults to Column and nothing moved it, so all three views had
none_cats {_Index, _Dim} and the stated input to the hazard was absent.
Setting it explicitly failed the property on the first run, and not on a cell
value: Second loses BOTH Total columns when the host stands on Default.
rebuild_layout fills Model::formula_cache for the HOST's view, and grid_in on the
elsewhere path builds another view's layout against that stale cache -- the Total
cells are keyed without a Product coordinate, the other view asks for one, finds
nothing, and prune_empty drops the columns. Filed improvise-ac2z (P2). Not local:
grid_in takes &App and recompute_formulas needs &mut Model.
Guarded by a_hosts_none_cats_leak_into_another_views_formula_columns, a
characterization test whose message says that failing means the bug is fixed. The
property skips pairs whose none_cats differ and asserts compared > 0, so a future
fixture cannot skip everything and pass.
3. Smaller, all taken. "Nothing else fails" on the t6db injection was wrong: it
fails three tests, including the pre-existing prune-invalidation one -- the #78
fix had more coverage than it was credited with. And the 5ubs should_panic named
one thing and exercised another: pointed at a struct it reached only the
split_once branch, and all three panics shared one literal so `expected` could not
discriminate. The parse is now a `parse_expected` seam fed hand-written mutated
messages, each refusal with its own wording, plus a happy path.
1470 -> 1475 passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
a10e95eb5f |
fix(mode): settle Normal/RecordsNormal once, after the rebuild (review of #91)
Two blockers from the adversarial review. BLOCKER 2, a new wrong state this PR introduced: resolving `AppMode::Normal` per-effect reads the batch's OPENING layout (gotcha 23), so a batch that CROSSES — `:toggle-records-mode . enter-mode normal` — settled against the grid it started on and left `RecordsNormal` over a pivot grid. Base was correct there. The fix moves the resolution to one place, `App::settle_normal_mode` called after `rebuild_layout`, where there is no crossing case to get wrong. That is strictly simpler than what it replaces: `ChangeMode` is an ordinary `simple_effect!` again, `ReturnToNormalUnlessMoved` and `CloseWizard` write plain `Normal`, and the per-edit coverage question the review filed as finding 3 is gone because there is one edit. Removing its call site fails exactly three tests, workspace-wide. And the prose defended the staleness with an example that does not bear the weight: `:toggle-records-mode` works, but because `ToggleRecordsMode` and `ViewBack` overwrite the answer in both directions, so it exercises nothing. Replaced by a test that drives the crossing both ways. BLOCKER 1, a false claim in `context/`: "keys.rs emits no ChangeMode" is true and irrelevant — keys.rs holds only `BrowserKey`. The live browser key resolver is `improvise_protocol::resolve`, which emits `Command::ChangeMode(Normal)` in five places, and `reduce.rs` folds it with no resolution. The browser front end still has the sihr bug; it is LIVE, not latent, and it compounds because the client's only records-ness fact is the mode it just lost. Named honestly, with improvise-e5pf cited. Still not closeable here — a client holds a `GridCache`, not a `GridLayout`. Also the two corrections: the thpe test's doc comment no longer claims a discrimination it does not have, and the 2ud deferral notes that `registry.rs`'s `visible_cols: 8` is production, not a test helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
46c3cfa158 |
docs: two things self-review turned up
The lockstep fixtures no longer fabricate a basis, but they do still assume a cursor at the origin: published_basis asks Sessions::connect for a window at offsets zero while client_of builds its grid at the cursor's offsets. They coincide in every fixture here. Recorded rather than closed. And the keypress path got improvise-jmjz's fix for free: compute_col_widths is one walk now rather than two, so App::apply_effects' own width computation roughly halved (134ms to 68ms at 18,000 cells). That is improvise-748f's held-key path and was not the ticket's subject. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
a449149998 |
docs: cite the two follow-up beads filed for this wave
improvise-6nsd (a prune-by-write renumbers the grid without moving layout_version) and improvise-26dm (the fourth length-not-completeness list, in ui/effect.rs). Both read back after bd create. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
ae1570b508 |
docs(command): every reframe is NOT also a reshape, measured (improvise-9v0j)
9v0j asks for the general relation between view_frame and view_shape, which are separated by example and never stated as a property. One direction is the known asymmetry -- AddCategory of an empty category reshapes without reframing. The other was open, and the ticket guessed "probably yes, and if not, layout_version under-reports, which per gotcha 35 leaves a client wrong forever". Probed by driving each ModelCommand through apply_command and reading cursor_epoch() and layout_version(). Every command in reshaping_commands() moves both; a ClearCell that prunes a row out of a pruning view moves the cursor epoch and NOT layout_version. That is improvise-mcq4 working as designed -- a cell write changes data, not the view's configuration, and view_shape hashes configuration. So the proposed property test would FAIL and writing it would be wrong. What remains open is narrower and stated as a question rather than answered: the grid has renumbered while layout_version has not moved, so a client still claiming the old layout tier is accepted. The record tier covers a write to the pruned record; it says nothing about a write to a surviving one whose row index moved. Whether that is a hole in ADR 001's middle tier or correctly delegated is an ADR question. No code change: view_frame/view_shape live in ui/app.rs, which is another wave's ground this round. Also recorded: the probe needed the right fixture to say anything. Against app_with_two_pruning_views, seven of the twelve commands are no-ops because that document has no Product category, and a table of false/false rows reads exactly like a finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
822547a332 |
perf(project): one walk of the grid instead of three (improvise-jmjz)
grid_of evaluated the whole grid roughly three times per frame: compute_col_widths walked every cell for its text, then every cell again for the column totals it must size, and grid_of then evaluated the window a third time. Every result was discarded at the end of the frame. ui/viewport.rs::grid_body is now the one walk. It evaluates each cell once and derives all three answers from that value -- widen the column, add to the column's sum, and if the position is inside the requested window keep the CellValue for cell_display to dress. compute_col_widths survives as grid_body with an empty window, because the keypress path wants widths and no cells, and two width rules would be the disease improvise-edp names. MEASURED, orchestrator's machine, cargo bench --bench grid_projection, before and after: typical (400 cells) 1.840ms -> 0.687ms large (9,600) 172.6ms -> 50.0ms 18k (gotcha 41) 294.4ms -> 95.2ms pathological (90,000) 4.481s -> 1.320s About 3x, which is the number of passes removed -- the shape the acceptance criterion asked for rather than a speedup argued from the structure. Three things the ticket did not predict. The benchmark's col_widths column now measures something ELSE. It read 45-47% and now reads 64-82%, which looks like a regression and is the opposite: compute_col_widths IS the fused walk, so its share rose while the frame fell by two thirds. A ratio whose denominator moved is not comparable across the change, and the benchmark now says so. The windowing ratio did not improve, and that is correct: the pass that remains is exactly the one a viewport cannot narrow. improvise-i5tk's decline stands and the two findings are now cleanly separated. It is not solved, only three times cheaper -- 18k cells is still 95ms, 5.7 frames. The next lever is caching ACROSS frames. Guard: a_window_narrows_the_cells_and_neither_the_totals_nor_the_widths. Fusing three consumers into one walk creates a hazard the separate passes did not have, and the natural "optimisation" (iterate rows rather than 0..row_count) makes a column total the sum of the visible rows, silently. Watched fail with left: [1.0] against right: [300021.0]. All 1469 pre-existing tests unchanged, which is also the evidence the outputs did not move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
c186909de6 |
docs: the review's two findings on this branch
`Command::ChangeMode(AppMode::Normal)` is on the wire and `reduce_view` folds it without the resolution, so a client that emitted it would keep improvise-sihr's behaviour. Not live — `keys.rs` emits no `ChangeMode`, and `apply_command` never folds a view-half command into the host's own slice — and not closeable here: a client holds a `GridCache`, not a `GridLayout`, so records-ness is not a fact it has. Named as a gap, with the shape that would close it. And the improvise-9c9o re-base costs one extra `view_frame` per batch that both switches and moves geometry, which the prose now states rather than leaving for the next person to measure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
8e9842b89f |
fix(cursor): the frame basis follows active_view, not the next geometry effect (improvise-9c9o)
PR #78 round 2's `was == self.view_name()` guard fixed a spurious invalidation and introduced its mirror: the first geometry effect after a mid-batch `SwitchView` skipped the check entirely, because there was no basis for the new view yet, so the code re-based and skipped. Under-reporting, which gotcha 35 says is the expensive side — a stale cursor has nothing in the stream to correct it. The re-base now happens at the switch, in the loop, as soon as the name moves, so by the time the geometry arm runs the name and the basis agree by construction and the guard is gone as unreachable. `SwitchView` stays `ViewOnly`: the basis is `App`'s own bookkeeping and none of it is on the wire, so this is not the protocol change the ticket feared. Also deletes a fixture line in the sibling test that was a complete no-op — `views["Second"].clone_into(&mut views["Default"].clone())` clones into a temporary that is dropped — and replaces it with the assertion it was pretending to be. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
d9cb16c397 |
Review round 2: the narrowing bought its speed with a correctness hole
Merge of `gh/main` at
|
||
|
|
bcccfdef6a |
Harden the fixture that made improvise-mcq4 visible, after merging #81
Merge of `gh/main` at
|
||
|
|
a808c3d7fd |
Merge #76: one cursor mechanism, not two, and the cost that exposes
`main` moved to #76 (improvise-748f, improvise-7kvw) while this branch was in
review. #76 approaches cursor invalidation from the per-effect side; this
branch (improvise-ni6k) replaced that table with an observation. The merge is
the interesting part.
TWO CONFLICTS, both resolved keeping both sides:
- `document_content` (#76's new O(cells) hash for the CellData arm) lands
beside `document_config`, which stays `pub(crate)` for improvise-1xf4's test.
- Three pre-loop bases now: #76's `content_basis` (writes_cells) and
`config_basis` (moves_geometry, their narrower gate), plus this branch's
`frame_basis`. Mine stays on `touches_model` and the comment says why: a
`CellData` write under `prune_empty` reframes.
THE ONE THAT DID NOT CONFLICT IS THE ONE THAT MATTERED. `effect.rs` auto-merged
clean and left #76's new per-effect `SetPageSelection` reset_cursor standing
beside the observation — two live mechanisms, 1435 tests green, nothing failing.
That is the failure mode, not a merge error. The stray is removed; one mechanism
remains, and `reset_cursor` now has exactly one caller in the crate.
#76's OWN TESTS ARE NOW THE GUARD, which is the outcome to want:
`a_page_flip_under_prune_empty_invalidates_the_cursor` and its negative twin
both pass against the observation with the per-effect call deleted, and
disabling the observation fails the positive arm while the negative one still
passes. So the observation subsumes the per-effect fix rather than merely
coexisting with it.
A MEASURED REGRESSION, FILED AS improvise-mcq4, NOT HIDDEN. 748f exists to keep
a no-cell batch off an O(cells) walk. `view_frame` builds through
`with_frozen_records`, which applies `prune_empty`, which walks cells. One
`CycleAxis` batch with pruning on, fingerprint off vs on: 3.0ms -> 12.4ms at
18,000 cells and 28.5ms -> 91.7ms at 90,000 — 3-4x, crossing the 16.7ms budget
where the tree before it sat at 3.0ms.
It was nearly missed twice, both times by a fixture that could not reach the
expensive branch: the SetCell bench shows +2-5% because #76's own walk dominates
that arm, and the geometry bench, written first against the existing fixture,
had `prune_empty` OFF and read 17-176us. `benches/reshape_observation.rs` now
carries the held-key case with pruning ON, and context/code/command.md carries
the argument.
Test function names diffed BY NAME across the merge, whole workspace including
harness/: nothing lost from either side. The seven present at
|
||
|
|
6f98bfd995 |
Review round 1: green CI, the SortData hole reopened, the paging regression removed
Four findings from the adversarial review of #78. Two blocked; all four are
real and one of them was a straight error on my part.
1. CI WAS RED AND I NEVER RAN THE CHECKS. Nine comment runs over the
two-line cap, all mine, all introduced this PR; both checks exit 0 at
|
||
|
|
14757a753f |
fix(command): the observation goes in the loop, or Save cannot clear it
Review of PR #70 found a regression the 1400-test suite did not name. `observe_reshape` ran AFTER the effect loop and re-raised model_state.dirty from the document_config comparison, while `Save` clears it INSIDE the loop. So gotcha 45's ordering property -- "Save clears it in loop order, so a batch that edits and then saves ends clean" -- survived for CellData/WholeWorkbook and was broken for LayoutGeometry. [SetNumberFormat, Save] -> reported a successful save, document dirty [SetCell, Save] -> fine on both Reachable from `:format ,.2f . w` and from any Binding::Sequence pairing a geometry effect with a save. The TUI draws "Saved to ..." while :q refuses; the worker, whose persist trigger IS the flag, pays a whole-document OPFS rewrite. The exact failure class this PR set out to close, one level up. The fix is position, not logic: the comparison is taken after each LayoutGeometry effect against the value held after the previous one. `layout_version` stays after the loop, which is not the same hazard -- nothing clears it, and an aborted batch's effects never ran, so an end comparison sees only what did. Neither arm of that is tested; filed. Two more review findings: a_panel_command_writes_that_panel_and_no_other asserted THROUGH the mapping it tests -- zeroing via panel_open_mut and reading via panel_open, the same four accessors. A consistent permutation of all four is a self-consistent bijection and passed. It now reads the three field pairs by name once per panel, and the consistent permutation fails it. It had been catching only a non-injective mapping, which is precisely the injection the evidence recorded. And improvise-56ek's claim of "no second copy left to agree with" was false when written: resolve.rs carried a fourth copy of the same Panel -> ViewState match, in the same crate on the same type. Deleted; its three call sites go through ViewState::panel_open. The claim is corrected rather than overstated -- registry.rs maps Panel to CmdContext fields, a different type, and the Panel -> (cursor, length) pairs are a different derivation and lockstep's subject. 1401 tests pass. Both new guards proved by injection: reinstating the after-loop comparison fails the ordering test by name with 454 others passing; a consistent Formula<->Category permutation across all four accessors fails the panel test, where before it did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
beef2587ad |
fix(command): a counter is bumped on an observed change, not a declared intent
`EffectKind` says what an effect *means* to do. `layout_version` and
`model_state.dirty` were believing it, and failed in both directions.
improvise-pdvm: AddItem/AddItemInGroup/RemoveItem declare CellData and
reshape the grid. Since improvise-9mo9 they call reset_cursor, so
cursor_epoch moved while layout_version did not -- one event moving one of
two counters that answer the same question, and a client told its cursor is
invalid while its layout basis tier still reads current.
improvise-75ld: a refused AddCategory (MAX_CATEGORIES) and a *duplicate* one
both bumped layout_version and raised dirty. The duplicate is the reachable
silent case -- Model::add_category returns Ok for a name it already has, so
it does not even abort -- and it refused every other client's next batch with
Rejection::LayoutMoved and made the worker rewrite the whole OPFS document.
Both are answered the way record_versions and cursor_epoch already are:
observe. Two fingerprints are taken around a batch that contains any
EffectKind::Model(_).
view_shape one view's DRAWN shape (axes, effective items minus this
view's hidden ones, collapsed groups with something drawn
under them, page selection, prune_empty). Per view name, so
it answers both "did anything move" and "which views moved".
document_config everything a LayoutGeometry effect can write that the
file would carry. Raises dirty. A different hash on purpose:
a number format and a hidden ghost are bytes on disk and not
shape.
No `Effect::kind()` declaration changed, so Dirty::of (gotcha 35) and
record_versions (gotcha 40) are untouched. What moved is which reader
believes the declaration: CellData and WholeWorkbook still raise dirty off
it, in an exhaustive match so a fourth ModelChange must answer for itself.
improvise-nynk: Sessions::adopt_cursor_epoch reset every mirror and returned
one Broadcast::ToAll, which improvise-jytm made wrong -- a reshape of view A
threw away the cursor of a client on view B. The trigger stays the epoch
moving (gotcha 24); only the audience narrows, to the observed reshaped-view
set, as one Broadcast::To per affected client. An empty set means "not
known", never "nobody", and cursor_invalidated is a named function with both
arms tested so that fail-safe cannot be tidied away.
improvise-56ek: SetPanelOpen and SetPanelCursor matched a Panel to a field
twice, in two crates. One home now -- ViewState::panel_open_mut /
panel_cursor_mut and their read halves -- and no compatibility test between
them, because there is no second copy to agree with.
1400 tests pass (baseline 1394: +7 guards, -1 replaced). Every guard proved
by injecting the fault it names; the injections and what each one broke are
in context/test-evidence/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
db68b0ee5f |
Merge main into docs/orchestrator-doc-pass
Two conflicts. .beads/issues.jsonl restored to main's export so the branch carries no beads diff. context/code/command.md: main rewrote the Sessions::apply paragraph (now #[cfg(test)], improvise-s17o), which supersedes this branch's 'verified at this commit' note; main's text is kept, minus its closing pointer telling the reader to read design-principles.md §5's 'Sessions::apply' as 'apply_checked', because this branch corrects §5 itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3fe4da52b0 |
Merge pull request #49 from fiddlerwoaroof/wave/n-cleanups
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Browser bundle + static site (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
wave 2 / WU-N: cursor invalidation, the reduce_full deletion, and two declines with evidence |
||
|
|
e176a29e75 |
fix: the no-op guards must ask what the grid draws, not what the model stores
Second review round. Six accepted-but-no-op commands still reset the cursor and
bumped the epoch; five are fixed here and the sixth is filed.
The one to remember is AddItem { _Measure, <a formula target> }. The guard read
category.items, but _Measure's effective items are explicit UNION formula targets
(gotcha 7), so it called "new" a column the grid was already drawing. The three
item effects now compare effective_item_names ACROSS the mutation rather than
predicting from one side of it, which is correct in both directions at once and
needs no separate rule for dedupe, for formula targets, or for an unknown
category. Prediction is what was wrong; observation is the fix.
Four smaller ones: SetAxis on a category the view does not register (set_axis
no-ops, but try_axis_of returns None, which compares unequal to Some(axis));
HideItem/ShowItem naming something the grid never drew (hide_item records any
string); and ToggleGroup on a group that does not exist, which never got a guard
at all because it merged CLEANLY through the #48 merge -- the clean-merge hazard
arriving in the one place nobody re-read.
TogglePruneEmpty on a grid with nothing empty is NOT fixed. Guarding it means
computing the pruned layout, which is building the frame improvise-ni6k proposes
fingerprinting. It is one keystroke in ordinary use, so it is filed loudly rather
than patched quietly.
The fixture could not reach any of these by construction -- no formula, no
category on Axis::None, no unknown-name case: the one-category-fixture shape the
doctrine names. It now carries a Profit formula, a Channel category on Axis::None
and a Ghost entry hidden under a name the model never had.
Two of the five injections initially failed to reproduce, and BOTH TIMES THE
INJECTION WAS WRONG RATHER THAN THE GUARD: ShowItem needed a hidden ghost name,
since is_hidden is false for a name nobody hid; and AddItem had to compute its
predicate BEFORE the add, because afterwards the item really is in category.items.
An injection that does not reproduce the original ordering tests nothing.
ToggleGroup's first guard was also wrong -- add_item_in_group sets item.group
without pushing to Category::groups -- and the existing reshape test caught it.
All nine earlier injections re-run and still caught.
Filed: improvise-nynk (adopt_cursor_epoch resets clients on views nothing
reshaped -- bisected to main/#48, but this branch takes it from 2 effects to 12),
and TogglePruneEmpty plus the Axis::None class onto improvise-ni6k.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
850ddcf0eb |
Merge gh/main (#48 active_view, #50 shared front end) into wave/b-projection-gaps
Eight conflicts, all resolved keeping both sides. The three in context/ were append-vs-append. The five in code were the restructure #50 declared in advance: - render.rs: render_model became a free fn over &ClientState and the mode label moved to improvise_protocol::mode_label. Took that shape and re-added the three doc_* fields. - state.rs -> improvise-protocol/src/client.rs: only the import path conflicted. The file now lives IN improvise-protocol, so `crate::` is right, plus DocumentCache. - keys.rs went 1387 -> 187 lines because resolve/step moved to improvise-protocol/src/resolve.rs. Took gh/main's slim file and ported every addition into resolve.rs: the Derived and Create arms of edit_or_drill, the or-patterns in record_at_cursor and record_cell_at_cursor, page_cycle with its Odometer, the two bindings, and `[`/`]` leaving the still-needs-the-host list. - project.rs: kept the value-based numeric/error from improvise-tom1 with gh/main's cell_id(app, layout, ..) signature; kept BOTH document_update and view_in; kept our title-bar projection inside their per-view projection set. - lockstep.rs: both sides appended tests and the hunks interleaved mid-function, so took gh/main's file and re-appended our page-turn block whole. THE CLEAN MERGE WAS THE DANGEROUS ONE, exactly as warned. `cargo check --workspace --all-targets` passed with three of our tests SILENTLY GONE — they lived in keys.rs's test module, which moved wholesale. i_on_a_derived_cell_names_ the_formula, the_page_keys_turn_the_odometer_as_the_command_does and a_filter_axis_is_projected_but_not_cycled are ported into resolve.rs, with client_on now returning a ClientState rather than a Client. One assertion changed meaning under us and is corrected: lockstep's page_selections read workbook.active_view(), which since improvise-jytm is the document's DEFAULT. SetPageSelection writes App::view(), the client's. They coincide in a one-view fixture, so it was passing by fixture luck; it now reads App::view() and still fails under the injection. All four injections re-run on the merged code and all four still discriminate: reversed odometer, dropped records formula_for lookup, title bar gated on Dirty, and filtering_cats leaking into the protocol. 1348 passing, clippy clean, fmt clean, all eight wasm crates clean, both comment-run checks clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a6258f7c50 |
Merge gh/main (PRs #48, #50) into wave/n-cleanups
Two conflicts, both in ui/effect.rs and ui/reduce.rs, both resolved by keeping BOTH sides rather than either. #48 introduced App::view_mut() as shorthand for model_state.workbook.active_view_mut(). SetAxis, HideItem and ShowItem conflicted because this branch had just given each a no-op guard (improvise-9mo9 review, F1). Resolution keeps the guards, expressed through the new accessor. ToggleGroup merged cleanly and kept its reset, which is the case worth noting: git had no reason to flag it. #50 moved records_named into improvise-protocol and re-exports it from ui/reduce.rs, and re-added reduce_view to the import list. reduce_view's last use in this file was deleted by improvise-s17o on this branch -- apply_command's fallback arm no longer folds -- so the resolution keeps the re-export and drops the now-dead import. A CLEAN MERGE IS THE DANGEROUS ONE, so all nine fault injections were re-run against the merged tree rather than trusting a green suite: SetAxis's moved check, AddItem's fresh check, AddCategory's Err arm, SetAxis's reset removed, SetNumberFormat gaining a reset (the negative arm), the ShowItem non-vacuity guard, reset_cursor clearing the whole cursor map, the CommitAndAdvance advance ordering, and apply_command folding again. All nine still caught, and the tree restores byte-identical. That matters most for the cursor ones, since #48 changed which view a session is projected against. 1333 passing (1328 from main plus this branch's 5), clippy and fmt clean, both comment-run checker modes clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ed412a0256 |
fix: review round — the Ok-but-no-op resets, the measure regression, and a
loop-of-one test F1 (HIGH). Gating the reset on Ok is not the same as gating it on "something changed". Six accepted commands changed nothing and reset every cursor anyway: SetAxis to the axis already held, HideItem on an already-hidden item, ShowItem on a never-hidden one, AddItem of an existing item (Category::add_item dedupes and returns Ok), and RemoveItem/RemoveCategory of something absent. Each now asks the state before mutating. The case that made it urgent needs no second client: TileSelect binds `r` to set-axis-for-tile row, which emits SetAxis with no comparison against the current axis, so `r` on a Row category sent the user's own cursor to the origin. F2 (REGRESSION this PR introduced). Naming a measure in the trailing `+` column at the bottom-right of a records grid sent the cursor home: AddItem now resets, inserts_a_column matches only NewCategory, and EnterAdvance at the bottom-right returns AbortChain with no SetSelected. The restore moves to BEFORE the advance, where an aborting advance cannot skip it -- gotcha 21's rule applied to the cursor rather than the mode -- which also subsumes the +cat arm's copy. F3. a_refused_model_command_invalidates_no_cursor named a class and tested one member; injecting reset_cursor into AddCategory's Err arm left it green. Now a table over all three refusable effects, and that injection fails it by name. F5. The benchmark could not reproduce its own figures: it took the mean over a few iterations, and review measured the same share at 8% and at 131% -- a part slower than the whole containing it, printed flat. It also passed (false, 2) where grid_of passes the view's (true, 0), so it timed a call that never happens. Now min-of-N with the view's own format, and a share above 100% prints UNSTABLE. Figures are stable across runs at ratio 1.2-1.5x, col_widths 45-49%. F6. The drg counts omitted the 8 simple_effect! invocations and misclassified AddItem at base through a regex that cannot see `app` newline `.model_state` -- doctrine(derive_then_verify) catching the person citing it. Correct: 57 effects, narrow set 32 -> 25, not 25 -> 17 of 49. The direction holds and is still the argument; the headline was overstated by about half again. Corrected in context/code/command.md and on the bead, not quietly restated. F9. a_model_command_that_leaves_the_active_view_alone_broadcasts_nothing asserts a ResetCursor broadcast, so the name read as a lie: ..._broadcasts_no_switch. F4 filed as improvise-pdvm: AddItem/RemoveItem declare CellData so layout_version does not bump while cursor_epoch does, and this PR's own reshape assertion is the first evidence for it. Related to improvise-75ld, same question other direction. Faults injected for every new guard: SetAxis's moved check, AddItem's fresh check, AddCategory's Err arm, and the pre-fix advance ordering ((0,0) vs (0,3)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7368467ce9 |
test: prove the drill's cursor ordering instead of arguing it
improvise-9mo9 made SetAxis reset the cursor, and DrillIntoCell issues eight or
so SetAxis effects. The PR claimed that was safe and did not test it: the claim
was that SwitchView precedes the batch, so reset_cursor writes the ACTIVE view's
cursor -- by then the freshly created _Drill one -- while the view drilled from
keeps its entry in ViewState::cursors. That is an ordering argument about effects
inside a batch, which is the class of claim gotcha 23 exists because people get
wrong, and I had flagged it to the reviewer as unverified.
It holds. Two faults injected: removing SwitchView from the batch fails the
non-vacuity guard ("the drill did not switch views, so this proves nothing"), and
making reset_cursor clear the whole cursors map rather than the active entry
fails on the origin view's cursor (left: (0,0), right: (1,1)).
Both sides are asserted on purpose. That _Drill opens at its origin is half the
property; that the other view keeps (1,1) and row_offset 1 is the half that would
have broken, and a test asserting only the first passes against an emptied map.
The cursor_epoch half stays argued and now says so in context/code/command.md: a
drill bumps it once per SetAxis, adopt_cursor_epoch compares before and after per
command so it relays once rather than eight times, and DrillIntoCell is key-path
only today, so no client sees it. That changes when drill becomes addressable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
942e690601 |
test(active-view): close the three guards review found missing
All three were demonstrated by injection against a green suite — this repo's characteristic defect, and the PR's own evidence table asserted coverage it did not have. - `ClientSession::view_in` had no guard: pointing it at the host's view left 1313 tests green. Only `grid_in` was checked; `category_tree_in` and `tile_bar_in` were not, and the tile bar is the control a user reassigns axes with. `a_session_on_another_view_is_projected_that_views_axes` closes it. - `adopt_deleted_views` and `apply_switch` had vacuous audience assertions: both bystanders sat on the document default, where a wrongly-addressed relay folds to a no-op. Both now sit on a view they can be moved off, and both read the audience off the `Broadcast` rather than inferring it from folded state. - `ViewBack`/`ViewForward`'s `EffectKind` had no guard — reclassifying either was silent. `walking_the_view_history_moves_no_document_state` asserts the consequences (`dirty`, `layout_version`) rather than the declaration. Four follow-ups filed: improvise-w3bf, improvise-pubt, improvise-h074, and the reachability trigger appended to improvise-lltt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
233a5cc635 |
fix(core): one resolution of a records cell, so the producer stops guessing
improvise-tom1. The user has ruled that a remote client must NOT see another client's staged drill edits, so the invisibility is correct and was never the defect. What remained was that three places answered "what text is in this cell" — and only two of them still do. GridLayout::records_value is now the resolution: it returns the CellValue, and records_display is one to_string over it, so the text can never disagree with the value it spells. project::cell_display used to call records_display and then GUESS numeric-ness with text.parse::<f64>() — the one place the producer guessed rather than knew, and it was wrong: a records coordinate column holds an item NAME, so a Year category with items 2024/2025 projected a coordinate the browser right-aligns as a number. Nothing in the suite could see it, because every records fixture in the tree names its items Region/East/North. A correction to the ticket worth more than the code: src/ui/grid.rs is NOT a third answer. Since improvise-anr9 the TUI grid takes its text off GridCache and then overlays drill_state.pending_edits — one derivation plus a session-local overlay, which is exactly what the ruling wants, and App::display_value_of does the same for the editor. The ticket was written before anr9 landed. Both halves of the ruling are now asserted, with the leak injected as the fault: teaching the producer to prefer pending_edits, which is the plausible "fix" the ticket exists to forbid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fb41255c49 |
docs: quote the benchmark as ranges over two runs, not one run's numbers
A second run of benches/grid_projection put the 'large' row's col_widths share at 45% where the first said 64%. 45-46% is what reproduces; 64% was noise, and a single-run number written down as a fact is the same defect as an argument written down as a measurement -- which is what the benchmark existed to avoid. What does not move between runs is what the decision rests on: ratio stays 1.2-1.5x and col_widths stays at roughly half the frame. improvise-jmjz's title and notes corrected to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
72b0d09119 |
docs: finish the reduce_full deletion in the prose that outlived it
Self-review caught three references the deletion commit missed, which is gotcha 48's rule arriving on schedule: when a fact is stated in more than one place, changing it is a grep, not an edit. - context/code/command.md still had a `### reduce_full / reduce_key` section calling it "the signature the three hosts share" -- the exact claim the ticket was filed against, two hundred lines below the paragraph that corrects it. - ADR 001 stated in the present tense that apply_command/reduce_full do not check, and recorded improvise-ondj giving both a Checked signature without saying that one of them has since gone. The ADR now says why deleting beat typing: having no caller was the condition that let the defect hide, and a Checked on a function nobody calls fixes the type and not that. - The test-evidence entry's "no longer compiles" is now "no longer exists". Also records the AddItem/AddItemInGroup success gating and the four effects that reset unconditionally because they cannot report a miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
93f795291e |
fix: a refused add must not invalidate anyone's cursor
Self-review of improvise-9mo9's own commit. AddCategory was gated on success and AddItem/AddItemInGroup were not: the reset sat after the `if let Err`, so a typo'd category name bumped cursor_epoch and broadcast Command::ResetCursor to every client for a change the document never made. The epoch is document-wide, so the cost was paid by everyone. SetNumberFormat's negative arm cannot see this -- it tests a command that succeeds and reshapes nothing, where this is one that reshapes nothing because it was refused. a_refused_model_command_invalidates_no_cursor is the second arm, proved by moving the reset back outside the match and watching it fail on the bystander's cursor. Also drops apply_command's now-dead `Command::Project(_)` arm: since the fallback stopped folding, both returned Dirty::NOTHING and the first was unreachable duplication. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3d8ea00872 |
docs: decline improvise-drg's Effect::apply narrowing, with counts
improvise-drg proposed narrowing each Effect::apply to the slice it touches. It was filed with three unpicked mechanisms and a note to decide before claiming. Declined, and the counts are the argument. Over the 49 impl Effect blocks: 8 touch only view_state, 9 only model_state, 32 cross-cut. At this wave's base commit the split was 9 / 16 / 24, so the narrow set fell from 25 to 17 in one wave -- and the mover was improvise-9mo9, earlier on this branch, which made nine reshaping effects call app.reset_cursor(). That is architectural, not incidental: a reshape lives in model_state.workbook and the cursor it invalidates lives in ViewState, so the effects that matter most are the ones that reconcile the two slices. Three more grounds. The type-level documentation already exists and is checked -- Effect::kind() has no default and apply_effects reads that one answer three times -- so a narrowed signature would be a second statement of the same fact, free to disagree. Every offered mechanism weakens a guard: three traits need three queues (destroying batch order, which gotchas 21/23/43 lean on) or an enum wrapper (a fourth classification beside EffectKind), and default methods give `apply` a default whose wrong answer is silent, which is exactly what kind()'s no-default rule exists to prevent. And finer-grained effect tests are not blocked: the 64 tests in effect.rs already drive apply against a one-line App fixture. Approach (B) -- rebinding the slice at the top of a body -- stays available as a per-function style choice and is not mandated. The bead carries the `human` label, so this is recorded as a recommendation with its evidence rather than settled unilaterally. Closes improvise-drg Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
69fe27e1d6 |
fix(active-view): close the deleted-view hole and unify view resolution
Two findings from running the code-review skill on this branch. `Workbook::delete_view` used to move the document's active view and `adopt_active_view` pushed the result into every mirror, so deleting the view a client was on moved that client. The split removed both, leaving that client naming a view the document has not got — projected the default's grid while its own slice says otherwise. `Sessions::adopt_deleted_views` answers it, observed rather than predicted: the check is whether a mirror still names a view the document has, not whether the command was a `DeleteView`. That needs a one-session audience that is not the asker, so `Broadcast::ToAsker` becomes `Broadcast::To(SessionId, Command)` and `Host::relay` reads the address off the message rather than taking `from`. And "what does this name name in this document" was answered twice — in `App::view_name` and again inside `project::grid_in`. `App::resolve_view` is now the one home both go through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
10cebfffd9 |
docs(active-view): record the split, its evidence, and the two gaps it opens
`context/code/command.md` gains the derivation for improvise-jytm: what moved, what projects per client, why `effect::SwitchView` is `ViewOnly`, and why `adopt_active_view`/`Broadcast::ToOthers` are deleted rather than left. `context/test-evidence/command.md` and `clients.md` follow the tests that were replaced, each entry naming the fault injected to prove the new check can fail. Two gaps filed rather than smuggled in: improvise-lltt (a view-scoped `ModelCommand` names no view) and improvise-azhb (`Initial View` no longer follows the TUI user). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc2f4a06ad |
docs: complete the corrections, per adversarial review of this PR
Four findings, all verified against the tree before acting.
F10 — this PR's own edit to design-principles §5 made a sibling note in
context/code/command.md false: it said §5 "still names Sessions::apply" and
recommended apply_checked. A PR whose whole subject is stale prose had left a
new stale copy behind, which is gotcha 48 exactly ("changing it is a grep, not
an edit"). Note deleted; the verified claim above it stands.
F11 — §5 named both apply and apply_checked as "the one door". apply has no
production caller anywhere in the workspace; Host calls apply_checked. Naming
two weakens the claim §5 makes, so it now names the one production uses.
F12 — gotcha 17 said "four call sites". There are six uses across the two
evaluators: a div/0 guard plus the Eq and Ne arms in each. In a file agents
follow literally, a reader who greps and finds six cannot tell whether the
prose or the code moved.
F13 — gotcha 17's sibling said the Dirty::of doc comment "keeps only the
one-line reason LayoutGeometry is spelled out rather than EVERYTHING". It says
no such thing: it points at context/code/command.md, and the EVERYTHING reason
is a two-line comment inside the match body. A correction to prose that
asserted the negation of the code should not itself misdescribe the code.
Two findings deliberately not fixed here and filed instead: improvise-ahdf
(repo-map gotchas 36 and 43 carry the same stale Sessions::apply claim —
pre-existing, and inside long paragraphs that need reading rather than a sed)
and improvise-rm4s (--check-evidence accepts headings naming tests that never
run).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5b7b53466f |
feat(active-view): split the document's default from each client's view
improvise-jytm, improvise-6c6q, improvise-k69y. `Workbook::active_view` becomes the document's *default* — what a freshly connected client opens and what `.improv` writes as `Initial View` — and `ViewState::active_view` becomes authoritative per client. Two clients may sit on different views of one document: the model is shared, the UI is not. - `App::switch_to_view` writes only the client's slice; `App::view()`/ `view_mut()`/`view_name()` resolve it against the workbook. - `effect::SwitchView`/`ViewBack`/`ViewForward` are `ViewOnly`, which takes `ViewBack`/`ViewForward` off `ModelCommand` — their operand is one client's history stack (the `ApplyAndClearDrill` argument). - `grid_in`/`category_tree_in`/`tile_bar_in` project against the session's view; `ClientSession` observes its own move through `projected_view`. - `apply_switch` takes a `SessionId`, moves only the asker's mirror, and relays `Broadcast::ToAsker` only when the document refused. `adopt_active_view` and `Broadcast::ToOthers` are deleted: nothing produces them any more. - `draw_status` reads the client's copy (improvise-k69y). `the_two_active_view_names_never_diverge` is replaced, not deleted, by `the_documents_default_view_outlives_this_clients_switches` and `a_fresh_session_opens_the_documents_default_view`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c8ebe3cae6 |
refactor: delete reduce_full and stop apply_command folding a client's view slice
ui/reduce.rs claimed every deployment mode reached the model through it. That was aspirational, and it is what let improvise-ondj's missing basis check sit unnoticed: reduce_full had no production caller anywhere in the workspace, so nothing exercised its Command::Model arm and nothing could notice it skipped the check. Deleted rather than wired up, on the argument in its own doc comment: "Per-client grid updates need a viewport and come from Sessions::project_all." It returned half of what a host needs. The browser worker was the natural candidate caller and reaches the model through Host::handle -> Sessions::apply_checked, which is the half that has the viewport. Its only caller was a test helper, which now spells the two lines it was. apply_command's fallback arm no longer folds. It routed a wire view command into app.view_state -- the authoritative slice -- which nothing in production did and nothing should: Sessions::apply_checked folds it into the asking client's mirror, because the model is shared and the UI is not. A client's SetStatus moving the host's status line was behaviour that was safe only because the arm was dead. view_slice_commands_land_in_the_authoritative_view_state pinned exactly that, so it is inverted rather than deleted, and its fixture writes a value into the status line first so the assertion can tell "the fold was removed" from "the field was empty anyway". Restoring the fold fails it. Sessions::apply is now #[cfg(test)]. Host::commands answers the whole frame with CheckedBatch::against, so a door that answers one command and then dispatches is the lost update improvise-955n fixed; cfg(test) makes "no production caller" a fact the compiler keeps rather than a note verified at one commit. Closes improvise-s17o Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0e193ec5c8 |
fix: reset the cursor in every effect that renumbers the grid
Only TransposeAxes and CycleAxis reset the cursor, while eight siblings that renumber just as thoroughly did not. SetAxis is the sharp case: CycleAxis *is* SetAxis to Axis::next(), so a pair differing only by an argument disagreed. reset_cursor is also the only writer of cursor_epoch, which is the only thing Sessions::adopt_cursor_epoch watches, so each gap was invisible in both directions at once: the host's own cursor stayed stale and no client was told. Gotcha 24 records that the second half is strictly worse than doing neither. SetAxis, HideItem, ShowItem, ToggleGroup, TogglePruneEmpty, AddCategory, RemoveCategory, AddItem, AddItemInGroup and RemoveItem now reset. The item effects are in because "an append cannot move an existing position" holds only of the outermost category on an axis: expand_category runs inside the cross product, so an item appended to an inner category repeats once per outer block. SetNumberFormat stays out and keeps the negative arm. CommitAndAdvance's +cat branch depended on the missing reset — it stayed put so the cursor would land on the column it had just created (improvise-2zq). It now says so with an explicit SetSelected, placed after AddCategory's abort so a refused category still leaves the batch alone. The new test asserts of every case that row_items/col_items really changed before asserting anything about a cursor; two faults were injected to show both arms discriminate. Reasoning in context/code/command.md, evidence in context/test-evidence/command.md. SetPageSelection under prune_empty is the one residual and is filed as improvise-7kvw. Closes improvise-9mo9 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
17ad0f1d1d |
docs(command): move command/keymap.rs and cmd/** arguments to context/
keymap.rs: the dispatch/registry-vs-table split (this file stays the authority on it) and the improvise-0hb Sequence-flattening decline, with its three findings. cmd/**: the two CommitAndAdvance orderings, CommitSave's mode-change-first, add_category's deliberate no-coordinate-write, EditOrDrill's mode-agnostic message choice, the SaveAndQuit and CreateAndSwitchView declines, and fit.rs's whole module doc including the measured wasm bundle cost. crates/improvise-command now reports no comment run over two lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
629f54919f |
docs(command): move ui/host.rs, viewport.rs and cat_tree.rs to context/
host.rs: the one-App-per-document argument, why ClientFrame::Key answers nothing, why Host::save bypasses Sessions, and the broadcast-before-projections ordering. The 955n evidence -- including arm 3's check-before-apply trade that clause 4 must pay back -- goes to the evidence file. viewport.rs/cat_tree.rs: the residue of two derivations that moved to improvise-protocol, and why each residue is the shape it is. ui/ now reports no comment run over two lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e9b800d7a9 |
docs(command): move ui/project.rs's arguments to context/
The diff-not-declare rule, Dirty::of's derivation (now recorded in context/code/command.md, which gotcha 35 should point at rather than the doc comment), axis_frame's three group-header edge rules, the Broadcast audience argument and Applied's three cases all move. Test narratives, including the injected faults for hkt6 and the renamed-axis case, go to the evidence file. One orphaned doc paragraph dropped: a struct doc describing a type that no longer exists at that site had fused with fn diff's. Noted in the evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
115b0bbb20 |
docs(command): move ui/app.rs's arguments to context/
The residue-tag taxonomy, the record-as-unit-of-atomicity argument, the process-local XOR fingerprint, and apply_effects reading dirtiness, layout_version and the record fingerprints off one EffectKind, go to context/code/command.md. Test narratives go to context/test-evidence/command.md. One comment was wrong rather than long: entering_records_mode_sorts_existing_data carried a stray unfinished first line from a neighbouring test. Dropped, and noted in the evidence entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cf8f4f9984 |
docs(command): move ui/effect.rs's arguments to context/
EffectKind's no-default rule and the simple_effect! mandatory-kind argument, the abort ordering Save/SaveAs/AddCategory rely on, and ReturnToNormalUnlessMoved's observe-don't-predict, go to context/code/command.md. Effect test bug-narratives go to context/test-evidence/command.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b2e69e85f3 |
docs(command): move ui/reduce.rs's arguments to context/
The Checked/CheckedBatch argument -- one basis per frame, checked once, two tiers hoist and the record tier cannot, and the create-exemption stated over a property -- goes to context/code/command.md. Test bug-narratives go to context/test-evidence/command.md, keyed by test name. The ```text fence illustrating the E0502 that killed the lifetime-tied Checked moves with its argument rather than being deleted; it was never a runnable doctest, only a block the checker exempts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |