Space in Finder opens the board — a Quick Look preview extension that outlines a .kanban package

A board is a folder, and a folder previews as a folder. `KanbanQuickLook.appex` gives it a
document's preview instead: the board's name, its tint and its symbol, then its lanes in display
order with each one's card count and its first few card titles.

The reading is `BoardOutline` (Kanban/Storage), deliberately not `BoardLoader.load`. The loader
throws on a half-broken board — right for opening one, wrong for pressing Space, where the honest
answer is the part that reads; it visits every card and lists `attachments/` and counts `comments/`
inside each; and it carries trash, tombstone migration and the defect stream, none of which renders.
This walk never throws and is capped at every level (`BoardOutlineLimits`): 12 lanes shown of at
most 100 considered, 6 card titles per lane of at most 200 parsed, counts by readdir-plus-stat up to
2000 per lane and never a parse. It re-derives nothing that decides *what* the answer is —
`FrontmatterDocument` parses, `IntegrityRules.isIdentityShaped` says what a lane or a card is,
`BoardLoader.directoryCandidates` supplies the stray tolerance, `Ranks` supplies display order,
`Palette` resolves colours — only *how far to look*.

The reply is HTML, the one data-based reply that reflows: a Quick Look panel is resized by the
user and a board outline is a wrapping row of columns, so a drawing block baked at a fixed
`contentSize` would be the wrong size a moment later. It gets vector text, its own scrolling and
light/dark for free. The board tint is a wash under the title and a lane's edge accent — never
under text, because a preview has none of `ContrastMath`'s ink-picking machinery and should not
grow one.

