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
297 lines
14 KiB
YAML
297 lines
14 KiB
YAML
name: Kanban
|
|
options:
|
|
bundleIdPrefix: dev.rzen.indie
|
|
deploymentTarget:
|
|
macOS: "26.0"
|
|
iOS: "26.0"
|
|
xcodeVersion: "26.0"
|
|
defaultConfig: Debug
|
|
|
|
packages:
|
|
Yams:
|
|
url: https://github.com/jpsim/Yams.git
|
|
from: 6.0.0
|
|
# apple/swift-markdown — the card window's Preview parser (05-card-window.md ▸ Preview).
|
|
# Pinned to the next *minor* rather than the next major because the package is pre-1.0: a
|
|
# `from:` range here would silently accept 0.9's breaking changes, which is not what `from:`
|
|
# means for Yams at 6.x.
|
|
swift-markdown:
|
|
url: https://github.com/apple/swift-markdown.git
|
|
minorVersion: 0.8.0
|
|
# The About window: app icon, version/build from the stamped Info.plist, and the bundled
|
|
# LICENSE/CHANGELOG documents. Pre-1.0 like swift-markdown, same pinning logic.
|
|
IndieAbout:
|
|
url: https://git.rzen.dev/rzen/indie-about.git
|
|
minorVersion: 0.2.2
|
|
# **The bundled git client** (06-history-undo.md ▸ Rules: "Bundled libgit2 — no git install
|
|
# required"): SwiftGitX vendors libgit2 as an in-process library, so every git operation the app
|
|
# performs runs inside the sandbox with no `Process`, no `/usr/bin/git`, and no sandbox extension.
|
|
# A board's history therefore works identically in a Release build on a machine that has never had
|
|
# Xcode or the command-line tools installed.
|
|
#
|
|
# `exactVersion` rather than a range, and pinned to the pathfinder's own 0.4.0: the package is
|
|
# pre-1.0 with an API that moves between minors (the commit and diff surfaces this depends on both
|
|
# changed shape in 0.3→0.4), so a floating pin would be a resolver deciding when the git provider
|
|
# stops compiling.
|
|
SwiftGitX:
|
|
url: https://github.com/ibrahimcetin/SwiftGitX
|
|
exactVersion: 0.4.0
|
|
# **The C library underneath SwiftGitX, named directly** (06-history-undo.md ▸ Interaction with
|
|
# external writers: "foreign changes are committed under the pinned synthetic author"). SwiftGitX
|
|
# 0.4.0's `commit(message:)` takes no signature — its `CommitOptions` leaves `author`/`committer`
|
|
# null and libgit2 falls back to `git_signature_default`, which reads a config the sandbox cannot
|
|
# see — so the auto-committer's per-commit authorship is unreachable through the wrapper. It
|
|
# reaches `git_commit_create` itself (`GitCommitOperation`), which needs the same vendored module
|
|
# SwiftGitX imports.
|
|
#
|
|
# **The same package SwiftGitX already resolves**, pinned to the version SwiftGitX pins exactly
|
|
# (`.package(url: …/libgit2.git, exact: "1.9.2")`): this adds no second copy of libgit2 to the
|
|
# binary and no second resolution to argue with — it makes a module the app already links
|
|
# *importable*. A drift between these two pins is a resolver error at build time, which is the
|
|
# loudest place for it to be.
|
|
libgit2:
|
|
url: https://github.com/ibrahimcetin/libgit2.git
|
|
exactVersion: 1.9.2
|
|
# Local backup/restore for the phone app's Settings tab (indie-backup skill). `KanbanMobile`
|
|
# only — see that target's dependencies below; the Mac app has no Settings surface for this
|
|
# yet. Pinned to `from: "2.0.0"`, the tag the persistence-agnostic API (a single `backupRoot`
|
|
# plus a rebuild-cache-on-restore callback — here, `BoardIndexStore.refresh()`, no database in
|
|
# the loop) shipped under; 1.0.x resolves to the older iCloud-required, CloudKit-coupled
|
|
# surface this app doesn't want.
|
|
IndieBackup:
|
|
url: https://git.rzen.dev/rzen/indie-backup.git
|
|
from: "2.0.0"
|
|
|
|
settings:
|
|
base:
|
|
SWIFT_VERSION: "6.0"
|
|
MACOSX_DEPLOYMENT_TARGET: "26.0"
|
|
DEVELOPMENT_TEAM: ${APPLE_TEAM_ID}
|
|
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
|
|
|
# **One app target** (12-editions.md ▸ The target, re-ruled 2026-07-30). Lanework ships as a single
|
|
# Mac App Store app, `dev.rzen.indie.Kanban`; **Lanework Pro is a subscription inside it**, not a
|
|
# second binary. The 2026-07-27 two-target split — a `KanbanPro` application target, its scheme, its
|
|
# bundle id, the `-module-alias Kanban=KanbanPro` test bundle and the twin source files that existed
|
|
# only because two bundles claimed different UTI ownership — retired wholesale with the collapse.
|
|
#
|
|
# What that buys this file: no anchors, no per-target exclusion lists, no second copy of the
|
|
# resources. There is one product, one Info.plist, one entitlements file, one Swift module, and one
|
|
# `.kanban` UTI **exported** (never imported) by the app that owns it.
|
|
#
|
|
# libgit2 arrived here with pro-m1, as SwiftGitX above — a dependency of the one target, dormant
|
|
# behind the subscription gate rather than the contents of a second download. It links into the free
|
|
# app and never runs there: detection itself is tier-gated (`HistoryStore.compose`), so a free-tier
|
|
# session opens without so much as a `fileExists` under `.git` (12-editions.md ▸ The free tier and
|
|
# `.git`, and `InertGitTests`).
|
|
targets:
|
|
# MARK: - Lanework
|
|
|
|
Kanban:
|
|
type: application
|
|
platform: macOS
|
|
sources:
|
|
- Kanban
|
|
# The bundled template store (09-templates.md ▸ Definition format): real board folders,
|
|
# copied into `Contents/Resources/Templates/` verbatim as a folder reference — the same
|
|
# arrangement `Fixtures/` uses below, and for the same reason. A template is a board, so it
|
|
# must reach the bundle as directories on disk, not as flattened resource files.
|
|
- path: Templates
|
|
type: folder
|
|
buildPhase: resources
|
|
# The About window's documents (LICENSE, CHANGELOG) — bundled so IndieAbout can render
|
|
# them in-app. Apple-inline-Markdown subset only: no `#` headings, no hard-wrapped
|
|
# paragraphs (every newline renders literally).
|
|
- path: LICENSE.md
|
|
type: file
|
|
buildPhase: resources
|
|
- path: CHANGELOG.md
|
|
type: file
|
|
buildPhase: resources
|
|
# The local StoreKit configuration (12-editions.md ▸ Distribution) — the subscription group
|
|
# "Lanework Pro" and its monthly product, mirroring what App Store Connect declares, so the
|
|
# Settings Pro section's subscribe/manage/restore flow is exercisable in Xcode without an ASC
|
|
# product existing yet. Attached to the scheme's run action below.
|
|
#
|
|
# **`buildPhase: none`, deliberately.** It is development configuration, not app content: the
|
|
# scheme reads it, the shipped bundle must never carry it. Listed in the project anyway, for
|
|
# the same reason `KanbanUITests`' verification documents are — visible where it belongs,
|
|
# built into nothing.
|
|
- path: Configuration.storekit
|
|
type: file
|
|
buildPhase: none
|
|
dependencies:
|
|
- package: Yams
|
|
- package: swift-markdown
|
|
product: Markdown
|
|
- package: IndieAbout
|
|
- package: SwiftGitX
|
|
- package: libgit2
|
|
postBuildScripts:
|
|
- script: '"${SRCROOT}/../indie-skills/skills/app-versioning/scripts/update_build_info.sh"'
|
|
name: Update Build Info
|
|
shell: /bin/sh
|
|
basedOnDependencyAnalysis: false
|
|
inputFiles:
|
|
- $(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)
|
|
- $(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist
|
|
settings:
|
|
base:
|
|
# The identifier 1.x shipped under, unchanged: 2.0 updates that App Store record rather
|
|
# than opening a second one (12-editions.md ▸ Distribution, ruled 2026-07-30).
|
|
PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.Kanban
|
|
MARKETING_VERSION: "2.0"
|
|
INFOPLIST_FILE: Kanban/Info.plist
|
|
# `Kanban/Kanban.entitlements` — sandbox, user-selected files, app-scope bookmarks, and
|
|
# `com.apple.security.network.client`, which is **declared now and dormant until Pro
|
|
# ships**: the one binary carries the key, and nothing exercises it until the git
|
|
# provider's remotes do under an active subscription (12 ▸ The target). No App Group and
|
|
# no keychain access group — groups exist to share *between* apps, and there is one app,
|
|
# so app-side state homes in the ordinary sandbox container (`AppStateHome`).
|
|
CODE_SIGN_ENTITLEMENTS: Kanban/Kanban.entitlements
|
|
GENERATE_INFOPLIST_FILE: false
|
|
SWIFT_STRICT_CONCURRENCY: complete
|
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
|
|
|
# MARK: - Lanework for iPhone
|
|
#
|
|
# **The mobile app is a second product, not a second edition** (distinct bundle id, ruled
|
|
# 2026-08-07): `dev.rzen.indie.KanbanMobile`, its own App Store record, iPhone-only for the MVP
|
|
# (iPad joins by widening TARGETED_DEVICE_FAMILY, not by a new target). It compiles
|
|
# `Kanban/Storage` **verbatim — the same source files, not a copy and not a package** — so the
|
|
# two apps cannot drift on the format contract: a storage change that breaks the phone breaks
|
|
# this project's build, in this repo, on the day it's made.
|
|
#
|
|
# What it deliberately does NOT compile: LiveStore (FSEvents — macOS-only machinery; the phone's
|
|
# change signal is NSMetadataQuery over the ubiquity container), Git/History (no git support in
|
|
# the mobile MVP), Tier, and all of `Kanban/UI`. The mobile app's own sources live under
|
|
# `KanbanMobile/`.
|
|
#
|
|
# Boards live in the iCloud Drive container `iCloud.dev.rzen.indie.Kanban` — named after the
|
|
# *Mac* app's bundle id because container ids are not bundle ids, and that name is the one the
|
|
# Mac app can adopt later without migrating anyone's data (see KanbanMobile.entitlements).
|
|
|
|
KanbanMobile:
|
|
type: application
|
|
platform: iOS
|
|
sources:
|
|
- KanbanMobile
|
|
# The shared storage engine — frontmatter, loader, writer, ranks, integrity. Pure
|
|
# Foundation + Yams + swift-markdown; no AppKit anywhere in the directory, which is what
|
|
# makes this line possible. One exclusion: AgentGuide reaches up into LiveStore's
|
|
# EchoLedger (its bracketed-write seam), and the AGENTS.md guide it writes is a Mac
|
|
# feature — agents work where the Mac app runs.
|
|
- path: Kanban/Storage
|
|
excludes:
|
|
- AgentGuide.swift
|
|
# The writer layer's one upward dependency: BoardWriter stamps every write into the
|
|
# task-local receipt ledger so a store can tell its own echoes from foreign changes. The
|
|
# recording side is pure CryptoKit + Foundation + Synchronization and compiles here; the
|
|
# ledger's *consumer* surfaces — verdicts (BoardDiff), harvest (HarvestedReceipt), comment
|
|
# retirement (CommentPath) — are the Mac watcher/committer's vocabulary and sit behind
|
|
# `#if os(macOS)` gates inside the file. On the phone the "watcher" is the metadata-query
|
|
# observer; when it grows a verdict surface, those gates are the seam it lands behind.
|
|
- path: Kanban/LiveStore/EchoLedger.swift
|
|
dependencies:
|
|
- package: Yams
|
|
# BodyMarkup (Kanban/Storage) imports Markdown, so the dependency rides along even though
|
|
# the MVP has no preview surface yet.
|
|
- package: swift-markdown
|
|
product: Markdown
|
|
- package: IndieBackup
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.KanbanMobile
|
|
MARKETING_VERSION: "1.0"
|
|
INFOPLIST_FILE: KanbanMobile/Info.plist
|
|
CODE_SIGN_ENTITLEMENTS: KanbanMobile/KanbanMobile.entitlements
|
|
GENERATE_INFOPLIST_FILE: false
|
|
SWIFT_STRICT_CONCURRENCY: complete
|
|
# iPhone only. The iPad milestone widens this to "1,2" rather than adding a target.
|
|
TARGETED_DEVICE_FAMILY: "1"
|
|
|
|
# MARK: - Unit tests
|
|
#
|
|
# The suite, hosted by the app: every test in `KanbanTests/` exercises the storage format, the
|
|
# live store or the UI model through `@testable import Kanban`, so it needs the app's module and
|
|
# its bundle around it. `Fixtures/` rides in as a folder reference for the same reason the
|
|
# templates do — a fixture is a board, and a board is directories on disk.
|
|
|
|
KanbanTests:
|
|
type: bundle.unit-test
|
|
platform: macOS
|
|
sources:
|
|
- KanbanTests
|
|
- path: Fixtures
|
|
type: folder
|
|
buildPhase: resources
|
|
dependencies:
|
|
- target: Kanban
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.KanbanTests
|
|
GENERATE_INFOPLIST_FILE: true
|
|
SWIFT_STRICT_CONCURRENCY: complete
|
|
|
|
# MARK: - UI tests
|
|
#
|
|
# These launch the real app and drive its menu bar — the slowest, most environment-dependent part
|
|
# of the suite. The accessibility audits (10-accessibility.md ▸ Verification) walk every surface
|
|
# the design names.
|
|
|
|
KanbanUITests:
|
|
type: bundle.ui-testing
|
|
platform: macOS
|
|
sources:
|
|
# The verification documents live with the tests they belong to (10-accessibility.md
|
|
# ▸ Verification: "A manual VoiceOver smoke script lives with the test plan"), so they are
|
|
# listed in the project — visible where the suite is — but with no build phase: they are
|
|
# documentation, not resources the bundle should carry. `EndToEndVerification.md` is the same
|
|
# arrangement for the golden-flow, fail-fast and performance suites: the run command, the
|
|
# prerequisites, and the flows that stayed manual.
|
|
- path: KanbanUITests
|
|
excludes:
|
|
- "**/*.md"
|
|
- path: KanbanUITests/AccessibilityVerification.md
|
|
buildPhase: none
|
|
- path: KanbanUITests/EndToEndVerification.md
|
|
buildPhase: none
|
|
dependencies:
|
|
- target: Kanban
|
|
settings:
|
|
base:
|
|
PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.KanbanUITests
|
|
GENERATE_INFOPLIST_FILE: true
|
|
SWIFT_STRICT_CONCURRENCY: complete
|
|
|
|
schemes:
|
|
# `xcodebuild … -scheme Kanban` is the established command for this repo, and it now means the
|
|
# only app there is.
|
|
Kanban:
|
|
build:
|
|
targets:
|
|
Kanban: all
|
|
run:
|
|
config: Debug
|
|
# StoreKit Testing in Xcode (12-editions.md ▸ Distribution): a ⌘R run resolves
|
|
# `dev.rzen.indie.kanban.pro.monthly` against the local `Configuration.storekit` instead of
|
|
# against the App Store, so the Settings Pro section shows a price and its purchase flow
|
|
# completes on a machine with no App Store Connect product and no sandbox account.
|
|
#
|
|
# It changes nothing about a build or an archive — it is a *run action* setting, so `xcodebuild
|
|
# build`, `xcodebuild test` and `scripts/release.sh` are untouched, and a shipped app resolves
|
|
# the real product or reports that it cannot reach the App Store.
|
|
storeKitConfiguration: Configuration.storekit
|
|
test:
|
|
config: Debug
|
|
gatherCoverageData: false
|
|
targets:
|
|
- KanbanTests
|
|
- KanbanUITests
|
|
profile:
|
|
config: Release
|
|
analyze:
|
|
config: Debug
|
|
archive:
|
|
config: Release
|