BoardRenderMetrics (DEBUG-only, the pathfinder's counter bag plus a
strip-body discriminator that tells a failed gate from a direct
Observation invalidation) counted at BoardView/LaneView/CardFaceView/
TrashLaneView bodies and MasonryLayout's callbacks. DragSignposts wraps
dropUpdated, retargetCards, commitDrop, and the commit-to-covering-
snapshot release pause; input latency reports honestly against
NSApp.currentEvent's mach base or labels itself base=none — no event
timestamp rides the drop path. BoardRenderPerformanceTests hosts the
real BoardView off-screen: a value-equal reload runs zero lane and zero
card bodies, a one-card edit repaints one card of 180. The lane-body
budget is <= laneCount with the headerInk chain documented and a
two-way tripwire that fails when the fix lands. Methodology in
RENDER-INSTRUMENTATION.md. Release build proves it all compiles out.
Drag-perf card a450ad09.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
RestingLayoutCache (session-scoped, @ObservationIgnored on DragSession,
cleared at begin and end) holds each lane's resting layout as ids +
heights — never [Card] — keyed on the hovered board's applied
snapshotGeneration, the registry's new heightsGeneration, the lane, and
the operation-following hidden set. The grid stays event-time on purpose
(autoscroll moves a lane's origin with the snapshot standing still), so
re-grounding rule 1 holds exactly: nothing survives a reload, only the
per-sample repetition goes. The steady-state cost of a hover is now the
containment scan plus four stores and a divide.
Drag-perf suspect #3, card b9f48fd1.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
`DragLocality.isSameBoard` resolved symlinks on both URLs, which stats every
path component. It ran five-plus times per `dropUpdated` and again per lane per
body evaluation through `renderedCards` → `hiddenMembers` — order of 50–100
stat calls per mouse-move, and worse on iCloud-backed paths. A pickup stall
sample had it on ~47 of 943 stacks.
`BoardRootKey` mints that canonical spelling once, where the root is, and every
locality comparison downstream is an `==` on two strings. The drag carriers hold
keys rather than URLs, so re-deriving under the cursor is no longer expressible.
`rootURL` keeps the user's spelling — the folder name is the display-name
fallback, and canonicalizing there would visibly rename a board opened through
a pin. The key follows the folder through `relocate(to:)`: a key frozen at open
would collide with a new board opened at the vacated path, and two boards
comparing as one is a cross-board drag silently behaving as a move.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
The loader gains a ParseMemo — the previous walk's parsed documents keyed
by root-relative path, trusted on the git-index heuristic (mtime + size,
no hashing) and passed as an input so the loader stays stateless. A hit
skips exactly one file read; schema, order, coercions, dedupe, and every
directory listing run fresh, so memoized and cold walks are output-
identical (golden-corpus equivalence suite). Entries record only past the
schema gate, so a defect can never be answered from the memo.
The store skips the snapshot assignment wholesale when the fresh model is
value-equal — no @Observable churn, no render pass, no snapshotGeneration
bump — and a new landedReloads counter carries walk-completion for the
three consumers whose subject is the walk, not the snapshot: the card
window's comment thread, the comment search index, and the auto-committer's
covering gate (which now counts a completed walk as covering even when
nothing changed). Warnings and defects move on their own equality; failed
reloads bump neither counter. An injectable ParseCounter makes the
single-file-echo claim a test.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
The modifier flip (⌥ copy / ⌘ move) now reflows the source board once:
a copy re-admits the dragged originals into the resting layout, standing
dimmed in place, and a move lifts them out as before. This retires
DRAG-REORDER.md's operation-blind carve-out and makes 04-interactions.md's
"originals stay" true in flight, not just at echo. The one seam is
DragSession.hiddenMembers reading the observed operation.
Consequences carried honestly: copyCards now takes its index in the
lane's full rendered space (the zones counted the originals, so the
geometry's number is the writer's number — the old neighbour remap is
deleted); resolveOperation freezes under the committed hold exactly as
propose does, fixing a real bug where a settled within-board ⌥-copy drew
the move arrangement until the echo and visibly re-shuffled. Stationary
flips still wait for the next dropUpdated (their own Backlog card).
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
The trash sorts by modified descending — the arrival rank mint retires
(Ranks.isOrderedForTrash one comparator, loader + merged order agree;
the legacy deleted: migration stamps modified from the tombstone
timestamp where parseable; delete undo steps validate existence-only;
agent guide v8). Trash selection goes kind-blind — ranges, marquee,
Select All, and the successor walk sweep both kinds; the guard moves to
the exits (mixed-payload drop refusal, copy/cut validation). The copy
stamping preflight widens back to comment depth (load-scoped posture —
the board always loads, the gesture refuses whole). Fixes a latent
no-op: trashed-lane drag restore never fired (DragSession.beginLanes
hard-coded the board container).
2403 tests in 413 suites green.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
Phase 2 completes the lanes-in-trash card. TrashEntry merges the
trash's two kinds by rank in exactly ONE place (ItemPath.resolve's
own merge deleted in favor of it — the three-merge-points finding
shrinks instead of growing). TrashLaneRowView renders the opaque
row — tertiary plate, level-default lane glyph never the lane's own
icon, title + card count, no accents, no expansion; the column badge
counts rendered rows. Selection grammar: kind-homogeneous trash
selections — ranges skip the other kind, ⇧-extension stops at the
kind boundary, plain arrows walk the merged order, marquee stays
card-only (now load-bearing: rows register frames for arrows),
Select All card-scoped; successor-on-purge crosses kinds like
navigation as the interim for open Gap 7b5cbc90. Drag: TrashDrop
accepts lane sessions (drop on shown trash deletes), restoreLanes
routes a trash-sourced strip drop as an arrival-ranked within-board
move with an undo step. Clipboard: ⌘X/⌘V lane restore via opaque
lane subjects; fixed boardRoot(ofLaneFolder:) returning .trash as
the root — a same-board restore looked like an import and would
have reminted the lane it was restoring (pinned by test). A11y:
row = one flattened "title, deleted lane, N cards" element with
Delete/Reveal actions; BoardDiff crossings read lanes as
deleted/restored, shown-trash churn digested at row level. Agent
guide stays v7 — the literal already teaches lanes-trash-by-move
and kind stamping; drift-guard pins those lines. README trash
paragraph notes lanes.
Both schemes 1893 tests / 322 suites green.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
Phase 3 finishes the pivot at the surface. One card face serves two
containers: CardFaceView extracted with a role — board or trash — so
stripe, tint, chip, selection stroke, cut dim, marquee registration,
and drag are shared by construction, the trash side differing only in
its absences: no Open, no rename, no Style, no file-hover highlight,
and a Delete that goes through the confirmation host. The column
rewrote around the lanes' own single-column masonry so drag reflow
reads as positional slides; chrome stays the hatched header, symbol,
and count — 11 gives Empty Trash to the File menu alone. Two real
grammar bugs die here: plain Backspace on a trash selection purged
without the confirmation the menu raises, and the context menu's
Delete resolved against the standing selection, so right-clicking a
trash card under a board selection silently did nothing — it now
stages the clicked set explicitly. Open, Rename, Style, and Empty
Trash validation became testable store seams; the column is one named
accessibility container of ordinary card elements. The tombstone era
is swept: deleteItem, restoreItem, stripTombstonedChildren — dead
since lane copies stopped nesting trash — the restore verb, the
unreachable put-back banner row, and every quasi-lane doc comment.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
Phase 2 swaps every consumer: Liveness and its ancestor walk are gone,
replaced by ItemContainer — a UUID set plus the container side it
lives on, presence the whole test, one selection boundary instead of
the old liveness law. Deletion stages by place: board cards move to
the trash at a store-minted head rank, trash-side delete is permanent
behind its confirmation, Delete Immediately skips the trash from
anywhere, lane delete captures the subtree and removes the folder.
Restore has no method at all — moveCards resolves members in either
container, so drag-out and cut-paste are the ordinary moves 13 calls
them, registering ordinary Move steps. The delete inverse moves the
card back to its captured lane and rank; redo replays the captured
trash rank, a value the gesture actually wrote; lane undo recreates
the subtree byte-faithfully in session. Purges register nothing —
where 13's trash section contradicts its own Rules on that, Rules
wins, filed for ruling. Staleness collapsed to present-or-absent: a
container is a path, so a foreign restore fails the delete step's
expectation structurally. Legacy tombstones migrate on the loose-file
tail hook, cards oldest-first so minting above top reproduces the
retired newest-first column, lanes returning live, one folded loss
row naming both directions. Put Back, restoreByDrag,
receiveRestoredCards, TrashEntry, and the kind machinery are deleted;
the trash column renders the container correctly with its full face
rework left to phase 3.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The settle rule 3f4125e introduced — at release the shadow swaps for the
dropped card(s) drawn in place immediately — is withdrawn on review. The
board returns to the presentation the committed-overlay hold has always
had: the shadows keep standing at their landing slot and the originals
keep their lift until the echo reload lands, whatever the operation was,
and the real faces arrive with the snapshot that carries the write.
Gone with it: the DropLanding/DroppedItem seam and the three landing
accessors, the hold's landing/operation payload and the removesOriginals
and keepsIdentity halves it decided, the pickup-captured titles a
cross-board arrival's face needed, LaneSlot.dropped with DroppedFace and
DroppedCardFace, the strip's dropped-lane run, and the trash column's
settled tombstone rows — the same presentation at the delete gesture's
end, which goes for the same reason while the drop-to-delete write and
its shadow row stay exactly as they were.
Kept: the hold itself, which predates the commit, along with the 1500 ms
failed-write timeout, its injectable duration and its extracted expire,
and the guard that keeps a settled release past retargeting. The tests
pinning those stay; the ones asserting what the landing slot drew are
gone, and the suite now pins the hold's own claim — the arrangement does
not move at the release.
The pause between the release and the echo is therefore an open design
question again, filed on the Redesign board.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
04's settled clauses were mostly shipped already — the create landing
resolved through DropSlotMath.cardSlot with one nominal shadow per
importable file — but a release on the lane header fell through to the
card zones, which clamp inward, so a scrolled lane could propose behind
the header stripe. FileDropZones now folds header, attach hit-test, and
card-slot resolution into one pure seam asked in that order, the header
answering topmost per the ruling; lane headers register their frames
for it. FinderDrop.shadowCount names the floor-at-one rule. New tests
pin the header boundary, a differential against cardSlot's own zones
(same zones, not similar), and a store-level differential proving a
file landing takes the very ranks a card move there takes.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
04's ruling makes the drag the pointer's delete gesture: the shown
trash column accepts live same-board card drags, the shadow pinned
topmost — honest, since the trash sorts by deleted newest-first — and
release tombstones through the same write path as Backspace, extracted
so the two gestures cannot drift. DropTarget grew a container case for
the quasi-lane (it has no lane id by construction); lane drags,
cross-board arrivals, option-copies (re-checked at release, the one
input that can flip without a callback), trashed-side payloads, hidden
trash, and the read-only lock all refuse — and a refusal falls through
to the strip retarget, never cancelling the drag. The settle draws the
tombstoned rows in the trash under the cards' own GUIDs, so the echo is
an invisible content swap and nothing winks out for a round trip.
Selection needs no surgery: the reload's resolve rule ejects tombstoned
members as the vanish it is, pinned by a test contrasting both gestures.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
03's sharpened settle rule: rendering the arrangement means rendering
the card — at release the shadow swaps for the dropped card(s) drawn in
place immediately, the appear never waiting for the echo reload. The
committed hold now carries the landing (ids, payload titles, operation)
and surfaces read one DropLanding seam: within-board moves draw the
real faces at their proposed slots under the arriving card's own key,
so the echo is an invisible content swap; cross-board card arrivals
draw payload-titled faces keyed positionally, so the echo reads as an
ordinary arrival. Cross-board lane arrivals deliberately keep their
shadow until the echo — a lane's face is a whole column with no honest
payload equivalent. The 1500 ms failed-write timeout is now seamed
(injectable duration, extracted expire) and pinned by tests.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
04's settled ruling: the attachment model is flat top-level files, so a
drag containing only folders never engages — no highlight, no proposal,
the standard incompatible-payload cursor — and a mixed drag proposes for
its files only, importing them at the drop while a loss row names the
skipped folders. Hover reads the providers' registered types (anything
conforming to public.directory refuses, packages included); commit
re-partitions authoritatively from the filesystem, so a synthetic
payload that hides its type still can't land a folder. The create path
now only ever fires with at least one importable file — the
mint-fail-remove dance is gone from the folder case and stays reserved
for genuine mid-batch failures.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
Files from Finder land on the board per 04-interactions.md § Drag & drop:
- Dropped on a card, they copy into its attachments/ (any type, multi-file),
the face highlighting while hovered; the hovered card is resolved by
hit-testing the same analytic masonry frames the card zones are built
from, so attach-beats-create adds no drop region and cannot drift from
the dispatch.
- Dropped on lane empty space, one card per file — filename minus extension
as the title (a blank stem omits the key), fresh GUID, rank at the drop
position through the ordinary insertion machinery, the file attached —
all in one bracket; a failed import removes the just-minted card, so
creating-then-abandoning never leaves an empty card behind.
- Every board drop surface now declares .fileURL beside the two board
types (the single-target-dispatch rule); tombstoned surfaces are inert;
file sessions ride a distinct session mode with their own watchdog and
no hysteresis, leaving the board-drag machinery untouched.
- Also: four empty fixture directories pinned with .keep files so git
preserves them, and a test-only visibility fix in DragSessionTests.
811 unit tests (12 new).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The second half: system drag sessions over phase 1's model, per
DRAG-REORDER.md and 04-interactions.md § Drag & drop.
- Card faces, lane headers, and trash rows drag as NSItemProvider sessions
(two exported UTTypes, JSON payload in flatten order, plain-text titles as
the secondary representation) — replacing m4's custom lane-reorder gesture
and trash drag-out wholesale; the app-wide DragSession carries the members,
the frozen dragged sizes, the live proposal, and the effective operation.
- Three drop delegates (lane masonry, strip, window fallback), each accepting
both types and routing internally per the single-target-dispatch rule; the
cursor is the physical mouse converted to strip space; proposals come from
DropSlotMath with hysteresis threaded through, and the lane-strip proposal
clamps in front of the shown trash.
- Locality picks the default — move within a board, copy across, the badge
tracking live; ⌥ forces copy (ignored on within-board lane drags), ⌘
forces move; trash rows restore within their board (positional), copy out
across boards by default, ⌘ forcing the true restore-move.
- N contiguous shadows with reflow keyed on the proposal; the
committed-overlay hold renders the dropped arrangement until the reload
echo lands (1.5 s dissolution deadline for refused writes); the
re-grounding trio: geometry re-derives per render, proposals re-validate
by liveness at release, an emptied drag cancels itself.
- Edge autoscroll (ticking driver over DragAutoScrollMath, re-targeting per
step), the mouse-up-gated late-event cleanup, and the polling watchdog —
the pathfinder's lifecycle traps, ported.
- Store: moveLanes and multi-card restoreByDrag join the one-bracket drop
commits.
784 unit tests.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY