Comments, phase 1 — storage, writer primitives, and the undo inventory

The kind: comment field table lands in IntegrityRules (the per-kind
hook's first exercise), CommentThread reads one card's thread
window-scoped (the board walk stays O(cards)), and CommentWriter gains
the five gestures: draft save, post (rename .draft to a fresh UUID,
created/modified restamped in the bracket), edit, delete into
comments/.trash/, and the purge with its crash-residue memo. Post and
delete register move-based undo steps; draft saves, edits, and the
purge deliberately register nothing (13's no-capture rule). Copy
boundaries strip comments/.trash, carry .draft verbatim, and remint
threads; comments graduates to a displacing claimed name, with .draft,
.trash, and a comment's attachments claimed one level down.
CommentPath classifies changed paths into the 06 verb family for
later announcer/composer wiring.

One stated narrowing pending a ruling (filed on the findings board):
the copy transaction's refuse-whole preflight stays cards-and-lanes —
an unstampable copied comment copies verbatim with a log line, because
comment defects never refuse.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-30 19:36:21 -04:00
parent e6dd4c0aa6
commit f68ac3668e
16 changed files with 2881 additions and 63 deletions
+12 -9
View File
@@ -349,15 +349,18 @@ struct CardClaimedNameTests {
#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 {
/// **`comments` graduated** (2026-07-30, with the comment storage) the timing principle run
/// forwards: the name became load-bearing, so its squatter joined the scheduled class exactly as
/// 01 said it would ("a wrong-kind holder is a tolerated stray today and joins the scheduled class
/// the day the name becomes load-bearing"). This test used to pin the absence of the defect.
@Test("A file on a card's comments is displaced now that the thread consumes the name")
func fileOnCommentsIsADefect() 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.claimedNameSquatters.map(\.name) == ["comments"])
#expect(result.looseCardFiles.isEmpty, "a reserved name is not a loose file either")
}
@@ -447,14 +450,14 @@ struct CardClaimedNameTests {
#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")
/// The table is the whole rule pinned so the level-uniform claim is stated rather than being an
/// accident of the probe's implementation. Both card-level names displace since `comments`
/// graduated with the comment storage (2026-07-30).
@Test("The card-level table claims attachments and comments, and both displace")
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)
#expect(names.allSatisfy { $0.displacesSquatters })
}
}