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
+15
View File
@@ -106,8 +106,23 @@ enum BoardTreeCopy {
throw .failed(url: source, error: error)
}
// The one exclusion this walk makes without being asked (01-storage-format.md § Enhanced
// schema: `comments/.trash/` is "**stripped at every copy boundary** (clipboard staging,
// Duplicate, Save as Template) a copy must not carry ghosts no window session will ever
// purge"). Unconditional because all three of this walk's callers *are* copy boundaries, and
// an opt-in flag would be three call sites agreeing to one rule instead of one rule.
//
// It is the one depth-sensitive exclusion, hence the parent check rather than a name in
// `excluded`: `.trash` at a board root is the board's own and carries or not per each flow's
// stated exclusions, while `.trash` inside a `comments/` is undo's backing store and never
// travels.
let isCommentThread = source.lastPathComponent.lowercased() == IntegrityRules.commentsFolderName
for entry in entries.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) {
guard !excluded.contains(entry.lastPathComponent.lowercased()) else { continue }
guard !isCommentThread
|| entry.lastPathComponent.lowercased() != IntegrityRules.commentTrashFolderName
else { continue }
// Between items, never mid-item: this is the whole of "checks cancellation between
// items", and the reason the copy is a walk at all.