Realign code with the 2026-07-29 findings-resolution rulings
Nine rulings land as code. Reorders don't stamp — one container-change predicate (WriteOperation.rewritesOrderOnly): within-container reorders and the renumber rescale rewrite only order, while cross-lane, cross-board, and trash moves stamp modified and clear modified-by; no trash special case exists, and the m8 undo inverses conform through the same seam. Copies are transactions: the root-strict/nested-lenient split retires for a whole-subtree stampability preflight that refuses loudly naming the offender, and every item-level copy severs remote/remote-state at every level (whole-board forks carry them verbatim). Paste refuses, never degrades: the embedded-index.md materialization and its loss row retire; a missing staged snapshot produces nothing and posts an error-tone one-shot named from manifest metadata. Coerce-tier fallbacks log through the Defect stream with path context attached loader-side. Displacement is level-uniform: a file squatting attachments inside a card heals by the same rename ladder as board-root squatters; comments stays tolerated. Delete Immediately joins card and lane context menus as Delete's ⌥-alternate with its own VO custom action, routed through an explicit container so the menu target outranks standing selection. Agent guide v7 teaches the stamp discipline and the card-level attachments claim, and sheds two stale v6 lines (lanes trash now; kind is taught). Verified conformant, unchanged: edition-aware Undo/Redo disable, trash marquee full-height backdrop. Both schemes 1854 tests / 318 suites green; verify-editions 30/30. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -287,3 +287,174 @@ struct ClaimedNamePhrasingTests {
|
||||
== "Couldn't move '.trash' aside — Lanework needs that name — permission denied")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The card level
|
||||
|
||||
/// **The rule is level-uniform** (01-storage-format.md § Fractal layout ▸ Rules, extended
|
||||
/// 2026-07-29):
|
||||
///
|
||||
/// > a card's reserved child names are claimed the same way — a regular file or symlink squatting
|
||||
/// > `attachments` (a directory name) displaces by the same ladder (`attachments` → `attachments 2`),
|
||||
/// > so imports, Finder drops, and the sidebar listing never fail one gesture at a time against a
|
||||
/// > squatted name; the displaced file, now an ordinary loose file, rides the next relocation into the
|
||||
/// > real `attachments/` — the heals compose.
|
||||
///
|
||||
/// The **reserved-but-unconsumed `comments`** is the timing principle's own illustration and is
|
||||
/// deliberately *not* displaced: nothing reads that name until the tracker era, so a wrong-kind holder
|
||||
/// degrades nothing while it stands and keeps the tolerated-stray posture.
|
||||
@MainActor
|
||||
@Suite("Claimed names ▸ the card level")
|
||||
struct CardClaimedNameTests {
|
||||
|
||||
/// A card holding a *file* called `attachments`. The load reports it and moves nothing — detection
|
||||
/// is read-only at every level.
|
||||
@Test("A file on a card's attachments is reported as a defect, and the load moves nothing")
|
||||
func fileOnAttachmentsIsADefect() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments", Data("not a folder".utf8))
|
||||
|
||||
let result = try BoardLoader.load(boardRoot: fixture.root)
|
||||
|
||||
#expect(result.claimedNameSquatters == [
|
||||
ClaimedNameSquatter(
|
||||
name: "attachments",
|
||||
found: .file,
|
||||
expected: .directory,
|
||||
location: .card(path: "\(Ident.lane1)/\(Ident.card1)")
|
||||
),
|
||||
])
|
||||
#expect(
|
||||
try fixture.data("\(Ident.lane1)/\(Ident.card1)/attachments") == Data("not a folder".utf8),
|
||||
"the loader never writes"
|
||||
)
|
||||
// A claimed name is not a stray, so it never earns the stray-tolerance vocabulary — and it is
|
||||
// not a loose file either, so the relocation has nothing to say about it yet.
|
||||
#expect(result.warnings.isEmpty)
|
||||
#expect(result.looseCardFiles.isEmpty)
|
||||
}
|
||||
|
||||
/// A **symlink** wearing the name is the same defect and is moved *as a link*, never followed
|
||||
/// (01 § Fractal layout ▸ Rules: "symlinks are never traversed").
|
||||
@Test("A symlink on a card's attachments is the same defect")
|
||||
func symlinkOnAttachmentsIsADefect() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try FileManager.default.createSymbolicLink(
|
||||
atPath: fixture.url("\(Ident.lane1)/\(Ident.card1)").appendingPathComponent("attachments").path,
|
||||
withDestinationPath: "../elsewhere"
|
||||
)
|
||||
|
||||
let result = try BoardLoader.load(boardRoot: fixture.root)
|
||||
#expect(result.claimedNameSquatters.map(\.found) == [.symlink])
|
||||
}
|
||||
|
||||
/// **`comments` stays tolerated** — the timing principle, stated as the absence of a defect.
|
||||
@Test("A file on a card's comments is not displaced — the name is not load-bearing yet")
|
||||
func fileOnCommentsIsTolerated() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.file("\(Ident.lane1)/\(Ident.card1)/comments", Data("someday".utf8))
|
||||
|
||||
let result = try BoardLoader.load(boardRoot: fixture.root)
|
||||
#expect(result.claimedNameSquatters.isEmpty)
|
||||
#expect(result.looseCardFiles.isEmpty, "a reserved name is not a loose file either")
|
||||
}
|
||||
|
||||
/// A real `attachments/` folder is a resident, not a squatter — the check is about the node's
|
||||
/// *kind*, and this is the negative case that keeps it honest.
|
||||
@Test("A real attachments folder is no defect at all")
|
||||
func aRealAttachmentsFolderIsFine() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/shot.png", Data([0x01]))
|
||||
|
||||
#expect(try BoardLoader.load(boardRoot: fixture.root).claimedNameSquatters.isEmpty)
|
||||
}
|
||||
|
||||
/// The heal itself, end to end: the ladder renames it inside the **card's** folder, the notice names
|
||||
/// old and new, and the file's bytes are exactly what they were.
|
||||
@Test("The heal displaces it by the ladder, inside the card's own folder")
|
||||
func theHealDisplacesItInsideTheCard() async throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let cardPath = "\(Ident.lane1)/\(Ident.card1)"
|
||||
try fixture.file("\(cardPath)/attachments", Data("squatter".utf8))
|
||||
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
store.displaceClaimedNames()
|
||||
await store.awaitQuiescence()
|
||||
|
||||
#expect(try fixture.data("\(cardPath)/attachments 2") == Data("squatter".utf8), "preserved verbatim")
|
||||
#expect(
|
||||
IntegrityRules.node(at: fixture.url(cardPath).appendingPathComponent("attachments")) == nil,
|
||||
"and the name is free for the app"
|
||||
)
|
||||
#expect(store.banners.losses.count == 1)
|
||||
let message = try #require(store.banners.losses.first?.message)
|
||||
#expect(message.contains("attachments"))
|
||||
#expect(message.contains("attachments 2"))
|
||||
}
|
||||
|
||||
/// **The heals compose** — 01's own word for it: once displaced, the file is an ordinary loose file
|
||||
/// beside the card's `index.md`, which is exactly what the loose-file relocation exists for. One
|
||||
/// reload later it is inside the real `attachments/`.
|
||||
@Test("Displaced, then relocated: the heals compose")
|
||||
func theHealsCompose() async throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let cardPath = "\(Ident.lane1)/\(Ident.card1)"
|
||||
try fixture.file("\(cardPath)/attachments", Data("squatter".utf8))
|
||||
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
store.displaceClaimedNames()
|
||||
await store.awaitQuiescence()
|
||||
|
||||
// The next load sees an ordinary loose file where the squatter was.
|
||||
let after = try BoardLoader.load(boardRoot: fixture.root)
|
||||
#expect(after.claimedNameSquatters.isEmpty)
|
||||
#expect(after.looseCardFiles.map(\.fileNames) == [["attachments 2"]])
|
||||
|
||||
let relocating = try BoardStore(rootURL: fixture.root)
|
||||
relocating.relocateLooseCardFiles()
|
||||
await relocating.awaitQuiescence()
|
||||
|
||||
#expect(
|
||||
try fixture.data("\(cardPath)/attachments/attachments 2") == Data("squatter".utf8),
|
||||
"and it landed in the real attachments/"
|
||||
)
|
||||
}
|
||||
|
||||
/// Two cards squatting the name are **two pieces of work** in one bracket — the signature carries
|
||||
/// the location, so one card's failed heal has no claim to have failed the other's.
|
||||
@Test("Two squatted cards are two defects, healed in one bracket")
|
||||
func twoCardsAreTwoDefects() async throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second"))
|
||||
try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments", Data("one".utf8))
|
||||
try fixture.file("\(Ident.lane1)/\(Ident.card2)/attachments", Data("two".utf8))
|
||||
|
||||
let result = try BoardLoader.load(boardRoot: fixture.root)
|
||||
#expect(result.claimedNameSquatters.count == 2)
|
||||
#expect(Set(result.defects.flatMap(\.signatures)).count == 2, "distinct work, by location")
|
||||
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
store.displaceClaimedNames()
|
||||
await store.awaitQuiescence()
|
||||
|
||||
#expect(try fixture.data("\(Ident.lane1)/\(Ident.card1)/attachments 2") == Data("one".utf8))
|
||||
#expect(try fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments 2") == Data("two".utf8))
|
||||
}
|
||||
|
||||
/// The table is the only thing to edit when `comments` graduates — pinned so the split is a stated
|
||||
/// rule rather than an accident of the probe's implementation.
|
||||
@Test("The card-level table claims attachments and comments, and displaces only attachments")
|
||||
func theTableStatesTheSplit() {
|
||||
let names = IntegrityRules.claimedCardChildNames
|
||||
#expect(names.map(\.name) == ["attachments", "comments"])
|
||||
#expect(names.allSatisfy { $0.expected == .directory })
|
||||
#expect(names.first { $0.name == "attachments" }?.displacesSquatters == true)
|
||||
#expect(names.first { $0.name == "comments" }?.displacesSquatters == false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user