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 # SwiftGitX 0.4.0 / libgit2 1.9.2 (both `github.com/ibrahimcetin/…`) left with the git excision # (strategy/01-git-excision.md, 2026-08-08) — the exact pins for resurrection are recorded there # and at the `pre-git-excision` tag. 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` — **one version, everything in it** (12-editions.md ▸ # PIVOT 2026-08-08): there is no edition axis for this file to express. 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. 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 dependencies: - package: Yams - package: swift-markdown product: Markdown - package: IndieAbout # The Quick Look preview extension, embedded into `Contents/PlugIns/` — an appex is only ever # found through the app that carries it, so this line is what makes pressing Space on a # `.kanban` package in Finder reach `BoardPreviewProvider` at all. - target: KanbanQuickLook 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 # Product is Lanework.app (user-facing — Finder, Open With, Dock); the Swift module keeps # the Kanban codename so `@testable import Kanban` and internal refs are stable. PRODUCT_NAME: Lanework PRODUCT_MODULE_NAME: Kanban MARKETING_VERSION: "2.0" INFOPLIST_FILE: Kanban/Info.plist # `Kanban/Kanban.entitlements` — sandbox, user-selected files, app-scope bookmarks, # `com.apple.security.print` (added 2026-08-09 — a sandboxed `NSPrintOperation` is denied # without it, with the misleading system alert "This application does not support # printing"), and `com.apple.security.network.client`, **declared now and exercised by # nothing**: the key stays because the sync capability to come needs it regardless # (12-editions.md ▸ PIVOT 2026-08-08), and a key added later is a new provisioning profile # and a new review surface. 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: - Quick Look preview extension # # **Space on a `.kanban` package in Finder** shows the board rather than a folder icon: its name, # its tint, its lanes in order with each one's card count and first few card titles. A classic # NSExtension appex (`com.apple.quicklook.preview`), embedded into the app above — the same # arrangement Xcode's own "Quick Look Preview Extension" template produces, since Quick Look's # preview point has no ExtensionKit spelling. # # **It compiles the shared storage engine verbatim, exactly as `KanbanMobile` does below** — the # same source paths, not a copy and not a package — and for the same reason: a preview that read # the format through its own parser would be a second reading of the storage contract, free to # drift. `Kanban/Storage` is one compilation unit in practice (`FrontmatterDocument` reaches # `IntegrityRules`, which reaches `BoardLoader` and `BoardWriter`), so it is listed whole; the # paired `LiveStore/EchoLedger.swift` is `BoardWriter`'s one upward dependency, riding along here # for the phone target's own stated reason. The write half is dead weight in a preview and stays # dead: the entitlements are read-only, so nothing in here *could* write even if something called # it. # # `STORAGE_ONLY` is what stops that one file dragging the whole live store in behind it. Its # consumer-side sections speak `BoardDiff`/`ItemPath`/`CommentPath`, which reach `BoardStore` and # everything under it; the phone excludes them with `#if os(macOS)` and cannot help here, because # this target *is* macOS. So EchoLedger.swift carries the same gate on a second axis — platform, # and layer — and this is the one target that sets it. See that file's "The reload's verdicts" # comment for the full statement. # # `Kanban/UI/Palette.swift` is the one UI file it takes: the twelve-and-twelve name→hex table, so a # board tinted `smokey-ocean` is the same colour in the preview as in the app. It is self-contained # — AppKit, SwiftUI and `FieldValue`, nothing else in `Kanban/UI` — which is what makes a single # file listable here at all. Nothing else from `Kanban/UI` compiles in: the preview is HTML, not a # view hierarchy, and every other file up there reaches the store. # # The walk itself is `Kanban/Storage/BoardOutline.swift` — capped at every level, read-only, and # unit-tested in `KanbanTests` where it can be, which is the whole reason it lives in the shared # directory rather than in `KanbanQuickLook/`. KanbanQuickLook: type: app-extension platform: macOS sources: - KanbanQuickLook - path: Kanban/Storage - path: Kanban/LiveStore/EchoLedger.swift - path: Kanban/UI/Palette.swift dependencies: - package: Yams - package: swift-markdown product: Markdown settings: base: # Prefixed by the host app's identifier, as the sandbox requires of an embedded extension. PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.Kanban.QuickLook MARKETING_VERSION: "2.0" INFOPLIST_FILE: KanbanQuickLook/Info.plist CODE_SIGN_ENTITLEMENTS: KanbanQuickLook/KanbanQuickLook.entitlements GENERATE_INFOPLIST_FILE: false SWIFT_STRICT_CONCURRENCY: complete # The Info.plist's `NSExtensionPrincipalClass` is `$(PRODUCT_MODULE_NAME).BoardPreviewProvider` # — stated here so the two can never drift apart silently. PRODUCT_MODULE_NAME: KanbanQuickLook # `$(inherited)` keeps the configuration's own `DEBUG` — this only *adds* the layer gate. SWIFT_ACTIVE_COMPILATION_CONDITIONS: $(inherited) STORAGE_ONLY # 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), 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: - path: KanbanMobile excludes: - CHANGELOG.md # The Settings tab's About section (indie-about skill): the shared ISC license verbatim from # the repo root, and a changelog of the phone app's own — it is a separate product with its # own 1.0, so the Mac app's CHANGELOG.md would narrate someone else's releases. Both ride in # Apple's inline-Markdown subset (bold headings, one physical line per paragraph). - path: KanbanMobile/CHANGELOG.md type: file buildPhase: resources - path: LICENSE.md type: file buildPhase: resources # 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. **No exclusions as of 2026-08-09**: `AgentGuide` used to sit # out of this list ("agents work where the Mac app runs"), but the owner's ruling that day # extended the guide to every board-creation call site and to a one-shot refresh at # `BoardSession.open()` — both phone-side — so it has to compile here too. It only reaches # up into `LiveStore/EchoLedger.swift`'s unconditional recording API # (`markHeal`/`recordMove`, below), never the `#if os(macOS)`-gated consumer surfaces, so # nothing about "no FSEvents, no git on the phone" changes. - path: Kanban/Storage # 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 # The Settings tab's About section — same package, same pin as the Mac target above. - package: IndieAbout # Same stamping as the Mac target: CFBundleVersion from git commit count, BuildDate/BuildHash # into the product's Info.plist, so the About section can show "Built YYYY-MM-DD". 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: 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 # The Mac icon's own 1024 master, alpha-flattened (App Store icons must be opaque) into a # single-size iOS set — one product family, one mark. ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # 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 # The host target is named Kanban (codename) but its product is Lanework.app # (PRODUCT_NAME), so XcodeGen's target-name-derived TEST_HOST would miss. TEST_HOST: "$(BUILT_PRODUCTS_DIR)/Lanework.app/Contents/MacOS/Lanework" BUNDLE_LOADER: "$(TEST_HOST)" # 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 # MARK: - iPhone UI tests # # The mobile app's first runtime coverage: launches the real app against a plain directory # (`LANEWORK_LOCAL_ROOT`, `CloudHome`'s DEBUG override) seeded from a copy of # `Fixtures/Valid/rich-board.kanban`, so these tests need no iCloud account and touch nothing but # a scratch directory the test itself creates and owns. `Fixtures/` rides in as a folder reference # for the same reason `KanbanTests`' copy does — a fixture is a board, and a board is directories # on disk. KanbanMobileUITests: type: bundle.ui-testing platform: iOS sources: - KanbanMobileUITests - path: Fixtures type: folder buildPhase: resources dependencies: - target: KanbanMobile settings: base: PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.KanbanMobileUITests 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 test: config: Debug gatherCoverageData: false targets: - KanbanTests - KanbanUITests profile: config: Release analyze: config: Debug archive: config: Release # The phone app's own scheme — otherwise auto-generated, declared here only because it needs a # test action pointed at `KanbanMobileUITests` rather than xcodegen's per-target default. KanbanMobile: build: targets: KanbanMobile: all test: config: Debug gatherCoverageData: false targets: - KanbanMobileUITests