The phone joins the format — KanbanMobile MVP: shared storage verbatim over an iCloud container
A second product, not a second edition: dev.rzen.indie.KanbanMobile (iOS 26,
iPhone-only) compiles Kanban/Storage as source files, so a format change that
breaks the phone breaks this build the day it's made. Boards live in
iCloud.dev.rzen.indie.Kanban — named after the Mac bundle id so the Mac app can
adopt the container later without a migration; until then the folder is
"Lanework" in iCloud Drive and the Mac opens boards there through the open
panel.
EchoLedger grows #if os(macOS) gates around its three consumer surfaces
(verdicts/BoardDiff, harvest/HarvestedReceipt, comment retirement/CommentPath)
— the recording side BoardWriter stamps compiles on every platform, and the
gates are the seam a future phone verdict surface lands behind. AgentGuide
stays Mac-only.
The phone's watcher is NSMetadataQuery: BoardIndexStore (one query, package
UTI export makes a .kanban directory one item, equality-gated rescans,
download kicks per pass), BoardSession (materialization sweep before every
fail-fast walk, NSFileCoordinator brackets, perform{} = coordinated write then
awaited reload, ParseMemo threaded), CloudHome (off-main container resolution,
LANEWORK_LOCAL_ROOT DEBUG override for simulator work without an account).
Screens: Boards -> lanes -> cards -> card editor, value-routed by ItemID with
every screen re-reading the live snapshot; leading swipe moves a card via
confirmationDialog, trailing swipe sends it to .trash/; the editor commits
title through the Mac's canonical rename path and body through writeBody, with
drafts that survive reloads; attributes are the three typed style fields
(icon, iconColor, background) — labels is a reserved unknown-field key and
deliberately has no editor. Settings carries IndieBackup (backup root = the
container's Documents, restore rebuild = an index rescan, controller
constructed only once the home resolves).
Arbiter: KanbanMobile green for iOS Simulator, Kanban green for macOS, 3006
unit tests / 517 suites passed (PointerLatencyTests excluded — mid-rework
uncommitted in a parallel session).
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -330,11 +330,17 @@ public final class EchoLedger: Sendable {
|
||||
/// Nothing is retired here, which is what makes this safe to call on every bracket: the
|
||||
/// announcer's consumption still decides what speaks, and the committer's copy still decides what
|
||||
/// each commit is authored by.
|
||||
// The harvest speaks `HarvestedReceipt` — committer-side vocabulary from
|
||||
// Git/CommitAttribution.swift, which the phone target doesn't compile (the mobile MVP has
|
||||
// no git). The recording side above is what BoardWriter needs on every platform; this copy
|
||||
// has exactly one caller and it is Mac-only.
|
||||
#if os(macOS)
|
||||
func outstandingEntries() -> [String: HarvestedReceipt] {
|
||||
receipts.withLock { store in
|
||||
store.mapValues { HarvestedReceipt(receipt: $0.receipt, isHeal: $0.isHeal) }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Whether the receipt at this path is heal-marked — `false` for a path with no receipt at all,
|
||||
/// which is the same shrug every other read here gives an unknown path.
|
||||
@@ -380,6 +386,11 @@ public final class EchoLedger: Sendable {
|
||||
/// spelling `CommentPath.classify` reads. The two are handed in together rather than derived
|
||||
/// from each other because the store already holds both (`BoardStore.commentSubject`).
|
||||
/// - Returns: the identities of the posted and just-deleted comments the app wrote.
|
||||
// Mac-only like the harvest above, and for the same shape of reason: the classification
|
||||
// here speaks `CommentPath` (LiveStore's comment-thread vocabulary), and its one caller is
|
||||
// the Mac store's comment reload. The phone's writers stamp comment receipts identically;
|
||||
// nothing on the phone retires them yet.
|
||||
#if os(macOS)
|
||||
func vouchedComments(inCard cardFolder: URL, cardPath: String) -> Set<ItemID> {
|
||||
let root = Self.key(cardFolder)
|
||||
let paths = receiptPaths(under: root)
|
||||
@@ -408,6 +419,7 @@ public final class EchoLedger: Sendable {
|
||||
}
|
||||
return vouched
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Every path the ledger holds a receipt for strictly *below* `folder`.
|
||||
///
|
||||
@@ -494,6 +506,13 @@ extension EchoLedger.Receipt {
|
||||
|
||||
// MARK: - The reload's verdicts
|
||||
|
||||
// Consumer-side machinery, Mac-only by compilation: everything below speaks `BoardDiff` and
|
||||
// `ItemPath` — the landing reload's vocabulary, which the phone target doesn't compile. The
|
||||
// phone records receipts through the surface above (BoardWriter stamps identically on every
|
||||
// platform); its metadata-query observer has no verdict surface yet, and when it grows one this
|
||||
// gate is the seam it lands behind.
|
||||
#if os(macOS)
|
||||
|
||||
/// One landing reload's provenance answers, in the two shapes the announcer needs.
|
||||
public struct EchoVerdicts: Sendable, Equatable {
|
||||
|
||||
@@ -700,3 +719,5 @@ extension EchoLedger {
|
||||
return footprints
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user