Materialize the trash — storage layer
Phase 1 of the trash pivot: the file format learns .trash/. The loader parses the reserved root container — cards only, one shared parseCard for both containers so fail-fast, attachments, and verbatim documents are literally the same code; absent means empty; symlinks and lane-shaped nestings fall out as strays by construction. BoardModel grows snapshot.trash as a plain rank-ordered card list — the container has no identity to carry. Legacy deleted: keys keep flowing through the retiring flag path so every tombstone consumer stays green, and are additionally reported through LoadResult.legacyTombstones in the loose-file idiom for phase 2's migration scheduling — nothing vanishes from view before its folder has actually moved, which is also 01's lock-deferral posture. Writer primitives land value-passing: move to trash with caller-minted rank and the deliberate modified stamp, tombstone migrations that surgically remove the key, physical lane removal, per-card and whole-container purge that leaves strays verbatim, and byte-faithful whole-subtree capture/recreate for lane undo. Board-wide identity now spans the trash, so an import colliding with a trashed UUID remints instead of colliding. The watcher already delivered .trash events — isGitInternal tests a component, not a dot — now stated and pinned rather than relied on. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -92,11 +92,39 @@ public struct BoardModel: Sendable, Equatable {
|
||||
/// parsed into a dedicated Swift shape, so future subkeys need no model change.
|
||||
public let template: YAMLValue?
|
||||
|
||||
/// Lanes in display order (`Ranks.sortedForDisplay`, folder-name tie-break) — **including
|
||||
/// tombstoned lanes**, which stay in the snapshot flagged (`Lane.isDeleted`) for the trash
|
||||
/// view (01-storage-format.md § Deletion).
|
||||
/// Lanes in display order (`Ranks.sortedForDisplay`, folder-name tie-break).
|
||||
///
|
||||
/// **Legacy tombstones ride along flagged during the migration window.** A lane still
|
||||
/// carrying a `deleted:` key loads here with `Lane.isDeleted` set, exactly as it did under
|
||||
/// the retired tombstone model, *and* is reported through `LoadResult.legacyTombstones` — see
|
||||
/// `BoardLoader`'s "The migration window" note for why both happen at once and what ends it.
|
||||
public let lanes: [Lane]
|
||||
|
||||
/// The board's **materialized trash**: the card folders sitting directly in
|
||||
/// `<root>/.trash/`, in display order (01-storage-format.md § Deletion, resettled
|
||||
/// 2026-07-28; 03-board-ui.md § Trash).
|
||||
///
|
||||
/// **A sibling container of `lanes`, not a lane.** `.trash/` is a reserved, app-claimed name
|
||||
/// at board root that "holds card folders directly — same shape as a lane's children, no
|
||||
/// `index.md` of its own", so it has no identity, no title, no `order`, and no frontmatter to
|
||||
/// model: the container *is* the list. That is why this is `[Card]` rather than a `Lane` or a
|
||||
/// `Trash` struct — there is nothing for either to carry that this array does not.
|
||||
///
|
||||
/// **Display order is `order` ascending, like any lane's cards** — `Ranks.sortedForDisplay`,
|
||||
/// same folder-name tie-break. Newest-first falls out of ordinary ranks rather than a
|
||||
/// timestamp sort: every arrival mints a rank *above* the current topmost
|
||||
/// (`Ranks.insertAtHead`), so the trash needs no sort rule of its own. There is deliberately
|
||||
/// no `deleted:` key on anything in here — a trashed card is an ordinary card in a special
|
||||
/// place.
|
||||
///
|
||||
/// Empty when `.trash/` is absent (the overwhelmingly common case — the folder is minted by
|
||||
/// the first delete), and empty when it holds nothing the loader recognizes as a card.
|
||||
///
|
||||
/// **Defaulted so a snapshot can be built without one.** The one construction site is
|
||||
/// `BoardLoader.load`, which always supplies it; the default keeps the memberwise initializer
|
||||
/// usable from tests and future fixtures that have no trash to describe.
|
||||
public var trash: [Card] = []
|
||||
|
||||
/// The full parsed `index.md`. Unknown/reserved keys (`labels`, `assignees`, `due`,
|
||||
/// `remote`, …) ride along uninterpreted via `document.unknownFields` so a future writer
|
||||
/// can round-trip them without this model knowing what they mean.
|
||||
@@ -132,9 +160,9 @@ public struct Lane: Identifiable, Sendable, Equatable {
|
||||
/// failing the load (01-storage-format.md § Frontmatter).
|
||||
public let width: FieldValue<Int>
|
||||
|
||||
/// Cards in this lane, in display order (`Ranks.sortedForDisplay`, folder-name tie-break)
|
||||
/// — **including tombstoned cards**, which stay in the snapshot flagged (`Card.isDeleted`)
|
||||
/// for the trash view.
|
||||
/// Cards in this lane, in display order (`Ranks.sortedForDisplay`, folder-name tie-break).
|
||||
/// A card still carrying a legacy `deleted:` key rides along flagged (`Card.isDeleted`) until
|
||||
/// its migration relocates it into `BoardModel.trash` — see `BoardModel.lanes`.
|
||||
public let cards: [Card]
|
||||
|
||||
/// The full parsed `index.md`; unknown/reserved keys ride along uninterpreted.
|
||||
@@ -148,6 +176,13 @@ public struct Lane: Identifiable, Sendable, Equatable {
|
||||
/// key's presence is what encodes deletion intent, a missing key is the only thing that
|
||||
/// means "not deleted" (`FieldValue.isMissing` already treats an explicit `deleted: null`
|
||||
/// as absent, matching the engine's own null-is-missing rule).
|
||||
///
|
||||
/// **Retiring.** The tombstone model is retired (01-storage-format.md § Deletion, resettled
|
||||
/// 2026-07-28): the app never writes a `deleted:` key again, and one found on load is
|
||||
/// migration input reported through `LoadResult.legacyTombstones` (a lane's migration removes
|
||||
/// the key and returns the lane live). This accessor survives only for the migration window,
|
||||
/// during which the retiring tombstone renderers still read it; it becomes permanently
|
||||
/// `false` for every board once the migration has run, and goes away with its last consumer.
|
||||
public var isDeleted: Bool { !deleted.isMissing }
|
||||
}
|
||||
|
||||
@@ -199,6 +234,8 @@ public struct Card: Identifiable, Sendable, Equatable {
|
||||
public var body: String { document.body }
|
||||
|
||||
/// A tombstoned card. See `Lane.isDeleted`'s doc comment — the same "presence, not
|
||||
/// validity" rule applies here.
|
||||
/// validity" rule applies here, and the same retirement: a trashed card lives in
|
||||
/// `BoardModel.trash` and carries no `deleted:` key at all, so this reads `false` for every
|
||||
/// card in the container that replaced the flag.
|
||||
public var isDeleted: Bool { !deleted.isMissing }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user