Realign code with the 2026-07-31 findings-resolution rulings
The full bullet list from Implementation card bf080d9a — both ruling batches, including the three appended mid-session by16ef377: - Restore subjects compose the inverse, never nest: crossing "Undo: S" emits "Redo: S" and vice versa; parity, not stack depth, reads a legacy double prefix (GitHistoryProvider.restoreSubject). - Git-operation failures join the one-shot failure banner tier: BannerCenter.GitFailureBanner (undo/redo/branchSwitch/addGit), error tone at failure rank merged with write one-shots by recency; the postLoss compromise is retired at both AppModel wirings. - order/schema optional below the board root: append-at-end reading (ordered siblings first, folder-name tie-break among the order-less), schema reads 1, both coerce-tier logged; the root keeps its requirements. Ranks.resolvedOrders materializes finite ranks so models and placement math stay untouched; first Writer rewrite stamps a real rank on touch, placement against an order-less sibling stamps that sibling inline in the same bracket. Agent guide v10 teaches optional keys and zero-read filing. Hostile-YAML order shapes become coercion tests; Fixtures/Valid/optional-keys.kanban replaces the four retired Malformed boards. - .gitignore is the relocation-heal noise gate: GitignoreRules pure matcher (standard semantics, board-root file only), loader consults it once per walk so matched loose files keep the stray posture; seeded (.DS_Store + .*.lanework-*) at board creation and template instantiation, healed in when missing at open — repo-nested included; empty file honored, existing files never edited; the committer's obedience via libgit2 status is pinned by test. - Comments crash-residue sweep gates on step ownership: HistoryStep derives backing from its own undo expectations, backedContent unions both stacks, the sweep purges per-entry only what no live step owns. - Skip-purge decoupled (16ef377): a stale-skipped coarse step strands whole in NativeHistoryProvider.strandedSteps — still backing, retired only at session end; clean exits purge as before. - Coarse close step named "Changes to '<card>'"; the fine body-edit wording never leaks onto the board menu. - Branch-switch settle clears every open card window's fine stack on Save All and Discard alike; the empty fold registers no coarse step. - Close flush awaits its covering snapshot (quiesce + one generation bump, 1s bound), and an explicit flush now queues behind an in-flight one instead of skipping — the audit-caught interleaving could lose a close flush permanently when the debounce fired inside the close sequence; regression tests force both races. - Commit comment bullets sort chronologically by created, not UUID. - The production-unwired CardBodyEditSession.editSessionDidChange seam is deleted with its seam-only tests. - Composition-root pins: beginSession composes the committer with the store's own EchoLedger and binds the announcer (the miswire class). - Deliberate 06 conformance pass over every 2026-07-31-tagged sentence: fixed Change-custom-key subjects (the retired named generic was the only producer), the unbuilt Replace attachment vocabulary, heal commits now authored Lanework Integrity, the config reader scopes identity to plain [user] sections, add-git re-runs detection at create (a stale mode-none could initialize inside the user's repo), and add-git failures answer at the form or the banner. Structural residue filed on the Redesign board. 2554 tests / 439 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -261,6 +261,12 @@ extension BoardStore {
|
||||
/// old stale-after-close skip scenario cannot arise". What reaches the board is the close step
|
||||
/// that folds it, whose undo restores the comment from a `comments/.trash/` the same step is
|
||||
/// keeping alive (`registerCardSession`).
|
||||
///
|
||||
/// The `.present(trashed)` half of the undo's expectations is also this step's **backing claim**,
|
||||
/// read off it by `HistoryStep.backing(declaredBy:)` and carried into the coarse step by the fold:
|
||||
/// it is what tells the next window open's residue sweep that the folder in `comments/.trash/` is
|
||||
/// a live step's, not a dead session's leftovers (13 ▸ Interaction with the trash, ruled
|
||||
/// 2026-07-31).
|
||||
@discardableResult
|
||||
public func deleteComment(_ commentID: ItemID, inCard id: ItemID, on window: CardWindowUndo? = nil) -> Bool {
|
||||
guard let card = commentSubject(id) else { return false }
|
||||
@@ -309,10 +315,16 @@ extension BoardStore {
|
||||
/// 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 — undone-and-superseded, dropped, gone stale — or when the board session ends. 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 crash-residue sweep at the next
|
||||
/// card-window open is unchanged.
|
||||
/// 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:)`.
|
||||
///
|
||||
/// 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
|
||||
@@ -327,10 +339,49 @@ extension BoardStore {
|
||||
/// **The crash-residue sweep**, run when a card window opens (§ Enhanced schema: "crash residue
|
||||
/// sweeps at the next card-window open, armed-then-cleared like every heal memo").
|
||||
///
|
||||
/// The same six steps every scheduled heal gets, through the same engine: **rest** when the trash
|
||||
/// is empty (which is every open on a board that closed cleanly, and costs no bracket at all),
|
||||
/// defer under a read-only lock, compare the signature, arm before attempting, one bracket, clear
|
||||
/// on success.
|
||||
/// ### Residue is what no live step owns — the stack is asked first
|
||||
///
|
||||
/// The ruling of 2026-07-31 (13-native-undo.md ▸ Interaction with the trash; § Enhanced schema's
|
||||
/// "residue defined as content no live coarse step owns"):
|
||||
///
|
||||
/// > "residue is defined by the purge-deferral condition itself: `comments/.trash/` content
|
||||
/// > referenced by a live coarse step on the board stack is a step's **backing, not residue** —
|
||||
/// > the open-time sweep consults the stack and skips owned content, re-arming when the owning
|
||||
/// > step leaves the stack (which is exactly when the deferred purge wanted to run; one condition,
|
||||
/// > two consumers). **Reopening a window can therefore never destroy its prior session's undo
|
||||
/// > backing.** Unowned content sweeps as before."
|
||||
///
|
||||
/// The defect it closes is the reopen: close a window that deleted a comment (the coarse step
|
||||
/// registers, its undo restores from `comments/.trash/`), open the same card again, and this sweep
|
||||
/// used to empty the trash out from under a step still sitting on the board's stack — leaving a
|
||||
/// ⌘Z that could only skip.
|
||||
///
|
||||
/// So the gate is `HistoryProviding.backedContent`, and it is the same fact the deferred purge
|
||||
/// waits on, asked as an inventory instead of as a moment. Nothing here has to *re-arm* by hand:
|
||||
/// when the owning step retires, its retirement runs the deferred purge and the content is gone;
|
||||
/// and a sweep that found nothing left to do rests, which clears the memo (`HealScheduler` step 1)
|
||||
/// so a later picture is a fresh one.
|
||||
///
|
||||
/// **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.
|
||||
///
|
||||
/// ### Entries, not the container
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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
|
||||
/// bracket at all), defer under a read-only lock, compare the signature, arm before attempting,
|
||||
/// one bracket, clear on success. The signature is computed from **the entries actually purged**,
|
||||
/// so the picture the memo remembers is the work that was attempted rather than everything the
|
||||
/// trash happened to hold.
|
||||
///
|
||||
/// **Silent** — `HealNotice.none`. `comments/.trash/` is "never a UI surface", and the residue is
|
||||
/// the app's own leftovers from a session that died; there is nothing here a user could act on.
|
||||
@@ -341,13 +392,21 @@ extension BoardStore {
|
||||
public func sweepCommentTrashResidue(inCard id: ItemID) {
|
||||
guard let card = commentSubject(id) else { return }
|
||||
let folder = card.folder
|
||||
let residue = CommentThread.trashedCommentIDs(inCard: 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))
|
||||
}
|
||||
heals.run(
|
||||
.commentTrashResidue,
|
||||
signature: Set(residue.map { "comment-trash:\(card.path)/\($0.rawValue)" }),
|
||||
on: self
|
||||
) { () throws(BoardWriteError) -> Void in
|
||||
_ = try BoardWriter.purgeCommentTrash(inCard: folder)
|
||||
for commentID in residue {
|
||||
try BoardWriter.purgeItem(at: CommentThread.trashedCommentFolder(commentID, inCard: folder))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user