Files
lanework/project.yml
T
rzen 9e6f4567df Print: entitle the sandbox, and stop the ⌘P chord from ever falling through
Root cause of the owner's repro (board window frontmost, File ▸ Print…
enabled, chosen from the menu, alert appears anyway): Kanban.entitlements
carried no com.apple.security.print key. The app is sandboxed, and a
sandboxed NSPrintOperation is denied by the sandbox with exactly this
wording — "This application does not support printing. Please contact
the application's developer." — regardless of which code path invokes
it. Fix: add the entitlement.

Alongside it, hardening for a separate, narrower failure mode that
happens to produce the identical alert text by a different mechanism:
PrintCommand used to disable itself over a window that published
neither a board nor a printable card (welcome, the template chooser,
Settings, the restore-bootstrap window, a card window whose board
hasn't joined). A disabled SwiftUI Button still owns its
.keyboardShortcut, so the unclaimed ⌘P chord fell through to AppKit's
own nil-target printDocument: action, whose stock failure is the same
system alert. The row now claims ⌘P unconditionally in every window;
scope resolves at the moment of the action instead (board, then card,
then a polite "Nothing to Print" / "Open a board or a card to print
it." refusal in the app's own voice). The boolean isEnabled(hasBoard:
hasPrintableCard:) becomes a three-way PrintCommand.resolveScope(...)
-> Scope pure function.

Also implements AppDelegate's application(_:printFiles:withSettings:
showPrintPanels:) — Finder's own File ▸ Print… / drag-to-printer /
print-and-open path was previously unhandled, its own separate route
to the same stock alert. PrintCoordinator.printFiles loads each path
headless through BoardLoader (no store, no window) and either prints
it or gives the same one-sentence refusal; the operation-building code
shared with the in-app path is factored out of run(_:) into
makeOperation(for:showsPrintPanel:) and runOperation(_:session:).

Docs: 11-command-nexus.md's Print row, PrintCommand's and
PrintCoordinator's doc comments, KanbanApp.swift's CommandGroup
comment, and project.yml's entitlements comment all narrate the
entitlement as the actual fix and the scope work as hardening beside
it.

Tests: PrintCommandValidationTests now exercises resolveScope's three
arms in place of the old boolean. A new PrintFinderResolutionTests
suite covers PrintCoordinator.resolveFinderPrint(atPath:) — the one
piece of the Finder half a test can drive without handing AppKit a
real print job — against a real board, an empty non-board folder, a
plain file, and an unsupported schema.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 23:46:16 -04:00

310 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
# 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
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: - 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