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:
@@ -105,6 +105,21 @@ final class CardWindowSession: CardSessionFlushing {
|
||||
/// object's to run, which is also true.
|
||||
var registerSessionStep: (@MainActor (CardWindowUndo, @escaping @MainActor () -> Void) -> Bool)?
|
||||
|
||||
/// **Where the close gives up this window's ownership of its card's comment trash** —
|
||||
/// `BoardStore.cardWindowDidClose(inCard:)`, wired by the host beside `registerSessionStep`
|
||||
/// (13-native-undo.md ▸ Interaction with the trash, ruled 2026-08-06):
|
||||
///
|
||||
/// > "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 ... the close then
|
||||
/// > settles by the same gate."
|
||||
///
|
||||
/// A seam rather than a store reference, `registerSessionStep`'s reason: this object is a
|
||||
/// lifecycle and stays testable by having no idea what a board is. `nil` — a window that never
|
||||
/// joined its board — owns nothing on any store, which is also true.
|
||||
///
|
||||
/// Its **order** is the load-bearing part, and `endSession()` is where it is spelled.
|
||||
var endCardWindowOwnership: (@MainActor () -> Void)?
|
||||
|
||||
/// The Edit buffer's dirty text, a typed-in raw-source outlet, or an inline comment edit session
|
||||
/// holding keystrokes its file has not got — see `CardSessionFlushing`.
|
||||
///
|
||||
@@ -199,6 +214,15 @@ final class CardWindowSession: CardSessionFlushing {
|
||||
// still restores comments out of it. Registering answers whether the step took the purge on —
|
||||
// and a board whose substrate keeps no steps has already run it by the time that answer comes
|
||||
// back, which is how Pro keeps purging at the close flush without a word about tiers here.
|
||||
//
|
||||
// **This window stops owning its card's comment trash first** (ruled 2026-08-06 — the
|
||||
// open-window carve-out). While the window is open the store defers every purge of this
|
||||
// card's trash, because the entries this session deleted are backed by *this* stack's fine
|
||||
// steps and the board-stack inventory cannot see them. The close is where that stops being
|
||||
// true, and it has to stop being true **before** either branch below runs: a no-step close
|
||||
// that unmarked itself afterwards would defer its own purge into a no-op, and a coarse step
|
||||
// registered here can retire in the same breath (its registration clears the redo stack).
|
||||
endCardWindowOwnership?()
|
||||
let purge: @MainActor () -> Void = { [comments] in comments.purgeTrashNow() }
|
||||
if registerSessionStep?(undo, purge) != true {
|
||||
purge()
|
||||
@@ -609,7 +633,17 @@ struct CardWindowHost: View {
|
||||
/// The pane's two window-scoped facts are set *before* the read, because both of them are things
|
||||
/// the read's results are resolved against: the folder every comment's attachments hang off, and
|
||||
/// whether the lock is on.
|
||||
///
|
||||
/// **And the window takes ownership of its card's comment trash here** (13-native-undo.md ▸
|
||||
/// Interaction with the trash, ruled 2026-08-06): "an open card window is itself an owner of its
|
||||
/// card's comment trash". From this line until `CardWindowSession.endSession` gives it back, a
|
||||
/// purge of this card's trash — a foreign step's retirement, another window's close — defers
|
||||
/// entirely, because the fine steps backing this session's deletes live on a stack the board's
|
||||
/// inventory cannot see. The sweep below is unaffected and runs as it always has: it is gated on
|
||||
/// `backedContent`, and a window that has not made a gesture yet holds nothing that gate is
|
||||
/// missing.
|
||||
private func openCommentThread(store: BoardStore) {
|
||||
store.cardWindowDidOpen(inCard: ref.cardIdentity)
|
||||
if case let .shows(placement) = Self.cardWindowFate(cardID: ref.cardID, in: store.snapshot) {
|
||||
session.comments.cardFolder = Self.cardFolder(root: store.rootURL, placement: placement)
|
||||
}
|
||||
@@ -689,6 +723,11 @@ struct CardWindowHost: View {
|
||||
session.registerSessionStep = { [weak store] undo, purge in
|
||||
store?.registerCardSession(undo, inCard: cardID, retiring: purge) ?? false
|
||||
}
|
||||
// The close half of the open-window carve-out (13 ▸ Interaction with the trash, ruled
|
||||
// 2026-08-06). Its open half is `CardWindowHost.openCommentThread`, beside the residue sweep;
|
||||
// the pair has to be wired from the two places the window already calls into the store, and
|
||||
// these are they. A released store owns nothing to give up, which is what `weak` says.
|
||||
session.endCardWindowOwnership = { [weak store] in store?.cardWindowDidClose(inCard: cardID) }
|
||||
}
|
||||
|
||||
/// Points the comments pane at its card — **the one place every comment gesture learns which card
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -57,11 +57,12 @@ public enum CommentTarget: Sendable, Equatable {
|
||||
/// - **`saveCommentDraft` registers nothing**: a draft is a durable file being edited in place, with
|
||||
/// no session boundary to coalesce at and no meaning for "undo" that emptying it does not already
|
||||
/// have (05-card-window.md ▸ The comments column).
|
||||
/// - **`purgeCommentTrash` registers nothing** — the permanent-delete posture. It is also what makes
|
||||
/// the interaction with the stack correct for free: leftover comment steps go stale after a purge
|
||||
/// and skip with the ordinary info-tone banner, because the folders their expectations name are
|
||||
/// gone. **The expectations validate disk, never the snapshot** (`HistoryStaleness`), which is
|
||||
/// exactly why this works — comments are not in the snapshot at all.
|
||||
/// - **`purgeCommentTrash` registers nothing** — the permanent-delete posture. What it *does* consult
|
||||
/// is the stack: since "every purge of `comments/.trash/` is per-entry behind the ownership gate"
|
||||
/// (13 ▸ Interaction with the trash, ruled 2026-08-06) a purge removes only what no live step backs,
|
||||
/// so it can no longer stale a step by construction. Lazy invalidation still stands for the
|
||||
/// staleness a *foreign* hand leaves — **the expectations validate disk, never the snapshot**
|
||||
/// (`HistoryStaleness`), which is exactly why that works: comments are not in the snapshot at all.
|
||||
@MainActor
|
||||
extension BoardStore {
|
||||
|
||||
@@ -309,30 +310,65 @@ extension BoardStore {
|
||||
|
||||
// MARK: The purge, and the crash residue it leaves
|
||||
|
||||
/// **Empties one card's `comments/.trash/`** — **when undo no longer needs it** (01-storage-format.md
|
||||
/// § Enhanced schema, re-ruled 2026-07-31; 13-native-undo.md ▸ Interaction with the trash).
|
||||
/// **Purges one card's `comments/.trash/`, entry by entry, of everything undo no longer needs**
|
||||
/// (01-storage-format.md § Enhanced schema, re-ruled 2026-07-31; 13-native-undo.md ▸ Interaction
|
||||
/// with the trash, re-ruled 2026-08-06).
|
||||
///
|
||||
/// The call site moved with that re-ruling and this method did not change: the window's close no
|
||||
/// longer purges on its own, it hands this work to the coarse close step as that step's
|
||||
/// **retirement** (`HistoryStep.Retirement`), and the purge runs when the step leaves the board
|
||||
/// stack **cleanly** — undone-and-superseded, or dropped off the end — or when the board session
|
||||
/// ends. A **stale skip is not a clean exit** and does not purge (the decoupling ruled
|
||||
/// 2026-07-31): that step's backing survives to session end instead, where `clear()` retires it
|
||||
/// and this method finally runs. On a git board the step is never kept, so the retirement fires at
|
||||
/// the close flush, which is where the purge always ran there ("purge rides the close flush").
|
||||
/// The call site moved with the 2026-07-31 re-ruling: the window's close no longer purges on its
|
||||
/// own, it hands this work to the coarse close step as that step's **retirement**
|
||||
/// (`HistoryStep.Retirement`), and the purge runs when the step leaves the board stack **cleanly**
|
||||
/// — undone-and-superseded, or dropped off the end — or when the board session ends. A **stale
|
||||
/// skip is not a clean exit** and does not purge (the decoupling ruled 2026-07-31): that step's
|
||||
/// backing survives to session end instead, where `clear()` retires it and this method finally
|
||||
/// runs. On a git board the step is never kept, so the retirement fires at the close flush, which
|
||||
/// is where the purge always ran there ("purge rides the close flush").
|
||||
///
|
||||
/// **It empties the folder whole, and its two callers are exactly the moments that is right**: a
|
||||
/// close that registered no step (nothing took the hold), and the retirement of the step that
|
||||
/// did (the hold has just ended). The *sweep* cannot assume either, which is why it purges per
|
||||
/// entry behind the ownership gate — `sweepCommentTrashResidue(inCard:)`.
|
||||
/// ### Entries, not the container — the ownership gate, on this side too
|
||||
///
|
||||
/// One bracket, no step. Leftover comment steps on a stack are not pruned here and must not be:
|
||||
/// invalidation is lazy (13 ▸ Rules), so they stay on the stack, look full, and skip with the
|
||||
/// ordinary info-tone banner the first time one is crossed.
|
||||
/// What changed on 2026-08-06 is *what* a purge is allowed to remove:
|
||||
///
|
||||
/// > "**Every purge of `comments/.trash/` is per-entry behind the ownership gate** (ruled
|
||||
/// > 2026-08-06 — the container-whole retirement purge retires): a step's retirement and a no-step
|
||||
/// > close remove only entries no live step still backs — the same `backedContent` inventory the
|
||||
/// > sweep consults, making it one condition, *three* consumers. The container-whole purge assumed
|
||||
/// > one owning step per card's comment trash, and two sessions over the same card broke it: the
|
||||
/// > second step's retirement — or a mere reopen-and-close that registered nothing — emptied the
|
||||
/// > first step's backing out from under it, silently killing an undo the stack still promised.
|
||||
/// > Under the gate a purge cannot stale a live step by construction; an entry that outlives its
|
||||
/// > owner is collected by whichever consumer runs next (the next retirement, close, open-time
|
||||
/// > sweep, or session end — convergence, not a leak)."
|
||||
///
|
||||
/// So this reads the same inventory `sweepCommentTrashResidue(inCard:)` reads, through the same
|
||||
/// filter (`unownedTrashedComments`), and removes with the same per-entry primitive. The two
|
||||
/// consumers differ only in their bracket and their trigger, which is all they ever should have.
|
||||
///
|
||||
/// ### And the carve-out the inventory cannot cover
|
||||
///
|
||||
/// > "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."
|
||||
///
|
||||
/// `openCardWindows` is that ownership, and the deferral is total rather than per entry: a live
|
||||
/// session's fine steps are on the *window's* stack, so `backedContent` is not merely incomplete
|
||||
/// about them, it is silent. The close is where the answer becomes knowable again — its coarse
|
||||
/// step becomes the owner, or the no-step close arrives here with the window already unmarked
|
||||
/// (`BoardStore.cardWindowDidClose(inCard:)`, run first by `CardWindowSession.endSession`).
|
||||
///
|
||||
/// **One bracket, no step — and no bracket at all for an empty remainder**, which is every close
|
||||
/// on a card nothing was deleted in. Leftover comment steps on a stack are not pruned here and
|
||||
/// must not be: invalidation is lazy (13 ▸ Rules), so a step whose backing something *else* removed
|
||||
/// stays on the stack, looks full, and skips with the ordinary info-tone banner the first time it
|
||||
/// is crossed. Under the gate this method is no longer one of those somethings.
|
||||
public func purgeCommentTrash(inCard id: ItemID) {
|
||||
// The carve-out, before anything else: an open window owns its card's whole comment trash.
|
||||
guard !openCardWindows.contains(id) else { return }
|
||||
guard let card = commentSubject(id) else { return }
|
||||
let folder = card.folder
|
||||
let unowned = unownedTrashedComments(inCard: id, at: folder)
|
||||
guard !unowned.isEmpty else { return }
|
||||
try? performWrite { () throws(BoardWriteError) -> Void in
|
||||
_ = try BoardWriter.purgeCommentTrash(inCard: card.folder)
|
||||
try Self.purgeTrashedComments(unowned, inCard: folder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,16 +401,22 @@ extension BoardStore {
|
||||
/// **A step a stale skip popped still counts as an owner** (the skip-purge decoupling, ruled
|
||||
/// 2026-07-31): "a stale-skipped step's backing instead survives to board-session end ... the skip
|
||||
/// is exactly when the user may want to inspect what the collision left". Nothing here says so —
|
||||
/// that is `backedContent`'s answer, and keeping it there is what makes the pair one condition
|
||||
/// read twice rather than two conditions kept in step by hand.
|
||||
/// see `unownedTrashedComments(inCard:at:)`, which is where the whole gate now lives.
|
||||
///
|
||||
/// ### Entries, not the container
|
||||
/// ### Entries, not the container — which is now every purge's posture
|
||||
///
|
||||
/// Which is why this no longer calls `BoardWriter.purgeCommentTrash(inCard:)` — that empties the
|
||||
/// folder whole, and the whole folder is exactly what this may not assume it owns. The per-entry
|
||||
/// primitive is the same one an undone create removes its folder with, over the entries the
|
||||
/// listing already narrowed to identity shape; a hand-editor's stray in there keeps the verbatim
|
||||
/// posture either way.
|
||||
/// This was the first consumer to purge per entry, because the whole folder is exactly what it may
|
||||
/// not assume it owns. It is no longer the only one: **"every purge of `comments/.trash/` is
|
||||
/// per-entry behind the ownership gate ... the same `backedContent` inventory the sweep consults,
|
||||
/// making it one condition, *three* consumers"** (13 ▸ Interaction with the trash, ruled
|
||||
/// 2026-08-06). The container-whole `BoardWriter.purgeCommentTrash(inCard:)` retired from
|
||||
/// production with that ruling; what this and `purgeCommentTrash(inCard:)` share is the filter
|
||||
/// (`unownedTrashedComments`) and the removal (`purgeTrashedComments`), so the two can differ only
|
||||
/// in bracket and trigger.
|
||||
///
|
||||
/// The per-entry primitive is the same one an undone create removes its folder with, over the
|
||||
/// entries the listing already narrowed to identity shape; a hand-editor's stray in there keeps
|
||||
/// the verbatim posture either way.
|
||||
///
|
||||
/// The rest is the same six steps every scheduled heal gets, through the same engine: **rest**
|
||||
/// when there is nothing unowned (which is every open on a board that closed cleanly, and costs no
|
||||
@@ -392,21 +434,48 @@ extension BoardStore {
|
||||
public func sweepCommentTrashResidue(inCard id: ItemID) {
|
||||
guard let card = commentSubject(id) else { return }
|
||||
let folder = card.folder
|
||||
// The board's stack, never a window's: a window's own fine comment steps die with the window
|
||||
// that owns them, and the window this sweep runs for has not made a gesture yet. What can
|
||||
// outlive a close is the coarse step the close folded the session into, and that is here.
|
||||
let backed = history?.backedContent ?? []
|
||||
let residue = CommentThread.trashedCommentIDs(inCard: folder).filter { commentID in
|
||||
!backed.contains(.trashedComment(commentID, inCard: id))
|
||||
}
|
||||
let residue = unownedTrashedComments(inCard: id, at: folder)
|
||||
heals.run(
|
||||
.commentTrashResidue,
|
||||
signature: Set(residue.map { "comment-trash:\(card.path)/\($0.rawValue)" }),
|
||||
on: self
|
||||
) { () throws(BoardWriteError) -> Void in
|
||||
for commentID in residue {
|
||||
try BoardWriter.purgeItem(at: CommentThread.trashedCommentFolder(commentID, inCard: folder))
|
||||
}
|
||||
try Self.purgeTrashedComments(residue, inCard: folder)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: The one condition the three consumers read
|
||||
|
||||
/// **The entries in one card's `comments/.trash/` no live step is holding** — the ownership gate,
|
||||
/// as a list (13-native-undo.md ▸ Interaction with the trash, ruled 2026-07-31 and widened
|
||||
/// 2026-08-06 to "every purge").
|
||||
///
|
||||
/// **The board's stack, never a window's**: a window's own fine comment steps die with the window
|
||||
/// that owns them, and what can outlive a close is the coarse step the close folded the session
|
||||
/// into, which is here. That is also exactly why an *open* window is given ownership of its card's
|
||||
/// whole trash by `purgeCommentTrash(inCard:)` rather than by anything in this list — a live
|
||||
/// session's holds are not merely missing from `backedContent`, they are invisible to it.
|
||||
///
|
||||
/// **A step a stale skip popped still counts as an owner** (the skip-purge decoupling, ruled
|
||||
/// 2026-07-31). Nothing here says so — that is `backedContent`'s answer, and keeping it there is
|
||||
/// what makes the pair one condition read three times rather than three conditions kept in step by
|
||||
/// hand.
|
||||
private func unownedTrashedComments(inCard id: ItemID, at folder: URL) -> [ItemID] {
|
||||
let backed = history?.backedContent ?? []
|
||||
return CommentThread.trashedCommentIDs(inCard: folder).filter { commentID in
|
||||
!backed.contains(.trashedComment(commentID, inCard: id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the named entries from one card's `comments/.trash/`, in order — the removal half the
|
||||
/// sweep and the purge share, so "per entry, never the container" has one spelling.
|
||||
///
|
||||
/// A failure stops the batch and throws, and everything already removed stays removed:
|
||||
/// `purgeItem(at:)`'s posture, unchanged, and the reason the callers can each keep their own
|
||||
/// bracket around it.
|
||||
private static func purgeTrashedComments(_ ids: [ItemID], inCard folder: URL) throws(BoardWriteError) {
|
||||
for commentID in ids {
|
||||
try BoardWriter.purgeItem(at: CommentThread.trashedCommentFolder(commentID, inCard: folder))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3084,8 +3084,10 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
|
||||
/// note is the precedent one level up.
|
||||
case deleteComment(title: String?)
|
||||
|
||||
/// `comments/.trash/` emptied — at card-window close, and as the crash-residue sweep at the next
|
||||
/// open (§ Enhanced schema).
|
||||
/// `comments/.trash/` emptied whole — the container-wide purge
|
||||
/// (`BoardWriter.purgeCommentTrash(inCard:)`), which 13-native-undo.md retired from production on
|
||||
/// 2026-08-06: both purge consumers now remove per entry behind the ownership gate, through
|
||||
/// `purgeItem(at:)` and therefore under `.purge`. Kept with its primitive — see that method.
|
||||
///
|
||||
/// **No payload, unlike its four siblings**, and for `.agentGuide`'s reason: this is bookkeeping
|
||||
/// the app does on its own over a folder that is "never a UI surface", with one outcome nobody
|
||||
|
||||
@@ -264,10 +264,26 @@ extension BoardWriter {
|
||||
try moveComment(trashed, into: CommentThread.folder(inCard: cardFolder), operation: operation)
|
||||
}
|
||||
|
||||
/// **Empties one card's `comments/.trash/`** — at card-window close, and as the crash-residue
|
||||
/// sweep at the next open (01-storage-format.md § Enhanced schema: "purged when the card window
|
||||
/// closes (rides the close flush; crash residue sweeps at the next card-window open,
|
||||
/// armed-then-cleared like every heal memo)").
|
||||
/// **Empties one card's `comments/.trash/` whole** — and **no production caller does that any
|
||||
/// more** (13-native-undo.md ▸ Interaction with the trash, ruled 2026-08-06):
|
||||
///
|
||||
/// > "**Every purge of `comments/.trash/` is per-entry behind the ownership gate** (ruled
|
||||
/// > 2026-08-06 — the container-whole retirement purge retires) ... The container-whole purge
|
||||
/// > assumed one owning step per card's comment trash, and two sessions over the same card broke
|
||||
/// > it."
|
||||
///
|
||||
/// Both consumers — the retirement/no-step-close purge and the open-time residue sweep — now
|
||||
/// filter by `HistoryProviding.backedContent` and remove per entry through `purgeItem(at:)`
|
||||
/// (`BoardStore.purgeCommentTrash(inCard:)`, `BoardStore.sweepCommentTrashResidue(inCard:)`). A
|
||||
/// primitive that empties the container cannot express the gate, so nothing above it may call this.
|
||||
///
|
||||
/// It is **kept rather than deleted**, deliberately and narrowly: `WriteOperation.purgeCommentTrash`
|
||||
/// is the vocabulary's word for this work and owns a bespoke user-facing sentence
|
||||
/// (`BannerCenter` — "Couldn't tidy up deleted comments") plus rows in three exhaustive switches,
|
||||
/// and retiring the case would be a ripple across files for no behavioural gain. Read this as
|
||||
/// legacy: the shape the ruling retired, still exercised by `CommentWriteTests` so the case's
|
||||
/// plumbing stays honest, and the home of the operation vocabulary the gated purge's per-entry
|
||||
/// failures do not use.
|
||||
///
|
||||
/// `emptyTrash`'s rules, one level down and for its reasons:
|
||||
///
|
||||
@@ -278,9 +294,7 @@ extension BoardWriter {
|
||||
/// removed stays removed.
|
||||
/// - A card with no thread trash removes nothing and answers `[]`.
|
||||
///
|
||||
/// **It registers no undo step** — the permanent-delete posture (13-native-undo.md), which is
|
||||
/// also what makes the leftover comment steps on the board stack go stale and skip with a banner
|
||||
/// rather than resurrect a folder that is gone.
|
||||
/// **It registers no undo step** — the permanent-delete posture (13-native-undo.md).
|
||||
@discardableResult
|
||||
public static func purgeCommentTrash(inCard cardFolder: URL) throws(BoardWriteError) -> [ItemID] {
|
||||
let operation = WriteOperation.purgeCommentTrash
|
||||
|
||||
Reference in New Issue
Block a user