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:
@@ -111,10 +111,18 @@ public struct HistoryStep {
|
||||
/// It exists for one consumer, and the design names it precisely: the window-close coarse step
|
||||
/// defers a card's `comments/.trash/` purge, because "the coarse close step's undo restores
|
||||
/// deleted comments, so their backing lives as long as the step does — the purge runs when the
|
||||
/// coarse step leaves the board stack (undone-and-superseded, dropped off the end, or gone
|
||||
/// stale) or the board session ends" (13-native-undo.md ▸ Interaction with the trash, re-ruled
|
||||
/// 2026-07-31). A step is the only object that knows all three of those moments, and it knows
|
||||
/// none of them itself — so the *provider* reports them, through this.
|
||||
/// coarse step leaves the board stack **cleanly** — undone-and-superseded, or dropped off the end
|
||||
/// — or when the board session ends" (13-native-undo.md ▸ Interaction with the trash, re-ruled
|
||||
/// 2026-07-31). A step is the only object that knows those moments, and it knows none of them
|
||||
/// itself — so the *provider* reports them, through this.
|
||||
///
|
||||
/// **A stale skip is deliberately not one of them** (the skip-purge decoupling, ruled
|
||||
/// 2026-07-31): "a stale-skipped step's backing instead survives to board-session end ... the
|
||||
/// skip banner says nothing was applied, and an irreversible purge riding that gesture would be
|
||||
/// surprise loss". Leaving the stack and leaving history for good stopped being the same event
|
||||
/// there, and this latch marks the second — which is why a substrate that keeps a skipped step's
|
||||
/// hold alive holds the *step* (`NativeHistoryProvider.strandedSteps`) rather than running this
|
||||
/// early and re-arming the sweep from somewhere else.
|
||||
///
|
||||
/// **A reference type inside a value type, deliberately.** `reversed` copies the step every time
|
||||
/// it crosses, and the two copies must not each run the work: sharing one latch is what makes
|
||||
@@ -150,6 +158,26 @@ public struct HistoryStep {
|
||||
/// The menu phrase, unprefixed — see the type's note.
|
||||
public let name: String
|
||||
|
||||
/// **What this step holds as undo backing** — deleted content that exists on disk only because
|
||||
/// *this step's undo would move it back out*, and which is therefore not residue for as long as
|
||||
/// the step is crossable (13-native-undo.md ▸ Interaction with the trash, ruled 2026-07-31).
|
||||
///
|
||||
/// > "`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."
|
||||
///
|
||||
/// The pair with `retirement` is the whole mechanism: this says *what* is being held, that says
|
||||
/// *when the hold ends*, and the two are read by the two consumers the ruling names — the
|
||||
/// crash-residue sweep (`BoardStore.sweepCommentTrashResidue(inCard:)`, through
|
||||
/// `HistoryProviding.backedContent`) and the deferred purge itself.
|
||||
///
|
||||
/// **Declared by the step's own expectations rather than by hand** — see `backing(declaredBy:)`.
|
||||
/// Empty for every board gesture, which is all but a comment delete and the coarse step folding
|
||||
/// one.
|
||||
public let backing: Set<HistoryAnchor>
|
||||
|
||||
/// What this step owes when it leaves history — see `Retirement`. `nil` for every step that owes
|
||||
/// nothing, which is all but the card-window close step.
|
||||
public let retirement: Retirement?
|
||||
@@ -165,26 +193,58 @@ public struct HistoryStep {
|
||||
public init(
|
||||
id: UUID = UUID(),
|
||||
name: String,
|
||||
backing: Set<HistoryAnchor> = [],
|
||||
retirement: Retirement? = nil,
|
||||
undo: @escaping @MainActor (HistoryDirection) -> HistoryStepOutcome,
|
||||
redo: @escaping @MainActor (HistoryDirection) -> HistoryStepOutcome
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.backing = backing
|
||||
self.retirement = retirement
|
||||
self.undo = undo
|
||||
self.redo = redo
|
||||
}
|
||||
|
||||
/// **The backing a step's own registration already names**: every target its **undo** expects to
|
||||
/// find *in a trash*.
|
||||
///
|
||||
/// Derived rather than passed, so no call site can forget it and no call site can say something
|
||||
/// its expectations contradict. The reading is exact: a step whose undo requires a folder to be
|
||||
/// present under `comments/.trash/` is a step whose undo *is* the move back out of it — which is
|
||||
/// the definition of backing, spelled in the currency the step already carries.
|
||||
///
|
||||
/// **`.trashedComment` is the whole vocabulary**, deliberately. The board's own `.trash/` holds
|
||||
/// the same relationship (a delete step's undo restores from it) and is deliberately absent: the
|
||||
/// board trash is a UI surface the user empties on purpose, with a confirm, and nothing sweeps it
|
||||
/// behind their back — so there is no consumer for the answer. `comments/.trash/` is the one
|
||||
/// trash the app purges on its own schedule (01-storage-format.md § Enhanced schema), which is
|
||||
/// exactly why it is the one that needs asking.
|
||||
public static func backing(declaredBy undoExpects: [HistoryExpectation]) -> Set<HistoryAnchor> {
|
||||
var backing: Set<HistoryAnchor> = []
|
||||
for expectation in undoExpects where expectation.presence == .present {
|
||||
guard case .trashedComment = expectation.anchor else { continue }
|
||||
backing.insert(expectation.anchor)
|
||||
}
|
||||
return backing
|
||||
}
|
||||
|
||||
/// The same step read backwards — what a provider puts on the opposite stack once this one has
|
||||
/// applied. The name does not change, which is the whole of "Undo Move Card" becoming "Redo Move
|
||||
/// Card": the phrase names the *gesture*, not the direction.
|
||||
///
|
||||
/// **The identity and the retirement travel with it**, both for the same reason: a step that has
|
||||
/// crossed is the same step, so the fold that keyed state on it must still find that state, and
|
||||
/// the purge it defers must still be owed exactly once.
|
||||
/// **The identity, the backing and the retirement travel with it**, all three for one reason: a
|
||||
/// step that has crossed is the same step, so the fold that keyed state on it must still find that
|
||||
/// state, the content it holds must still be held, and the purge it defers must still be owed
|
||||
/// exactly once.
|
||||
///
|
||||
/// The backing claim is constant across the crossing rather than swapped with the closures, and
|
||||
/// that is the honest reading: an undone coarse step has *already* moved its comments back out of
|
||||
/// `comments/.trash/`, so there is nothing there to sweep — and its redo will put them back,
|
||||
/// after which its undo needs them again. A claim that lapsed while the step sat on the redo stack
|
||||
/// would be a claim that lapsed exactly when the step was still crossable.
|
||||
public var reversed: HistoryStep {
|
||||
HistoryStep(id: id, name: name, retirement: retirement, undo: redo, redo: undo)
|
||||
HistoryStep(id: id, name: name, backing: backing, retirement: retirement, undo: redo, redo: undo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,14 +280,31 @@ public struct HistoryStep {
|
||||
/// - **No routing.** Which surface ⌘Z reaches is focus's answer, not the substrate's
|
||||
/// (06 ▸ Undo routing, tier-independent) — `BoardUndoRouting`.
|
||||
///
|
||||
/// ### One obligation every implementation shares: retire what you drop
|
||||
/// ### One obligation every implementation shares: retire what you let go of
|
||||
///
|
||||
/// A step may owe work for as long as it is crossable and no longer (`HistoryStep.Retirement` — the
|
||||
/// deferred `comments/.trash/` purge). Only the substrate knows when a step stops being crossable, so
|
||||
/// **every implementation must call `retirement?.run()` on every step it lets go**: the redo stack it
|
||||
/// clears on a `register`, a step it drops as stale, everything in `clear()`, and — for a substrate
|
||||
/// that keeps no steps at all — the step handed to `register` itself. Nothing else in the app can
|
||||
/// observe that moment, and a step dropped in silence would defer its work forever.
|
||||
/// A step may owe work for as long as the substrate holds it and no longer (`HistoryStep.Retirement`
|
||||
/// — the deferred `comments/.trash/` purge). Only the substrate knows when it has stopped holding
|
||||
/// one, so **every implementation must call `retirement?.run()` on every step it lets go**: the redo
|
||||
/// stack it clears on a `register`, everything in `clear()`, and — for a substrate that keeps no
|
||||
/// steps at all — the step handed to `register` itself. Nothing else in the app can observe that
|
||||
/// moment, and a step dropped in silence would defer its work forever.
|
||||
///
|
||||
/// **Held is not the same as crossable**, since the skip-purge decoupling (13 ▸ Interaction with the
|
||||
/// trash, ruled 2026-07-31): a step a stale skip popped is crossable by nothing, and its hold
|
||||
/// nonetheless stands to the session's end, because "the skip banner says nothing was applied, and an
|
||||
/// irreversible purge riding that gesture would be surprise loss". A substrate that pops stale steps
|
||||
/// therefore has a place to put them (`NativeHistoryProvider.strandedSteps`); one that never keeps a
|
||||
/// step is untouched by the distinction, which is why the git provider needed no change.
|
||||
///
|
||||
/// ### And its mirror: say what you are still holding
|
||||
///
|
||||
/// The same fact, asked the other way round — `backedContent`. Retirement is the *moment* a hold
|
||||
/// ends; that is the *inventory* of holds standing right now, which is what a sweep needs before it
|
||||
/// removes anything (13 ▸ Interaction with the trash, ruled 2026-07-31: "one condition, two
|
||||
/// consumers"). The two must be answered off the same steps or the pair stops being one condition —
|
||||
/// which is the whole reason a stranded step is kept whole rather than filleted into a retirement
|
||||
/// here and a set of anchors there. It defaults to nothing, so a substrate that keeps no steps needs
|
||||
/// no line of code.
|
||||
@MainActor
|
||||
public protocol HistoryProviding: AnyObject {
|
||||
|
||||
@@ -262,4 +339,32 @@ public protocol HistoryProviding: AnyObject {
|
||||
/// Drops every step in both directions — session-only persistence (13 ▸ Rules), run at the board
|
||||
/// session's teardown. Also what a substrate that must re-seed (a branch switch, 06) calls first.
|
||||
func clear()
|
||||
|
||||
/// **The undo backing every step this substrate still holds** — the union of `HistoryStep.backing`
|
||||
/// over everything it has not yet let go of.
|
||||
///
|
||||
/// Read by the crash-residue sweep before it removes anything
|
||||
/// (`BoardStore.sweepCommentTrashResidue(inCard:)`): "content referenced by a live coarse step on
|
||||
/// the board stack is a step's backing, not residue" (13 ▸ Interaction with the trash, ruled
|
||||
/// 2026-07-31). Both directions count, because both are live: a step sitting on the redo stack has
|
||||
/// not retired, and 13's own release condition — "undone-and-superseded" — is the moment it does.
|
||||
/// So does a step a stale skip stranded, which is crossable in *neither* direction and holding all
|
||||
/// the same: the skip-purge decoupling put its backing's release at the session's end, and a sweep
|
||||
/// that could not see it would take the release back (`NativeHistoryProvider.strandedSteps`).
|
||||
///
|
||||
/// A *point-in-time* answer, computed on demand rather than cached: the stacks are the truth, and
|
||||
/// a second copy of this could only ever be a stale one.
|
||||
var backedContent: Set<HistoryAnchor> { get }
|
||||
}
|
||||
|
||||
extension HistoryProviding {
|
||||
|
||||
/// **A substrate that keeps no steps holds no backing** — the git provider's honest answer
|
||||
/// (`GitHistoryProvider.register` retires every step on arrival, which is what makes "purge rides
|
||||
/// the close flush" true on Pro), and a test fake's.
|
||||
///
|
||||
/// A default on the requirement rather than a free function, so the dispatch is the substrate's:
|
||||
/// an implementation that *does* keep steps overrides it and every caller through `any
|
||||
/// HistoryProviding` sees the override.
|
||||
public var backedContent: Set<HistoryAnchor> { [] }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user