Land the 2026-07-31 design-ruling sweep

Uncommitted ruling prose from the pro-m1 sessions, committed as found:
trash sorts newest-first by modified stamp (no rank minting); kind-blind
trash selection; native undo in every tier with the provider following
the board; session-coarsening for card-window stacks; column-major
masonry; changed-path channel (02); window-scoped comment-thread heals;
commit-message vocabulary growth; integrity commit author; signature
passed per-commit instead of repo config; repo-state validation
tightening; comments pane defaults on; draft close-failure guard;
deferred comment-trash purge; 2.0 ships only with pro-m1 (RELEASE.md).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-31 17:49:18 -04:00
parent b8667699ae
commit ade7d34cd8
14 changed files with 61 additions and 58 deletions
+4 -4
View File
@@ -69,7 +69,7 @@ Edge case: if a dead region is hovered with **no valid prior proposal** — a fr
## The card masonry (2D)
A lane lays its cards out with `MasonryLayout`: card `i` goes to interior column `i % columnCount`, and each column stacks its cards top-aligned and independently, with no row alignment across columns (03-board-ui.md § Lane). Card widths are uniform — the column width — and heights vary, so the grid is genuinely two-dimensional and the span-cap applies on the vertical axis.
A lane lays its cards out with `MasonryLayout`, dealing **column-major** (ruled 2026-07-31, replacing the pathfinder's round-robin): with `n` cards and `C` columns, `base = n / C` and the first `n % C` columns take one more, so column `c` holds the contiguous run `[start(c), start(c+1))` and logical order runs *down* each column before crossing to the next. Each column stacks its cards top-aligned and independently, with no row alignment across columns (03-board-ui.md § Lane). Card widths are uniform — the column width — and heights vary, so the grid is genuinely two-dimensional and the span-cap applies on the vertical axis.
The resting grid is **re-run, not measured**: `MasonryPlacement.frames(heights:)` replays the same placement over the lane's rendered cards minus the dragged ones, from `(columnCount, columnWidth, spacing, heights)`. `MasonryLayout` itself places subviews through that one function, so the resting grid a drag reasons about and the grid SwiftUI draws cannot drift apart.
@@ -78,10 +78,10 @@ The resting grid is **re-run, not measured**: `MasonryPlacement.frames(heights:)
Cursor → proposal, in three steps (`DropSlotMath.cardSlot`):
1. **Column.** The interior columns' x-bands tile the lane's card area — column `c` plus half a spacing on each side — and the cursor's band picks `c`. Outside the outermost bands the cursor clamps inward, so the lane's padding and its header target the nearest column rather than nothing. Exact-boundary ties keep the current proposal's column, as in 1D.
2. **Row.** Column `c`'s cards are logical indices `c, c + C, c + 2C, …`; their vertical extents feed the *same* 1D span-capped machinery the strip uses, with `draggedSpan` = the **first dragged card's frozen height** (the run's footprint at the landing spot; the remaining shadows stack below it, and the trigger rect that matters is the one the cursor is over). Dead regions hold, the tail slot below the column's last card is uncapped, and the region above the first card is uncapped.
3. **Logical index.** Column `c`, row `r` is logical position `r · C + c`, clamped to the card count. The clamp is the only place the arithmetic bends: every column's tail slot maps at or past the end, so "below the last card of any column" is the end slot — appending — which is the honest reading, since a round-robin masonry has no landing spot below one column that is not simply the end.
2. **Row.** Column `c`'s cards are the contiguous logical range `[start(c), start(c+1))`; their vertical extents feed the *same* 1D span-capped machinery the strip uses, with `draggedSpan` = the **first dragged card's frozen height** (the run's footprint at the landing spot; the remaining shadows stack below it, and the trigger rect that matters is the one the cursor is over). Dead regions hold, the tail slot below the column's last card is uncapped, and the region above the first card is uncapped.
3. **Logical index.** Column `c`, row `r` is logical position `start(c) + r` — no clamp needed, since `r` never exceeds the column's own card count. A non-final column's tail slot is a **genuine mid-list position** (`start(c+1)`, immediately before the next column's first card); only the last column's tail is the end slot — appending. This is a landing spot the round-robin deal could not offer, where every column's tail collapsed to the end.
The insertion index is therefore always a position in the lane's **logical card order**, which is what the store writes and what 10-accessibility.md's logical-order rule requires. A consequence worth stating out loud: because assignment to columns is round-robin, inserting at index `k` shifts every later card one position and therefore *across* columns. That reflow is the point — `MasonryLayout` is a `Layout` over a single `ForEach` precisely so those moves animate as positional slides rather than as remove/insert blinks.
The insertion index is therefore always a position in the lane's **logical card order**, which is what the store writes and what 10-accessibility.md's logical-order rule requires. Because the columns re-deal on every count change, inserting at index `k` slides later cards down within their columns and moves at most one card across each column boundary — far gentler than the round-robin deal this replaced, which sent every later card sideways. `MasonryLayout` is a `Layout` over a single `ForEach` precisely so the moves that do happen animate as positional slides rather than as remove/insert blinks. One presentation consequence of the re-deal, accepted with the ruling: a *tail* proposal's shadow draws at the head of the **next** column — exactly where the card will sit once the columns re-deal around it — so the shadow is not always directly under the cursor; the drop still lands exactly where the shadows show.
Everything else carries over unchanged: resting-layout reconstruction, boundary ties, own-slot no-op, uncapped terminal slots.