Files
lanework/project.yml
T
rzen b18f7ca609 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
2026-08-09 02:37:10 -04:00

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