Every board root keys once at its store — the hover path stops walking the filesystem

`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
This commit is contained in:
2026-08-01 17:55:48 -04:00
parent 8a8ec4dfd1
commit ef423bb9d2
8 changed files with 232 additions and 124 deletions
+41
View File
@@ -8,6 +8,32 @@ import SwiftUI
// MARK: - Vocabulary
/// **A board root's identity**, as the one canonical spelling every locality comparison is made
/// against (04-interactions.md Drag and drop the Finder volume model).
///
/// Two roots name the same board when they name the same place: symlinks are resolved first a
/// board reached through a pinned symlink is the same board as the one reached directly
/// (01-storage-format.md's symlink pins) and the standardized path is the comparison key, so
/// `/Boards/./Work.kanban/` and `/Boards/Other/../Work.kanban` are one board.
///
/// **Minting touches the filesystem; comparing does not.** `resolvingSymlinksInPath` stats every
/// component of the path the trap `EchoLedger.key` refuses outright for the same reason and the
/// drag's locality question is asked several times per `dropUpdated` and again per lane per body
/// evaluation. Resolution therefore happens exactly once per board, where the root is
/// (`BoardStore.rootKey`), and every reader downstream compares two strings. Holding one of these
/// rather than a `URL` is what makes the re-derivation unwriteable rather than merely avoided.
public struct BoardRootKey: Hashable, Sendable {
/// The resolved, standardized path kept for equality and for nothing else. The root's *user*
/// spelling stays on `BoardStore.rootURL`, which is what the display-name fallback and every
/// derived write path read: canonicalizing there would rename a board reached through a pin.
public let path: String
public init(_ url: URL) {
path = url.resolvingSymlinksInPath().standardizedFileURL.path
}
}
/// Why a board is refusing writes the read-only lock's cause, and now the whole of the
/// vocabulary 02-architecture.md names.
///
@@ -370,6 +396,19 @@ public final class BoardStore: HealHost {
/// snapshot at the new root, after which the two agree again.
public private(set) var rootURL: URL
/// This board's **identity**, minted once here because minting is the filesystem-touching step
/// (`BoardRootKey`). Every locality comparison a drag makes at hover, at release, and in every
/// lane's resting layout is an `==` against this value.
///
/// **Follows the folder, exactly as `rootURL` does** an absorbed rename or move re-mints it
/// (`relocate(to:)`), at one filesystem touch per relocation and none on the hover path. A key
/// frozen at open would outlive the place it names, and a *new* board opened at the vacated path
/// would then mint the same one: two boards comparing as one, which is a cross-board drag
/// silently behaving as a within-board move. The residue of following instead a rename
/// absorbed mid-drag re-reads that drag's locality is the cosmetic failure of the pair, and
/// is what shipped before the key existed.
public private(set) var rootKey: BoardRootKey
// MARK: Banners
/// The board window's banner strip, as a model (02-architecture.md § The banner surface).
@@ -710,6 +749,7 @@ public final class BoardStore: HealHost {
/// surface.
public init(rootURL: URL, loaded result: LoadResult, skipping: Set<String> = []) {
self.rootURL = rootURL
self.rootKey = BoardRootKey(rootURL)
self.snapshot = result.model
self.loadWarnings = result.warnings
self.defects = result.defects
@@ -1228,6 +1268,7 @@ public final class BoardStore: HealHost {
guard newRoot != rootURL else { return }
Self.logger.debug("board root relocated; Writer URLs now derive from the new location")
rootURL = newRoot
rootKey = BoardRootKey(newRoot)
}
/// Raises the vanished-root read-only lock the registry's call, after bookmark re-resolution