Relocate loose card files into attachments

01's Lanework-owns-the-board carve-out: a regular file beside a card's
index.md belongs in attachments/, and the app moves it there. The
loader detects read-only — a new LoadResult.looseCardFiles channel,
separate from the stray-tolerance warnings because it says the opposite
thing — skipping directories, symlinks, hidden entries, and the
reserved names compared case-insensitively (on APFS, Index.md IS the
index). The relocation rides one performWrite bracket at the tail of
every successful reload, which makes lock deferral free: the reload
that lifts a read-only lock is the reload that relocates. A
lane/card/filename memo keeps a failing relocation from hot-looping —
one one-shot, then silence until disk changes. The notice rides the
loss-row class, phrasing folded by BannerCenter (one file, one card's
files, a multi-card sweep), naming original filenames per the
importAttachment rule. Paste normalizes at the import boundary: staged
snapshots' loose files land in the pasted card's attachments silently,
every arrival path declaring its side via an explicit
normalizingLooseFiles parameter — drag paths decline and fall back to
the destination's own carve-out. checkIsCardFolder closes the hole
where a lane's notes.txt would have been relocated: card depth is
exact, UUID under UUID.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 09:09:31 -04:00
parent 5c0c0e5619
commit af1860debf
10 changed files with 1434 additions and 22 deletions
+13
View File
@@ -204,6 +204,19 @@ public final class BoardStoreRegistry {
bookmark: bookmark,
lastKnownRoot: rootURL
)
// The loose-file carve-out's first firing (01-storage-format.md § Fractal layout Rules,
// settled 2026-07-28): files an agent or a hand-editor left beside a card's `index.md`
// while this board was closed are relocated into `attachments/` now, with the notice.
//
// **Here rather than in `BoardStore.init`**, and last rather than first: the store's own
// init is one tree walk and no writes, and a relocation written before the brackets and the
// watcher exist would be a write nothing is watching landing on disk with the snapshot
// above it left one reload stale. By this line the pair is wired, so it is an ordinary
// bracketed app write whose echo reload refreshes the board like any other. Every reload
// thereafter re-fires it from `BoardStore.land`; this call is only the one the opening walk
// would otherwise have no reload behind.
store.relocateLooseCardFiles()
return store
}