Align the identity predicate with the ratified shape-only rule

Accept liberally, emit conservatively (764a4d4): the gate is 8-4-4-4-12
hex in any case and any UUID version — uuidgen and UUID().uuidString
print uppercase, and a strict lowercase gate would silently stray an
agent's standard-tool card. Identity comparison is UUID-value equality
everywhere: ItemID keeps its byte-faithful rawValue but equates and
hashes on the lowercased canonical form, and the writer's
import-boundary collision check canonicalizes, so a same-UUID arrival
spelled in another case remints instead of slipping past. The app still
mints only lowercase v4 and never renames to canonicalize.

Full suite 342 tests in 63 suites green. Two findings filed.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 20:53:38 -04:00
parent 1d7449e49a
commit 4418b7f981
6 changed files with 301 additions and 51 deletions
+128 -11
View File
@@ -47,11 +47,12 @@ private struct BoardFixture {
}
}
/// A fresh folder name with UUIDv4's shape (lowercase hex, `8-4-4-4-12`) the only shape
/// `BoardLoader` accepts as a lane/card candidate (01-storage-format.md § Fractal layout
/// Rules, "Name shape gates level detection"). Used wherever a test just needs *a* valid
/// lane/card identity and doesn't care about the exact value; tests that need a specific
/// lexicographic ordering use literal UUID-shaped strings instead.
/// A fresh folder name in the app's own emission spelling lowercase v4. The loader's gate is
/// shape-only (hex, `8-4-4-4-12`, any case, any version 01-storage-format.md § Fractal layout
/// Rules, "Name shape gates level detection"), so this is *a* valid identity rather than the
/// only kind; the case tests below cover the rest. Used wherever a test just needs some lane/card
/// identity and doesn't care about the exact value; tests that need a specific lexicographic
/// ordering use literal UUID-shaped strings instead.
private func uuidFolderName() -> String {
UUID().uuidString.lowercased()
}
@@ -303,22 +304,90 @@ struct BoardLoaderNonUUIDStrayTests {
#expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "\(lane)/scratch")))
}
/// Case sensitivity: an uppercase (or mixed-case) UUID string doesn't have UUIDv4's
/// *lowercase* shape, so it's a stray folder names are never normalized.
@Test func uppercaseUUIDFolderIsTreatedAsNonUUIDStray() throws {
/// The identity predicate is **shape-only, any case** (01-storage-format.md § Fractal layout
/// Rules, settled): `uuidgen` and `UUID().uuidString` both print uppercase, so an
/// uppercase folder is an ordinary lane never a silently skipped stray. Its `rawValue`
/// keeps the exact spelling: the app accepts liberally and never renames to canonicalize.
@Test func uppercaseUUIDFolderIsALaneWithItsSpellingPreserved() throws {
let fixture = try BoardFixture()
defer { fixture.tearDown() }
let lowercaseLane = uuidFolderName()
let uppercaseLane = UUID().uuidString // Foundation renders this uppercase.
try fixture.index("", "schema: 1\n")
try fixture.index(lowercaseLane, "schema: 1\norder: 1024\n")
try fixture.index(uppercaseLane, "schema: 1\norder: 2048\n")
let result = try BoardLoader.load(boardRoot: fixture.root)
#expect(result.model.lanes.map(\.id.rawValue) == [lowercaseLane, uppercaseLane])
#expect(result.warnings.isEmpty)
}
/// One level down, and mixed case rather than uniform: an agent's `uuidgen`-named card
/// folder loads as a card, spelling intact.
@Test func mixedCaseUUIDCardFolderLoadsAsACardWithItsSpellingPreserved() throws {
let fixture = try BoardFixture()
defer { fixture.tearDown() }
let lane = uuidFolderName()
let mixedCaseCard = "AbCdEf01-2345-6789-aBcD-EF0123456789"
try fixture.index("", "schema: 1\n")
try fixture.index(lane, "schema: 1\norder: 1024\n")
try fixture.index("\(lane)/\(mixedCaseCard)", "schema: 1\norder: 1024\ntitle: From an agent\n")
let result = try BoardLoader.load(boardRoot: fixture.root)
let loadedLane = try #require(result.model.lanes.first)
#expect(loadedLane.cards.map(\.id.rawValue) == [mixedCaseCard])
#expect(loadedLane.cards.first?.title.value == "From an agent")
#expect(result.warnings.isEmpty)
}
/// **Any version**, not just v4: the version nibble protects no invariant here a v7 (or a
/// nibble no RFC ever assigned) is exactly as unique as a v4, so it is an identity, not a
/// stray. Only the shape is checked.
@Test func oddVersionAndVariantNibblesAreStillIdentities() throws {
let fixture = try BoardFixture()
defer { fixture.tearDown() }
let v7Lane = "01912d5e-7c00-7000-8000-abcdefabcdef" // version nibble 7
let oddLane = "01912d5e-7c00-c000-f000-abcdefabcdef" // version c, variant f no RFC's
try fixture.index("", "schema: 1\n")
try fixture.index(v7Lane, "schema: 1\norder: 1024\n")
try fixture.index(oddLane, "schema: 1\norder: 2048\n")
let result = try BoardLoader.load(boardRoot: fixture.root)
#expect(result.model.lanes.map(\.id.rawValue) == [v7Lane, oddLane])
#expect(result.warnings.isEmpty)
}
/// The shape itself is still strict: near-misses stay strays. Hex only, exact group lengths,
/// hyphens exactly where they belong.
@Test func nearMissUUIDShapesAreStillStrays() throws {
let fixture = try BoardFixture()
defer { fixture.tearDown() }
let realLane = uuidFolderName()
let uppercaseLane = UUID().uuidString // Foundation renders this uppercase.
let strays = [
"GGGGGGGG-0000-4000-8000-000000000000", // not hex
"00000000-0000-4000-8000-00000000000", // one digit short
"000000000-000-4000-8000-000000000000", // hyphens misplaced
"00000000-0000-4000-8000-000000000000-", // trailing hyphen
]
try fixture.index("", "schema: 1\n")
try fixture.index(realLane, "schema: 1\norder: 1024\n")
try fixture.index(uppercaseLane, "schema: 1\norder: 2048\n")
for (offset, stray) in strays.enumerated() {
try fixture.index(stray, "schema: 1\norder: \(2048 + offset)\n")
}
let result = try BoardLoader.load(boardRoot: fixture.root)
#expect(result.model.lanes.map(\.id.rawValue) == [realLane])
#expect(result.warnings.contains(.nonUUIDFolderIgnored(path: uppercaseLane)))
for stray in strays {
#expect(result.warnings.contains(.nonUUIDFolderIgnored(path: stray)))
}
}
/// Reserved card children are covered "by construction" now: `attachments/` and
@@ -343,6 +412,54 @@ struct BoardLoaderNonUUIDStrayTests {
}
}
// MARK: - ItemID value semantics (01-storage-format.md § Fractal layout Rules, "Identity
// comparison is UUID-value equality, never string equality")
/// `ItemID` stores the folder's exact spelling it builds URLs but *compares* as a UUID
/// value: two case-spellings of one UUID are one identity everywhere.
struct ItemIDValueSemanticsTests {
private static let lower = "abcdef01-2345-6789-abcd-ef0123456789"
private static let upper = "ABCDEF01-2345-6789-ABCD-EF0123456789"
private static let mixed = "AbCdEf01-2345-6789-aBcD-eF0123456789"
@Test func caseSpellingsOfOneUUIDAreEqualAndHashAlike() {
let lower = ItemID(rawValue: Self.lower)
let upper = ItemID(rawValue: Self.upper)
let mixed = ItemID(rawValue: Self.mixed)
#expect(lower == upper)
#expect(lower == mixed)
#expect(upper == mixed)
#expect(lower.hashValue == upper.hashValue)
#expect(upper.hashValue == mixed.hashValue)
}
/// Equality is by value, but the spelling is never rewritten the app accepts liberally and
/// emits conservatively, and `rawValue` is what builds the folder's URL.
@Test func rawValueKeepsTheExactSpelling() {
#expect(ItemID(rawValue: Self.upper).rawValue == Self.upper)
#expect(ItemID(rawValue: Self.mixed).description == Self.mixed)
}
@Test func distinctUUIDsAreUnequalHoweverTheyAreSpelled() {
let one = ItemID(rawValue: "abcdef01-2345-6789-abcd-ef0123456789")
let other = ItemID(rawValue: "ABCDEF01-2345-6789-ABCD-EF012345678A")
#expect(one != other)
}
/// The consequence every `Set`/`Dictionary` keyed by `ItemID` inherits selection
/// membership included (`BoardStore.Selection`).
@Test func aSetCollapsesTheTwoSpellingsToOneMember() {
let set: Set<ItemID> = [ItemID(rawValue: Self.lower), ItemID(rawValue: Self.upper)]
#expect(set.count == 1)
#expect(set.contains(ItemID(rawValue: Self.mixed)))
var byID: [ItemID: String] = [:]
byID[ItemID(rawValue: Self.upper)] = "written uppercase"
#expect(byID[ItemID(rawValue: Self.lower)] == "written uppercase")
}
}
// MARK: - Board-level deleted
struct BoardLoaderBoardLevelDeletedTests {
+60
View File
@@ -1014,6 +1014,66 @@ struct BoardWriterMoveTests {
#expect(try fixture.indexData("B.kanban/\(Ident.lane3)/\(Ident.card1)") == twinBefore)
}
/// **Identity comparison is UUID-value equality, never string equality** (01-storage-format.md
/// § Fractal layout Rules, settled): the destination board already holds the arriving UUID
/// spelled in *uppercase* an agent's `uuidgen` card so the two are one identity and the
/// import boundary must remint. A verbatim string set would sail straight past this and leave
/// a duplicate UUID in the board.
@Test func aCollisionSpelledInADifferentCaseIsStillOneIdentityAndRemints() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try boardA(fixture)
try boardB(fixture)
let arriving = "abcdef01-2345-6789-abcd-ef0123456789"
let twin = arriving.uppercased() // The same UUID, as `uuidgen` would have printed it.
try fixture.item("A.kanban/\(Ident.lane1)/\(arriving)", Item.rich(order: "3072", title: "Card One"))
try fixture.item("B.kanban/\(Ident.lane3)/\(twin)", Item.rich(order: "2048", title: "Stale Twin"))
let twinBefore = try fixture.indexData("B.kanban/\(Ident.lane3)/\(twin)")
let result = try move(
fixture, "A.kanban/\(Ident.lane1)/\(arriving)",
to: "B.kanban/\(Ident.lane4)", into: "B.kanban"
)
let minted = result.id.rawValue
#expect(BoardLoader.isUUIDShaped(minted))
#expect(ItemID(rawValue: minted) != ItemID(rawValue: arriving))
#expect(ItemID(rawValue: minted) != ItemID(rawValue: twin))
#expect(result.reminted == [MoveResult.Remint(from: ItemID(rawValue: arriving), to: ItemID(rawValue: minted))])
#expect(try FrontmatterDocument.parse(fixture.indexText("B.kanban/\(Ident.lane4)/\(minted)")).title
== .valid("Card One"))
// The resident twin is untouched, and the source left as for any move.
#expect(try fixture.indexData("B.kanban/\(Ident.lane3)/\(twin)") == twinBefore)
#expect(!fixture.exists("A.kanban/\(Ident.lane1)/\(arriving)"))
}
/// The same rule one level down: a lane arrives carrying a card whose UUID the destination
/// board already holds under a different case-spelling that card, and only that card, is
/// reminted.
@Test func aLaneMoveRemintsAChildCollidingOnlyByCaseSpelling() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try boardA(fixture)
try boardB(fixture)
let arriving = "abcdef01-2345-6789-abcd-ef0123456789"
let twin = arriving.uppercased()
try fixture.item("A.kanban/\(Ident.lane1)/\(arriving)", Item.rich(order: "3072", title: "Colliding"))
try fixture.item("B.kanban/\(Ident.lane3)/\(twin)", Item.rich(order: "2048", title: "B's Own"))
let result = try move(fixture, "A.kanban/\(Ident.lane1)", to: "B.kanban", into: "B.kanban")
#expect(result.id.rawValue == Ident.lane1)
#expect(result.reminted.map(\.from.rawValue) == [arriving])
let minted = try #require(result.reminted.first?.to.rawValue)
#expect(fixture.exists("B.kanban/\(Ident.lane1)/\(minted)"))
#expect(!fixture.exists("B.kanban/\(Ident.lane1)/\(arriving)"))
// The card that collided with nothing kept its identity.
#expect(fixture.exists("B.kanban/\(Ident.lane1)/\(Ident.card1)"))
}
/// The collision sitting in the very lane being dropped into the case a move-then-rename
/// could not repair, because the plain move would fail on the existing name.
@Test func aCollisionInTheDestinationParentItselfIsStillReminted() throws {