Every comment-trash purge kneels to the ownership gate — the container-whole retirement retires

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-06 17:10:09 -04:00
parent a60d97689e
commit ea15d1ac74
7 changed files with 449 additions and 55 deletions
+42
View File
@@ -541,6 +541,48 @@ public final class BoardStore: HealHost {
@ObservationIgnored
public var commitSeam: HistoryCommitSeam?
// MARK: The open card windows
/// **Which of this board's cards have a card window open right now** one fact, kept for one
/// consumer: the `comments/.trash/` purge's ownership gate (13-native-undo.md Interaction with
/// the trash, ruled 2026-08-06).
///
/// > "One carve-out: **an open card window is itself an owner of its card's comment trash** a
/// > retirement firing while the card's window is open defers its purge to that window's close,
/// > because entries deleted in the live session are backed by the window's fine steps, which the
/// > board-stack inventory cannot see; the close then settles by the same gate."
///
/// A window's fine comment steps live on the *window's* stack (`CardWindowUndo`), which
/// `history.backedContent` the board's stack cannot see. So while a window is open the
/// inventory is knowingly incomplete for that card, and the only honest answer to a purge is *not
/// yet*. `purgeCommentTrash(inCard:)` is the one reader.
///
/// **Deliberately not a reference to the window.** The store is the *board's*, a card window is a
/// scene, and 02-architecture.md § Components has the ownership pointing the other way; what the
/// gate needs is an identity, not an object. It is also why this is a `Set` rather than a count:
/// a card window's identity is `(board, card)` (`CardWindowRef`), so a card has at most one.
///
/// A window that never unmarked a store torn down under an open window costs nothing beyond a
/// purge that did not run, which the next open's residue sweep collects. Convergence, not a leak.
@ObservationIgnored
public private(set) var openCardWindows: Set<ItemID> = []
/// **A card window opened over this card** called from the window's open, beside the crash-residue
/// sweep it already runs there (`CardWindowHost.openCommentThread`).
public func cardWindowDidOpen(inCard id: ItemID) {
openCardWindows.insert(id)
}
/// **A card window over this card is closing** called from the window's session end, and
/// **before** that close's own purge (`CardWindowSession.endSession`).
///
/// The order is the whole of the carve-out's honesty: the close is the moment the window stops
/// being an owner, so a close that unmarked itself *after* running its purge would defer that
/// purge into a no-op and hand the work to nobody.
public func cardWindowDidClose(inCard id: ItemID) {
openCardWindows.remove(id)
}
// MARK: Reload machinery
/// Monotonic id of the most recently *started* reload and therefore also the number of tree