Orphaned .backgrounds/ files get tidied — a repoint trims the one it leaves behind, and every open sweeps what got away
Follow-up to d0c5461's .backgrounds/ folder: generating, pasting, or choosing solid now trims the app's own prior file in .backgrounds/ as part of the same write when it repoints or unsets background.image away from it — silent, best-effort, never blocking the gesture that triggered it. A scheduled heal at every board open sweeps whatever that trim declined or missed: any .backgrounds/ file the board's current background.image no longer names, announced with a loss-row notice in the loose-file relocation's own voice. Legacy root-level references are untouched by both paths — the tidy scopes to .backgrounds/ only, since that is the one folder the app can prove it wrote into. Removal is via FileManager.trashItem, matching the attachment-removal precedent (recoverable, never a hard delete). Supersedes applySolidBackground's earlier "facets.png survives on disk" contract for the settled case: the ruling reads "unsets" as one more shape of "repoints away from a .backgrounds/ file", so a settled solid choice now trims the generated picture it displaces, same as switching producers does. The superseded test and doc comment are updated to the new behavior; the echo-window (no-reload-yet) case is unaffected and still leaves the file in place. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -2767,6 +2767,58 @@ public enum BoardWriter: Sendable {
|
||||
return trashedURL as URL?
|
||||
}
|
||||
|
||||
/// Moves a file out of `.backgrounds/` to the **system** Trash — `trashAttachment`'s body one
|
||||
/// folder over, and the shared primitive both orphan-tidy mechanisms use (01-storage-format.md §
|
||||
/// Validation and healing, ruled 2026-08-09): the replace-in-place trim a background apply makes
|
||||
/// on the file it just repointed or unset away from (`BoardStore.applyGeneratedBackground`,
|
||||
/// `applyPastedBackground`, `applySolidBackground`), and the open-time sweep of whatever
|
||||
/// `.backgrounds/` holds that the board's current `background.image` no longer names
|
||||
/// (`BoardStore.tidyBackgroundImages`).
|
||||
///
|
||||
/// **`FileManager.trashItem`, never `removeItem`** — `trashAttachment`'s own reasoning, applied to
|
||||
/// a file the app rather than the user put there: an app-written background is recoverable exactly
|
||||
/// the way a trashed attachment is, by the user, in Finder, with no help from this app, which is
|
||||
/// the ruling's own "removal via `FileManager.trashItem` (recoverable, matches
|
||||
/// attachment-removal precedent)".
|
||||
///
|
||||
/// **A name no longer present in `backgroundsFolder` is not a failure** — `trashAttachment`'s rule
|
||||
/// again: the reload (or, for the in-flow caller, the disk itself a moment later) is the authority
|
||||
/// on what is there, and a file that vanished between the scan and the write — a hand delete, a
|
||||
/// losing race with a foreign write — contributes nothing to report.
|
||||
///
|
||||
/// - Parameter healMarked: whether the receipt this drops is marked as a heal's
|
||||
/// (06-history-undo.md ▸ Commit messages; `relocateLooseFiles`' own parameter, restated here).
|
||||
/// `true` for the scheduled sweep, which is app-initiated work discovered on its own; `false` for
|
||||
/// the in-flow trim, which rides inside the gesture that triggered it rather than a heal's own
|
||||
/// bracket.
|
||||
/// - Returns: where the file now sits inside the Trash, or `nil` when there was nothing to move.
|
||||
@discardableResult
|
||||
static func trashBoardImage(
|
||||
named name: String,
|
||||
inFolder backgroundsFolder: URL,
|
||||
healMarked: Bool,
|
||||
operation: WriteOperation
|
||||
) throws(BoardWriteError) -> URL? {
|
||||
let fileURL = backgroundsFolder.appendingPathComponent(name)
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
|
||||
|
||||
var trashedURL: NSURL?
|
||||
do {
|
||||
try FileManager.default.trashItem(at: fileURL, resultingItemURL: &trashedURL)
|
||||
EchoLedger.current?.recordDeletion(at: fileURL)
|
||||
if healMarked {
|
||||
EchoLedger.current?.markHeal(at: fileURL)
|
||||
}
|
||||
} catch {
|
||||
throw BoardWriteError(
|
||||
operation: operation,
|
||||
path: fileURL.path,
|
||||
reason: .io(message: "could not move file to the Trash: \(error.localizedDescription)")
|
||||
)
|
||||
}
|
||||
return trashedURL as URL?
|
||||
}
|
||||
|
||||
// MARK: - Move/copy pre-flight
|
||||
|
||||
/// The rank a moved or copied root lands on: the caller's explicit value — a drop between
|
||||
@@ -3084,6 +3136,22 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
|
||||
/// never the Finder-renamed one it would have landed under.
|
||||
case relocateLooseFile(filename: String)
|
||||
|
||||
/// A `.backgrounds/` file being moved to the **system** Trash because nothing names it any more —
|
||||
/// the orphan tidy's write, both mechanisms (01-storage-format.md § Validation and healing, ruled
|
||||
/// 2026-08-09): the replace-in-place trim a background apply makes on the file it just repointed
|
||||
/// or unset away from, and the open-time sweep of whatever `.backgrounds/` holds that the board's
|
||||
/// current `background.image` no longer names.
|
||||
///
|
||||
/// Its own case on `.relocateLooseFile`'s reasoning, doubled: this is work the *app* started on
|
||||
/// its own, on a file the user never named, and a banner saying the app "couldn't move 'facets
|
||||
/// 2.png' to the Trash" without this case would borrow `.removeAttachment`'s sentence for a
|
||||
/// gesture — a click on a Remove row — the user never made. In practice only the sweep's own
|
||||
/// failure ever reaches a banner: the in-flow trim swallows whatever this throws (`try?`, the
|
||||
/// ruling's own "temp-file posture"), so this case's phrasing exists for the sweep and for the
|
||||
/// developer-facing `description` either mechanism's failure would log. `filename` is the bare
|
||||
/// name inside `.backgrounds/`, the one the sweep's own successful notice would have used.
|
||||
case tidyBackgroundImage(filename: String)
|
||||
|
||||
/// The board-root `CLAUDE.md` being written or upgraded — and the rescue move that precedes it
|
||||
/// when a markerless one has to be displaced to `CLAUDE.user.md` (08-agent-integration.md ▸ The
|
||||
/// agent guide; `AgentGuide`).
|
||||
@@ -3275,8 +3343,8 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
|
||||
// the name the user is looking at and the only one its banner should say — and there is no
|
||||
// document to enrich from anyway, since the failure it describes is the read of one.
|
||||
case .createBoard, .createLane, .createCard, .importAttachment, .importBoard, .listAttachments,
|
||||
.removeAttachment, .renumberChildren, .relocateLooseFile, .agentGuide, .seedGitignore,
|
||||
.mintBoardIndex, .stampSchema, .setBoardBackground,
|
||||
.removeAttachment, .renumberChildren, .relocateLooseFile, .tidyBackgroundImage, .agentGuide,
|
||||
.seedGitignore, .mintBoardIndex, .stampSchema, .setBoardBackground,
|
||||
.displaceClaimedName, .repairDuplicateID, .saveCommentDraft, .postComment,
|
||||
.editComment, .deleteComment, .purgeCommentTrash:
|
||||
self
|
||||
@@ -3345,8 +3413,8 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
|
||||
.style, .resize, .collapse, .expand, .rename, .duplicateBoard, .saveAsTemplate, .shareBoard, .paste,
|
||||
.exportBoard, .importBoard,
|
||||
.importAttachment,
|
||||
.listAttachments, .removeAttachment, .relocateLooseFile, .agentGuide, .seedGitignore,
|
||||
.mintBoardIndex, .stampSchema, .setBoardBackground,
|
||||
.listAttachments, .removeAttachment, .relocateLooseFile, .tidyBackgroundImage, .agentGuide,
|
||||
.seedGitignore, .mintBoardIndex, .stampSchema, .setBoardBackground,
|
||||
.displaceClaimedName,
|
||||
.repairDuplicateID, .toggleTask, .editBody, .rawSource, .saveCommentDraft, .postComment,
|
||||
.editComment, .deleteComment, .purgeCommentTrash:
|
||||
@@ -3388,6 +3456,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
|
||||
case let .removeAttachment(filename): "move attachment '\(filename)' to the Trash"
|
||||
case .renumberChildren: "renumber children"
|
||||
case let .relocateLooseFile(filename): "relocate loose file '\(filename)'"
|
||||
case let .tidyBackgroundImage(filename): "tidy background image '\(filename)'"
|
||||
case .agentGuide: "update the agent guide"
|
||||
case .seedGitignore: "seed the board's .gitignore"
|
||||
case .mintBoardIndex: "create this board's index.md"
|
||||
|
||||
Reference in New Issue
Block a user