The extension compiles `Kanban/Storage` whole, the `KanbanMobile` arrangement — the directory is
one unit in practice, so a narrower list is not on offer. `STORAGE_ONLY` is new: EchoLedger's
consumer sections speak the live store's vocabulary, and the phone's `#if os(macOS)` cannot exclude
them from a target that *is* macOS. Platform, and layer. Nothing else defines it.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 02:37:10 -04:00
parent da5d310673
commit b18f7ca609
8 changed files with 1260 additions and 2 deletions
+322
View File
@@ -0,0 +1,322 @@
import Foundation
/// **Everything a Quick Look preview knows about a board** its name, its tint, and each lane's
/// first few card titles read straight off the folder tree with no store, no watcher and no
/// window behind it.
///
/// ### Why this is not `BoardLoader.load`
///
/// The snapshot the app opens a board with is the wrong shape for a preview in three ways, and each
/// one is a reason rather than a preference:
///
/// - **It fails.** `BoardLoader` collects fail-fast defects and throws a `BoardLoadFailure` for a
/// missing root `index.md`, an unreadable `schema`, a lane whose YAML will not parse
/// (01-storage-format.md § Malformed input). That is exactly right for opening a board the user
/// is about to edit it and exactly wrong for pressing Space in Finder, where the honest answer to
/// a half-broken board is *the part that reads*. Nothing in this file throws: every failure
/// degrades to the empty answer for the thing that failed and the walk carries on.
/// - **It is unbounded.** The loader visits every card, and inside each one it lists `attachments/`
/// and counts `comments/` (`BoardModel.Card`) three directory reads per card, plus a parse, on
/// a board that may hold thousands. A preview has a few hundred milliseconds and no second
/// chance, so this walk is *capped at every level* (`BoardOutlineLimits`) and opens no file it
/// does not put on screen.
/// - **It carries what a preview cannot use.** Trash, tombstone migration, the identity dedupe, the
/// parse memo, the integrity defect stream, the coerce-tier trace none of it renders, and all of
/// it costs.
///
/// What this does reuse is everything that decides *what the answer is*: `FrontmatterDocument` for
/// the parse and every field reading, `IntegrityRules.isIdentityShaped` for what counts as a lane or
/// a card, `BoardLoader.directoryCandidates` for the stray tolerance (hidden entries and symlinks
/// out, folder-name order in), and `Ranks` for display order. A second walker that re-derived any of
/// those would be a second storage format; this one only decides *how far to look*.
///
/// ### The name
///
/// "Outline", not "summary": `KanbanMobile/Cloud/BoardSummary.swift` already owns that word for the
/// phone's board *list* row a name, a size, a download state, nothing about the contents and the
/// phone compiles this directory into its own module, so the two would collide outright. The words
/// are worth keeping apart anyway: that one summarizes a board's *file*, and this one outlines its
/// *contents*.
public struct BoardOutline: Sendable, Equatable {
/// The board's name its `title`, falling back to the folder name with `.kanban` stripped
/// (01-storage-format.md § Board naming). Non-optional, deliberately: this is
/// `AppModel.displayName(of:)`'s rule read one layer down, and "Untitled" appears nowhere on a
/// board because a board always has a folder (BoardInfoPopover Rename "boards do not have
/// it"). Contrast `LaneOutline.title`.
public let title: String
/// The `background` mapping's colour as written a palette name or a `#RRGGBB[AA]` hex or
/// `nil` for a board that carries none.
///
/// **The value, not a colour**: resolving it is the renderer's job through `Palette`, exactly as
/// it is in the app, so an unrecognized value degrades to "no tint" at the one place that knows
/// what no tint looks like (Palette.swift "Lenient, never an error").
public let background: String?
/// The board's `icon` (an SF Symbol name) and `iconColor`, both as written and both lenient in
/// the same way `background` is: a name the running system cannot draw is the renderer's problem
/// to degrade, not this walk's to validate.
public let icon: String?
public let iconColor: String?
/// The lanes the preview shows, in display order **a prefix**, capped by
/// `BoardOutlineLimits.lanes`. `hiddenLaneCount` is the rest.
public let lanes: [LaneOutline]
/// Every lane the walk found, shown or not the number the preview's header line states.
///
/// `laneCountIsCapped` marks the walk that stopped counting (`BoardOutlineLimits.laneScan`), so
/// a renderer can say "100+" rather than a number it would be lying about. On every board that
/// has ever existed this is `false`; it is here because a preview must have a bounded worst case
/// even on a folder nobody meant to be a board.
public let laneCount: Int
public let laneCountIsCapped: Bool
/// The lanes counted but not shown the "+N more lanes" the preview ends on, and `0` on a board
/// whose lanes all fit.
public var hiddenLaneCount: Int { max(0, laneCount - lanes.count) }
public init(
title: String,
background: String? = nil,
icon: String? = nil,
iconColor: String? = nil,
lanes: [LaneOutline] = [],
laneCount: Int = 0,
laneCountIsCapped: Bool = false
) {
self.title = title
self.background = background
self.icon = icon
self.iconColor = iconColor
self.lanes = lanes
self.laneCount = laneCount
self.laneCountIsCapped = laneCountIsCapped
}
}
/// One lane in a board summary: a title, a colour, a count, and the first few card titles.
public struct LaneOutline: Sendable, Equatable {
/// The lane's `title` as written, or `nil` where it carries none **"Untitled" is a rendering,
/// never a value** (03-board-ui.md § Card face; the same rule `Lane`/`Card` follow throughout the
/// app), so the placeholder is applied where the text is drawn and never stored here. An empty
/// or whitespace-only title reads as `nil` for the same reason a blank lane header does.
public let title: String?
/// The lane's `background` colour as written see `BoardOutline.background`; a lane's is an edge
/// accent rather than a backdrop (03-board-ui.md § Styling Capabilities).
public let background: String?
/// The first few cards' titles, in display order, each `nil` where the card carries no title.
/// Capped by `BoardOutlineLimits.cardTitles`.
///
/// **In display order over the *scanned* cards**, which is every card in the overwhelming case
/// and the first `BoardOutlineLimits.cardScan` of them otherwise see that field for the cost
/// the cap accepts.
public let cardTitles: [String?]
/// How many cards the lane holds a `readdir` plus one `stat` per child, never a parse, which is
/// `BoardLoader.commentCount(in:)`'s own arrangement one level up: a card is an identity-shaped
/// child carrying its own `index.md`, and whether that file *parses* costs a read this count does
/// not pay.
public let cardCount: Int
/// Whether the count stopped at `BoardOutlineLimits.cardCount` a lane holding more cards than
/// the walk was willing to stat, which a renderer states as "2000+" rather than as a number.
public let cardCountIsCapped: Bool
/// The cards counted but not titled the "+N more" under a lane's last shown title.
public var hiddenCardCount: Int { max(0, cardCount - cardTitles.count) }
public init(
title: String? = nil,
background: String? = nil,
cardTitles: [String?] = [],
cardCount: Int = 0,
cardCountIsCapped: Bool = false
) {
self.title = title
self.background = background
self.cardTitles = cardTitles
self.cardCount = cardCount
self.cardCountIsCapped = cardCountIsCapped
}
}
/// **How far the summary walk looks** the whole of what makes a preview bounded.
///
/// Quick Look gives an extension a short budget and no way to say "still working"; a board is a
/// folder tree of unknown size that a user can point Finder at. So every level of the walk has a
/// ceiling, and the ceilings are here rather than scattered through the walk so the answer to "what
/// does a preview cost, worst case?" is one struct: at most `laneScan` lane `index.md` parses, plus
/// per shown lane at most `cardCount` stats and `cardScan` card `index.md` parses.
///
/// The defaults are `preview`'s. Every one is generous enough that a real board is summarized
/// exactly the caps exist for the pathological folder, not for the ordinary one.
public struct BoardOutlineLimits: Sendable, Equatable {
/// Lanes rendered, in display order. The rest become `BoardOutline.hiddenLaneCount`.
public var lanes: Int
/// Root children the walk will consider a lane at all. Past this the count itself stops
/// (`BoardOutline.laneCountIsCapped`) it is the one cap that bounds the lane parse, since
/// display order cannot be known without reading every sibling's `order`.
public var laneScan: Int
/// Card titles shown per lane. The rest become `LaneOutline.hiddenCardCount`.
public var cardTitles: Int
/// Card `index.md` files parsed per lane, taken in folder-name order.
///
/// **The one cap with a visible cost**, stated plainly: a card's rank lives in its own file, so
/// the true first-`cardTitles` of a lane cannot be known without reading all of them. A lane
/// holding more cards than this shows the top of the *scanned* subset instead its **count
/// stays exact**, and the titles are still cards in that lane, just not provably the topmost.
/// Set high enough that no lane a person has ever built reaches it.
public var cardScan: Int
/// Children stat'd per lane while counting. Past this the count stops
/// (`LaneOutline.cardCountIsCapped`).
public var cardCount: Int
public init(lanes: Int, laneScan: Int, cardTitles: Int, cardScan: Int, cardCount: Int) {
self.lanes = lanes
self.laneScan = laneScan
self.cardTitles = cardTitles
self.cardScan = cardScan
self.cardCount = cardCount
}
/// The Quick Look preview's own ceilings the only set anything but a test uses.
public static let preview = BoardOutlineLimits(
lanes: 12, laneScan: 100, cardTitles: 6, cardScan: 200, cardCount: 2000
)
}
// MARK: - The walk
extension BoardOutline {
/// Summarizes the board rooted at `boardRoot` **total, and never throwing**.
///
/// A preview has no error surface: there is no alert to raise, no Repair sheet to offer and no
/// second attempt, so every failure this walk can meet degrades in place. A root that is not a
/// directory, an `index.md` that is missing or unparseable or not UTF-8, a lane folder that
/// vanishes mid-walk, a permissions race on a directory listing each yields the empty answer
/// for that one thing and nothing else changes. The floor is a summary carrying the folder's own
/// name and no lanes, which is still a truthful preview of a folder that is not a board.
///
/// A lane or card whose `index.md` will not parse is **kept, not skipped**: it reads as untitled
/// and order-less (append-at-end, `Ranks.resolvedOrders`), so a board with one broken file
/// previews with one blank title rather than with a lane silently missing and a count that
/// disagrees with Finder.
public static func read(boardRoot: URL, limits: BoardOutlineLimits = .preview) -> BoardOutline {
let document = parsedIndex(in: boardRoot)
// The lane ceiling is applied to the *name-shaped* children, before the per-child `stat` that
// asks whether each holds an `index.md` `readLane`'s ordering, and for its reason.
let identityShaped = directoryCandidates(in: boardRoot)
.filter { IntegrityRules.isIdentityShaped($0.lastPathComponent) }
let considered = identityShaped.prefix(max(0, limits.laneScan))
let candidates = considered.filter(hasIndex)
let walked = candidates.map { (url: $0, document: parsedIndex(in: $0)) }
let orders = Ranks.resolvedOrders(
of: walked,
stored: { $0.document?.order.value },
name: { $0.url.lastPathComponent }
)
let ranked = Array(zip(walked, orders))
let ordered = Ranks.sortedForDisplay(
ranked,
order: { $0.1 },
name: { $0.0.url.lastPathComponent }
)
let shown = ordered.prefix(max(0, limits.lanes))
return BoardOutline(
title: text(document?.title) ?? boardRoot.deletingPathExtension().lastPathComponent,
background: text(document?.background),
icon: text(document?.icon),
iconColor: text(document?.iconColor),
lanes: shown.map { readLane(at: $0.0.url, document: $0.0.document, limits: limits) },
laneCount: ordered.count,
laneCountIsCapped: identityShaped.count > considered.count
)
}
/// One lane: its own fields off an already-parsed `index.md`, then its cards counted by
/// listing, titled by parsing, and both capped.
private static func readLane(
at laneURL: URL,
document: FrontmatterDocument?,
limits: BoardOutlineLimits
) -> LaneOutline {
// The count's two halves, in this order on purpose: the name-shape filter is a string test
// and free, so the ceiling is applied to *it* and the per-child `stat` below never runs more
// than `limits.cardCount` times.
let identityShaped = directoryCandidates(in: laneURL)
.filter { IntegrityRules.isIdentityShaped($0.lastPathComponent) }
let considered = identityShaped.prefix(max(0, limits.cardCount))
let cards = considered.filter(hasIndex)
let scanned = cards.prefix(max(0, limits.cardScan))
let walked = scanned.map { (url: $0, document: parsedIndex(in: $0)) }
let orders = Ranks.resolvedOrders(
of: walked,
stored: { $0.document?.order.value },
name: { $0.url.lastPathComponent }
)
let ordered = Ranks.sortedForDisplay(
Array(zip(walked, orders)),
order: { $0.1 },
name: { $0.0.url.lastPathComponent }
)
return LaneOutline(
title: text(document?.title),
background: text(document?.background),
cardTitles: ordered.prefix(max(0, limits.cardTitles)).map { text($0.0.document?.title) },
cardCount: cards.count,
cardCountIsCapped: identityShaped.count > considered.count
)
}
// MARK: - Filesystem helpers
/// `BoardLoader.directoryCandidates(in:)` with its typed throw flattened to the empty answer
/// the function never actually throws (an unlistable folder already degrades to `[]` inside it),
/// and this walk has nowhere to put an error even if it did.
private static func directoryCandidates(in folder: URL) -> [URL] {
(try? BoardLoader.directoryCandidates(in: folder)) ?? []
}
private static func hasIndex(_ folder: URL) -> Bool {
FileManager.default.fileExists(
atPath: folder.appendingPathComponent(IntegrityRules.indexFileName).path
)
}
/// A folder's `index.md`, parsed `nil` for every way that can fail: no file, unreadable bytes,
/// not UTF-8, unparseable YAML. The strict UTF-8 decode is `BoardLoader.parseDocument`'s, so a
/// BOM'd file reads here exactly as it loads there: as nothing.
private static func parsedIndex(in folder: URL) -> FrontmatterDocument? {
let url = folder.appendingPathComponent(IntegrityRules.indexFileName)
guard let data = try? Data(contentsOf: url),
let text = String(validating: data, as: UTF8.self)
else { return nil }
return try? FrontmatterDocument.parse(text)
}
/// A lenient string field's text, with **blank read as absent** a title of `""` or of spaces is
/// a key that says nothing, and the summary's whole vocabulary for "says nothing" is `nil`
/// (`AppModel.displayName(of:)` applies the same `!title.isEmpty` test to a board's own name).
/// A missing key and a malformed one collapse to `nil` here too, exactly as they do at every
/// other lenient call site in the app.
private static func text(_ field: FieldValue<String>?) -> String? {
guard let value = field?.value else { return nil }
return value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : value
}
}