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
376 lines
18 KiB
YAML
376 lines
18 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
|
|
# 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
|