Generated and pasted board backgrounds move into .backgrounds/ — the board root stops collecting the app's own pictures

New app-written background images (Theme tab ▸ Pattern, and Edit ▸ Paste
as Board Background) now land in a hidden `.backgrounds/` folder at
board root instead of beside index.md, matching the `.trash/` app-managed
pattern. `background.image` stores the qualified relative reference
(`.backgrounds/facets.png`); the resolver needed no change at all, since
it already accepted any relative path inside the board root — the same
mechanism that already resolved `art/backdrops/sunset.png` resolves the
new location for free. `BoardWriter.writeBoardImage` now creates its
destination folder if missing, since `.backgrounds/` won't exist until a
board's first generated or pasted background.

The Finder collision-ladder (`BoardStore.boardImageName`) is rescoped to
`.backgrounds/`'s own contents, and its overwrite-in-place check now
recognizes only the qualified form as "ours" — a legacy bare
`background.image: facets.png` from before this change is read as a
foreign reference rather than migrated, so a regeneration writes a fresh
`.backgrounds/` file and orphans the old one in place, per the no-migration
ruling. The Theme tab's Pattern/Solid mode-detection was updated to
recognize both the legacy and current spellings as the generator's own
output.

The board loader needed no change: `.backgrounds/` is a hidden,
non-UUID-shaped name, and `.skipsHiddenFiles` already keeps every hidden
entry off the lane walk before any name-based exclusion is consulted —
pinned with a new loader test. Deliberately did not add `.backgrounds` to
IntegrityRules' claimed-name/squatter-displacement table: that table
mirrors a specific existing DESIGN ruling this card doesn't amend.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 08:38:07 -04:00
parent 0a01405ced
commit d0c546179f
9 changed files with 270 additions and 88 deletions
+79 -32
View File
@@ -306,11 +306,17 @@ public final class BoardStore: HealHost {
/// following a reroll inside one reload window would read `facets.png` as its own and overwrite a
/// file it never wrote.
///
/// **`name` and `bareName` differ since the `.backgrounds/` ruling** (2026-08-09): `name` is the
/// relative reference `background.image` stores (`.backgrounds/facets.png`), and `bareName` is
/// the file name it was written under inside that folder (`facets.png`) the two calls
/// `boardImageName` feeds (`BoardWriter.writeBoardImage`'s `named:` and
/// `FrontmatterDocument.setBackgroundImage`'s value) no longer take the same string.
///
/// `@ObservationIgnored` because nothing renders it: it is bookkeeping about a file name, and a
/// view that redrew when it changed would be redrawing for the write it is already going to be
/// told about by the reload.
@ObservationIgnored
var generatedBackgroundEcho: (name: String, base: String, reloads: Int)?
var generatedBackgroundEcho: (name: String, bareName: String, base: String, reloads: Int)?
/// Tolerated anomalies from the load that produced `snapshot` (stray folders, an indexless
/// UUID-shaped folder, a board-level `deleted:`). Replaced with the snapshot, so they always
@@ -2009,9 +2015,10 @@ public final class BoardStore: HealHost {
// MARK: - Generated background
/// **Applies a generated background to this board** the picture into the board folder and the
/// `background` mapping pointed at it, in one bracket (03-board-ui.md § Styling Capabilities;
/// DESIGN/explorations/board-backgrounds.md; `FacetsGenerator`).
/// **Applies a generated background to this board** the picture into `.backgrounds/`
/// (`BoardBackdrop.backgroundsFolderName`, ruled 2026-08-09) and the `background` mapping pointed
/// at it, in one bracket (03-board-ui.md § Styling Capabilities; DESIGN/explorations/
/// board-backgrounds.md; `FacetsGenerator`).
///
/// **The pixels are the caller's**, and that is the isolation contract: rendering a 3072 px mesh
/// and PNG-encoding it is tens of milliseconds of pure computation, so it belongs on a detached
@@ -2032,9 +2039,11 @@ public final class BoardStore: HealHost {
///
/// Regenerating is the common gesture the user rerolls until they like it so a board must not
/// accumulate a PNG per roll. The board's own generated file is therefore **overwritten in place**
/// whenever `background.image` already names it, and the Finder ladder is used only when the name
/// belongs to somebody else (`BoardWriter.freshName`): a hand-placed `facets.png` in the board
/// folder is the user's file and is never written through.
/// whenever `background.image` already names the `.backgrounds/facets.png` this store writes, and
/// the Finder ladder scoped to `.backgrounds/`'s own contents is used only when the name
/// belongs to somebody else (`BoardWriter.freshName`): a hand-placed `facets.png` inside
/// `.backgrounds/` is the user's file and is never written through. A **legacy** bare `facets.png`
/// at board root is not "ours" by this reading either see `boardImageName`'s own note.
///
/// ### The undo restores the fields, not the bytes
///
@@ -2061,30 +2070,36 @@ public final class BoardStore: HealHost {
let root = rootURL
let priorImage = snapshot.backgroundImage
let priorColor = snapshot.background
let name = boardImageName(
let target = boardImageName(
base: FacetsGenerator.fileName, replacing: priorImage.value, inRoot: root
)
let backgroundsFolder = root.appendingPathComponent(
BoardBackdrop.backgroundsFolderName, isDirectory: true
)
let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
try BoardWriter.writeBoardImage(
data: png, named: name, inRoot: root, operation: .setBoardBackground
data: png, named: target.bareName, inRoot: backgroundsFolder, operation: .setBoardBackground
)
// `kind: .board` for the one subject whose position nothing can infer the board root
// (`BoardWriter.updateIndex`'s on-touch backfill), exactly as `applyStyle` passes it.
try BoardWriter.updateIndex(
inItemFolder: root, kind: .board, operation: .setBoardBackground
) { document in
Self.pointBackground(at: name, color: colorHex, in: &document)
Self.pointBackground(at: target.reference, color: colorHex, in: &document)
}
}
guard landed != nil else { return false }
generatedBackgroundEcho = (name: name, base: FacetsGenerator.fileName, reloads: landedReloads)
generatedBackgroundEcho = (
name: target.reference, bareName: target.bareName, base: FacetsGenerator.fileName,
reloads: landedReloads
)
// restyle prior style (13-native-undo.md Rules). The board's own stack, never a window's:
// there is no card here to have a session.
registerStep(
HistoryPhrase.name(.restyle, kind: .board),
undoExpects: [.present(root, .background(colorHex), .backgroundImage(name))],
undoExpects: [.present(root, .background(colorHex), .backgroundImage(target.reference))],
redoExpects: [.present(root, .background(priorColor.value), .backgroundImage(priorImage.value))]
) { _ in
try BoardWriter.updateIndex(
@@ -2099,7 +2114,7 @@ public final class BoardStore: HealHost {
try BoardWriter.updateIndex(
inItemFolder: root, kind: .board, operation: .setBoardBackground
) { document in
Self.pointBackground(at: name, color: colorHex, in: &document)
Self.pointBackground(at: target.reference, color: colorHex, in: &document)
}
}
return true
@@ -2180,14 +2195,30 @@ public final class BoardStore: HealHost {
document.setBackgroundImage(nil)
}
/// The name a board image is written under: **ours to overwrite**, or the next free one.
/// The name a board image is written under: **ours to overwrite**, or the next free one always
/// inside `.backgrounds/` (`BoardBackdrop.backgroundsFolderName`, ruled 2026-08-09: "new
/// app-written background images go into an app-claimed folder", the `.trash/` pattern applied
/// one concept over).
///
/// `base` is the producer's own file name `facets.png` for the generator, `Pasted
/// Background.<ext>` for a paste and `current` is what `background.image` says now. When that
/// is already `base` the file is this board's own output and is replaced in place including
/// when it has been deleted from the folder by hand, which is a board whose backdrop is broken
/// and is exactly what regenerating (or re-pasting) fixes. Otherwise the Finder ladder decides,
/// which yields the plain name when nothing holds it and `facets 2.png` when something does.
/// `base` is the producer's own bare file name `facets.png` for the generator, `Pasted
/// Background.<ext>` for a paste and `current` is what `background.image` says now, in whatever
/// form it happens to be spelled. Only the **qualified** form (`.backgrounds/facets.png`) reads as
/// "ours to overwrite in place" including when the file has been deleted from `.backgrounds/` by
/// hand, which is a board whose backdrop is broken and is exactly what regenerating (or
/// re-pasting) fixes. Otherwise the Finder ladder scoped to `.backgrounds/`'s own contents
/// decides, which yields the plain name when nothing there holds it and `facets 2.png` when
/// something does.
///
/// ### A legacy bare reference is not "ours"
///
/// A board whose `background.image` still names a bare `facets.png` written before this ruling,
/// at board root does **not** match the qualified form, so it is read the same as a hand-placed
/// image belonging to nobody: this write proceeds into `.backgrounds/facets.png` (laddering only
/// if `.backgrounds/` itself already holds one) rather than overwriting the legacy file in place.
/// That is the deliberate reading of "legacy referenced images stay where they are no
/// migration": the old file is left exactly as it was, at the price of becoming an orphan the
/// moment the field starts pointing at the new one the same leftover an ordinary regeneration
/// already produces (`applySolidBackground`'s own note).
///
/// ### The reroll's echo
///
@@ -2207,12 +2238,23 @@ public final class BoardStore: HealHost {
/// **The echo is only consulted for its own family** (`base`), which is what keeps two producers
/// off each other: a paste landing inside the reroll's echo window must not read `facets.png` as
/// a name it owns.
private func boardImageName(base: String, replacing current: String?, inRoot root: URL) -> String {
if current == base { return base }
///
/// - Returns: the bare name to write the bytes under (`BoardWriter.writeBoardImage`'s `named:`)
/// and the qualified relative reference to store in `background.image` no longer the same
/// string now that every write lives one folder down from the board root.
private func boardImageName(
base: String, replacing current: String?, inRoot root: URL
) -> (bareName: String, reference: String) {
let backgroundsFolder = root.appendingPathComponent(
BoardBackdrop.backgroundsFolderName, isDirectory: true
)
let qualified = "\(BoardBackdrop.backgroundsFolderName)/\(base)"
if current == qualified { return (base, qualified) }
if let echo = generatedBackgroundEcho, echo.base == base, echo.reloads == landedReloads {
return echo.name
return (echo.bareName, echo.name)
}
return BoardWriter.freshName(for: base, in: root)
let fresh = BoardWriter.freshName(for: base, in: backgroundsFolder)
return (fresh, "\(BoardBackdrop.backgroundsFolderName)/\(fresh)")
}
// MARK: - Pasted background
@@ -2239,8 +2281,8 @@ public final class BoardStore: HealHost {
/// deleted.
///
/// **The undo restores the field, not the bytes** `applyGeneratedBackground`'s own note,
/// unchanged and for its reason: a re-paste over this board's own `Pasted Background.png`
/// overwrites pixels nothing kept a copy of.
/// unchanged and for its reason: a re-paste over this board's own `.backgrounds/Pasted
/// Background.png` overwrites pixels nothing kept a copy of.
///
/// - Returns: whether bytes reached disk, which is the same question as "is an echo reload
/// coming".
@@ -2249,20 +2291,25 @@ public final class BoardStore: HealHost {
let root = rootURL
let priorImage = snapshot.backgroundImage
let base = "\(PastedImage.backgroundBaseName).\(fileExtension)"
let name = boardImageName(base: base, replacing: priorImage.value, inRoot: root)
let target = boardImageName(base: base, replacing: priorImage.value, inRoot: root)
let backgroundsFolder = root.appendingPathComponent(
BoardBackdrop.backgroundsFolderName, isDirectory: true
)
let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
try BoardWriter.writeBoardImage(
data: data, named: name, inRoot: root, operation: .setBoardBackground
data: data, named: target.bareName, inRoot: backgroundsFolder, operation: .setBoardBackground
)
try BoardWriter.updateIndex(
inItemFolder: root, kind: .board, operation: .setBoardBackground
) { document in
document.setBackgroundImage(name)
document.setBackgroundImage(target.reference)
}
}
guard landed != nil else { return false }
generatedBackgroundEcho = (name: name, base: base, reloads: landedReloads)
generatedBackgroundEcho = (
name: target.reference, bareName: target.bareName, base: base, reloads: landedReloads
)
// restyle prior image (13-native-undo.md Rules). The board's own stack, never a window's:
// there is no card here to have a session. **Only the image subkey is declared**, which is
@@ -2270,7 +2317,7 @@ public final class BoardStore: HealHost {
// the step, because this gesture never wrote a colour.
registerStep(
HistoryPhrase.name(.restyle, kind: .board),
undoExpects: [.present(root, .backgroundImage(name))],
undoExpects: [.present(root, .backgroundImage(target.reference))],
redoExpects: [.present(root, .backgroundImage(priorImage.value))]
) { _ in
try BoardWriter.updateIndex(
@@ -2284,7 +2331,7 @@ public final class BoardStore: HealHost {
try BoardWriter.updateIndex(
inItemFolder: root, kind: .board, operation: .setBoardBackground
) { document in
document.setBackgroundImage(name)
document.setBackgroundImage(target.reference)
}
}
return true