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
+25
View File
@@ -848,6 +848,31 @@ public final class BannerCenter {
// The buffer is still on screen: the banner says the app could not put those bytes on
// disk, not that they are gone.
if let title { "Couldn't apply source changes to '\(title)'" } else { "Couldn't apply the source changes" }
// **The comment family names the card, because that is the title these operations carry**
// a comment has none of its own (01-storage-format.md § Enhanced schema: "No `title`, no
// `order`"), and the card is the thing on the window in front of them. Each sentence is the
// gesture's own word, the vocabulary's standing rule: they pressed , or Delete, or nothing
// at all in the draft's case.
case let .saveCommentDraft(title):
// **"draft", never "save the comment"**: nothing has been posted, and a banner claiming a
// comment could not be saved would name one that does not exist yet. The typed text is
// still in the composer this says the app could not put it on disk.
if let title { "Couldn't save your draft comment on '\(title)'" } else { "Couldn't save your draft comment" }
case let .postComment(title):
// The Comment button's own word (05-card-window.md The comments column, " posts").
// The draft is untouched on disk, which is what makes this a retry rather than a loss.
if let title { "Couldn't post your comment on '\(title)'" } else { "Couldn't post your comment" }
case let .editComment(title):
if let title { "Couldn't save your edit to a comment on '\(title)'" } else { "Couldn't save your edit to the comment" }
case let .deleteComment(title):
if let title { "Couldn't delete a comment on '\(title)'" } else { "Couldn't delete the comment" }
case .purgeCommentTrash:
// No title, and no mention of a trash the user has never seen: `comments/.trash/` is
// "never a UI surface", so the sentence is about the *card's* files being tidied the
// agent guide's posture, one level down. Nothing is lost either way; the next window
// open sweeps again.
"Couldn't tidy up deleted comments"
}
}