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
310 lines
14 KiB
YAML
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
|