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:
@@ -320,6 +320,15 @@ public final class ClipboardStore {
|
||||
/// does rather than earning a sub-rule for the one case where the items were already on this
|
||||
/// board. It is cleared here rather than at ⌘V so the two staleness guards keep their meaning: a
|
||||
/// paste the pasteboard moved under lands nothing, and so clears nothing.
|
||||
///
|
||||
/// **A paste is an import boundary, so normalization applies** (04 ▸ Clipboard, settled
|
||||
/// 2026-07-28 — 01-storage-format.md's loose-file carve-out): every arrival below passes
|
||||
/// `normalizingLooseFiles: true`, so a loose file the staged snapshot faithfully carried beside
|
||||
/// a card's `index.md` lands inside the pasted card's `attachments/`, Finder-renamed on
|
||||
/// collision. Both branches and both operations, unqualified, because 04's sentence is
|
||||
/// unqualified. Nothing is dropped and nothing is announced: the snapshot preserved the file,
|
||||
/// the paste kept it, and it is where the schema says it belongs — the carve-out's notice is for
|
||||
/// files the app moves *without* being asked, which is the loader's path, not this one.
|
||||
private func perform(_ manifest: ClipboardManifest, plan: Plan, into store: BoardStore) {
|
||||
refresh()
|
||||
// The pasteboard moved under this paste (another app copied while the chain settled): the
|
||||
@@ -338,10 +347,17 @@ public final class ClipboardStore {
|
||||
operation: .move,
|
||||
toLane: target.laneID,
|
||||
at: target.index,
|
||||
clearingTombstones: false
|
||||
clearingTombstones: false,
|
||||
normalizingLooseFiles: true
|
||||
)
|
||||
case let .lanes(index):
|
||||
store.receiveLanes(sources, operation: .move, at: index, clearingTombstones: false)
|
||||
store.receiveLanes(
|
||||
sources,
|
||||
operation: .move,
|
||||
at: index,
|
||||
clearingTombstones: false,
|
||||
normalizingLooseFiles: true
|
||||
)
|
||||
}
|
||||
consumeCut()
|
||||
return
|
||||
@@ -383,14 +399,16 @@ public final class ClipboardStore {
|
||||
operation: .copy,
|
||||
toLane: target.laneID,
|
||||
at: target.index,
|
||||
clearingTombstones: clearingTombstones
|
||||
clearingTombstones: clearingTombstones,
|
||||
normalizingLooseFiles: true
|
||||
)
|
||||
case let .lanes(index):
|
||||
store.receiveLanes(
|
||||
sources,
|
||||
operation: .copy,
|
||||
at: index,
|
||||
clearingTombstones: clearingTombstones
|
||||
clearingTombstones: clearingTombstones,
|
||||
normalizingLooseFiles: true
|
||||
)
|
||||
}
|
||||
store.banners.postDegradedPaste(losses)
|
||||
|
||||
Reference in New Issue
Block a user