Collapse the edition split — one target, twins merged, verify-editions retired

Phase 1 of the 2026-07-30 one-app pivot (DESIGN 0bec9a6, card c3a3ddd5):
the KanbanPro target, LaneworkPro scheme, KanbanProTests module-alias
bundle, KanbanPro/ source root and scripts/verify-editions.sh retire
wholesale. project.yml reads as a single-target file again (anchors
inlined, header rewritten in tier vocabulary).

The edition twins merge: EditionTypes -> PasteboardTypes (one
UTType(exportedAs:) home — the one app owns the family types),
EditionAbout -> AboutBox (the quiet Pro signpost survives as the About
box's one line; "…in Settings" deferred until the StoreKit phase gives
it somewhere to point). InertGitTests drops its Base prefix — the
inert-.git posture is unconditional app behavior, unsubscribed and
lapsed being one state.

Entitlements gain com.apple.security.network.client, declared now and
dormant until Pro's remotes use it; no keychain access group. The App
Group key deliberately stays — it goes with AppGroup.swift in phase 2,
since pulling it first would silently drop the app into the fallback
container. README/RELEASE.md build-and-pipeline prose updated to the
one-record world; the subscription story lands with phase 3.

1893 tests in 322 suites green.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-30 17:25:50 -04:00
parent f7c8088783
commit 092300c7d2
52 changed files with 253 additions and 905 deletions
+34 -124
View File
@@ -17,8 +17,8 @@ packages:
swift-markdown:
url: https://github.com/apple/swift-markdown.git
minorVersion: 0.8.0
# The About window (both editions): app icon, version/build from the stamped Info.plist, and
# the bundled LICENSE/CHANGELOG documents. Pre-1.0 like swift-markdown, same pinning logic.
# 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
@@ -30,18 +30,20 @@ settings:
DEVELOPMENT_TEAM: ${APPLE_TEAM_ID}
ENABLE_USER_SCRIPT_SANDBOXING: NO
# The edition split (12-editions.md ▸ Targets). Two app targets compiled from one source tree:
# base builds `Kanban/` alone, Pro builds `Kanban/` *plus* `KanbanPro/`. There is no build flag
# and no `#if` in shared code — "why a real split and not feature flags" (12) is answered by the
# file list, so an edition difference is always a file one target compiles and the other does not.
# libgit2 arrives on the Pro target with pro-m1; the target, its entitlements and its source root
# exist now so that milestone lands without re-splitting anything.
# **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`; **Lanework Pro is a subscription inside it**, not a
# second binary. 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.
#
# YAML anchors (`&name` / `*name`) carry the genuinely identical parts across the two apps and the
# two unit-test bundles. They are resolved by the YAML parser before XcodeGen sees the file, so
# they cost nothing at generate time and cannot drift the way copy-paste does.
# 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.
#
# libgit2 arrives here with pro-m1 — a dependency of the one target, dormant behind the subscription
# gate rather than the contents of a second download.
targets:
# MARK: - Lanework (base)
# MARK: - Lanework
Kanban:
type: application
@@ -64,12 +66,12 @@ targets:
- path: CHANGELOG.md
type: file
buildPhase: resources
dependencies: &appDependencies
dependencies:
- package: Yams
- package: swift-markdown
product: Markdown
- package: IndieAbout
postBuildScripts: &updateBuildInfo
postBuildScripts:
- script: '"${SRCROOT}/../indie-skills/skills/app-versioning/scripts/update_build_info.sh"'
name: Update Build Info
shell: /bin/sh
@@ -79,87 +81,33 @@ targets:
- $(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist
settings:
base:
# Base keeps the current identifier — it is the app that ships first, so nothing
# re-wires (12 ▸ Targets, ruled 2026-07-27).
# 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
MARKETING_VERSION: "2.0"
INFOPLIST_FILE: Kanban/Info.plist
# `Kanban/Kanban.entitlements` — sandbox, user-selected files, app-scope bookmarks, the
# App Group (removed with the app-side-state collapse, a later phase), and
# `com.apple.security.network.client`, which is **declared now and dormant until Pro
# ships**: the one binary carries the key, and nothing exercises it until the git
# provider's remotes do under an active subscription (12 ▸ The target). No keychain
# access group — groups exist to share items *between* apps, and there is one app.
CODE_SIGN_ENTITLEMENTS: Kanban/Kanban.entitlements
GENERATE_INFOPLIST_FILE: false
SWIFT_STRICT_CONCURRENCY: complete
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
# MARK: - Lanework Pro
KanbanPro:
type: application
platform: macOS
sources:
# The shared tree, minus the three files that are base's *identity* rather than its code.
# (XcodeGen already keeps `Info.plist` and `.entitlements` out of build phases; excluding
# them here keeps them out of the Pro target's file list too, so there is exactly one
# plist and one entitlements file visible per app.)
- path: Kanban
excludes:
- Info.plist
- Kanban.entitlements
- Assets.xcassets
- Assets.xcassets/**
# Base's About-box configuration carries the one-line Pro signpost (12-editions.md ▸
# Quiet signposts); Pro compiles its own `KanbanPro/Edition/EditionAbout.swift` twin
# without it — the edition split's file-level doctrine, applied to a string.
- App/EditionAbout.swift
# Base declares the family pasteboard types `UTType(exportedAs:)` to match its
# UTExportedTypeDeclarations; Pro's plist *imports* them (12-editions.md ▸ Targets), so it
# compiles its own `KanbanPro/Edition/EditionTypes.swift` twin using `importedAs` — the
# same doctrine, applied to an initializer.
- App/EditionTypes.swift
# Pro's own source root — reserved now, filled by pro-m1. See KanbanPro/Edition/ProEdition.swift.
- path: KanbanPro
excludes:
- Info.plist
- KanbanPro.entitlements
# The same bundled template store — the inventory is the app's, not an edition's.
- path: Templates
type: folder
buildPhase: resources
# The About documents, same files as base — one license, one changelog, two bundles.
- path: LICENSE.md
type: file
buildPhase: resources
- path: CHANGELOG.md
type: file
buildPhase: resources
dependencies: *appDependencies
postBuildScripts: *updateBuildInfo
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.KanbanPro
MARKETING_VERSION: "2.0"
INFOPLIST_FILE: KanbanPro/Info.plist
CODE_SIGN_ENTITLEMENTS: KanbanPro/KanbanPro.entitlements
GENERATE_INFOPLIST_FILE: false
SWIFT_STRICT_CONCURRENCY: complete
ASSETCATALOG_COMPILER_APPICON_NAME: AppIconPro
# Spelled out because the whole shared-test-sources arrangement below hangs on it: the two
# apps must have *different* Swift module names, since both emit `<module>.swiftmodule`
# into the same `Build/Products/<config>/` and equal names would have one app silently
# overwrite the other's testable interface.
PRODUCT_MODULE_NAME: KanbanPro
# MARK: - Unit tests
#
# One suite, run twice — once hosted by each app. Every test in `KanbanTests/` is
# edition-agnostic (it exercises the storage format, the live store and the UI model, none of
# which differ between editions), so duplicating the *sources* would be duplicating the thing
# that is identical; what is worth running twice is the suite against each *binary*, which is
# what these two targets do. The Pro bundle compiles the very same files — it is the same
# `sources` list, by anchor — against `KanbanPro`.
# 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: &unitTestSources
sources:
- KanbanTests
- path: Fixtures
type: folder
@@ -172,31 +120,11 @@ targets:
GENERATE_INFOPLIST_FILE: true
SWIFT_STRICT_CONCURRENCY: complete
KanbanProTests:
type: bundle.unit-test
platform: macOS
sources: *unitTestSources
dependencies:
- target: KanbanPro
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.KanbanProTests
GENERATE_INFOPLIST_FILE: true
SWIFT_STRICT_CONCURRENCY: complete
# The shared sources say `@testable import Kanban`, and they should: they are testing the
# app, not an edition. `-module-alias` (Swift 5.7+) resolves that import to the Pro
# module for this bundle only — the supported way to bind one source name to a different
# module, and it keeps the 56 test files edition-blind instead of littering them with
# conditional imports.
OTHER_SWIFT_FLAGS: -module-alias Kanban=KanbanPro
# MARK: - UI tests
#
# Base-only, deliberately: these tests launch the real app and drive its menu bar, which is the
# slowest, most environment-dependent part of the suite, and a second copy would double it to
# re-assert something the Pro unit bundle already proves (it launches the Pro app as its test host
# on every run). The accessibility audits (10-accessibility.md ▸ Verification) are edition-blind —
# every surface they walk is built from the shared tree.
# 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
@@ -224,8 +152,8 @@ targets:
SWIFT_STRICT_CONCURRENCY: complete
schemes:
# `Kanban` is unchanged, on purpose: `xcodebuild … -scheme Kanban` is the established command
# for this repo and base is the app it has always meant.
# `xcodebuild … -scheme Kanban` is the established command for this repo, and it now means the
# only app there is.
Kanban:
build:
targets:
@@ -244,21 +172,3 @@ schemes:
config: Debug
archive:
config: Release
LaneworkPro:
build:
targets:
KanbanPro: all
run:
config: Debug
test:
config: Debug
gatherCoverageData: false
targets:
- KanbanProTests
profile:
config: Release
analyze:
config: Debug
archive:
config: Release