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
+16 -7
View File
@@ -1,6 +1,14 @@
import Foundation
import os
/// ** Retired by the one-app collapse one-app collapse phase 2.** 12-editions.md App-side
/// state (re-ruled 2026-07-30) removes the App Group wholesale: with one app there is no sibling
/// to share a container *with*, so the registry and its peers home in the ordinary sandbox
/// container, records carry one grant and one open-now flag, and this type goes away with the
/// entitlement. Everything below still describes the shipping code, and the code still works
/// it is simply describing a world that is being dismantled in a later phase, so the two-app
/// reasoning is left standing rather than half-rewritten into a fiction.
///
/// **The family App Group** where every edition's app-side state lives (12-editions.md
/// § Distribution, ruled 2026-07-29; 02-architecture.md § Per-board app state).
///
@@ -44,10 +52,11 @@ public enum AppGroup {
/// case and never a shipped app's.
public static let baseEditionID = "dev.rzen.indie.Kanban"
/// Pro's bundle id (12 Targets, ruled 2026-07-27). Named here as well as in
/// `ProEdition.bundleIdentifier` because *base* has to know it: the popover's awareness line and
/// the grant-slot keying are shared-tree code that must be able to name the other edition
/// without compiling any of it.
/// The bundle id the retired Pro *app* would have carried (12 Targets, ruled 2026-07-27)
/// **one-app collapse phase 2**: no app has ever shipped under it, and nothing will now that Pro
/// is a subscription rather than a second binary (12 Distribution, re-ruled 2026-07-30). It
/// stays only because the awareness line and the grant-slot keying still read it; both go in the
/// same phase, and this constant with them.
public static let proEditionID = "dev.rzen.indie.KanbanPro"
/// Which edition is running the key every per-edition slot on a shared record is stored under.
@@ -128,12 +137,12 @@ public enum AppGroup {
///
/// ### Why the app has to know
///
/// The unit-test host **is the app** (`KanbanTests` and `KanbanProTests` are hosted bundles), so
/// The unit-test host **is the app** (`KanbanTests` is a hosted bundle), so
/// `KanbanApp.init()` runs for real on every test launch and builds an `AppModel` over whatever
/// the defaults resolve to. Every *object* a test constructs takes its storage by injection that
/// is the seam, and it is untouched but the host's own launch has no injection point, and after
/// the 2026-07-29 ruling the thing it would reach for is a container shared with the sibling
/// edition and with the developer's running copy. Its launch sweep would collect real staged
/// the 2026-07-29 ruling the thing it would reach for is a container shared with the developer's
/// own running copy. Its launch sweep would collect real staged
/// clipboard trees; its `refreshRecents()` would resolve, refresh and rewrite real records.
///
/// So the *default* moves for a test host, which is the one place a default can be wrong in a way
+1 -1
View File
@@ -219,7 +219,7 @@ public enum BoardAnnouncer {
/// What that operation wants said when it lands "Pulled 3 commits", "Switched to branch
/// 'redesign'". `nil` for a bracket whose completion is not worth speech, which is every
/// base-edition bracket today (see `BoardStore.performWholesale(announcing:_:)`).
/// free-tier bracket today (see `BoardStore.performWholesale(announcing:_:)`).
public var completion: String?
/// The snapshot comparison **already narrowed to the foreign-classified changes**
+4 -4
View File
@@ -19,11 +19,11 @@ import Foundation
/// ### Why it lives in `LiveStore/` and not beside the announcement
///
/// 10 says announcements "reuse the auto-committer's summarizer" and 06-history-undo.md says the
/// committer synthesizes its commit messages from the same comparison. The committer is Pro-edition
/// committer synthesizes its commit messages from the same comparison. The committer is Pro-tier
/// work (pro-m1) and does not exist yet, so the summarizer is built here first, in the layer both
/// consumers can reach: base compiles `Kanban/`, Pro compiles `Kanban/` *plus* `KanbanPro/`
/// (12-editions.md Targets), so a type in the store layer is available to the committer by
/// construction while a type in `KanbanPro/` would not be available to the announcer. It imports
/// consumers can reach: the store layer is below the history providers, so it is available to the
/// committer by construction, where a type living inside the git provider would not be reachable
/// from the announcer (12-editions.md The provider seam). It imports
/// `Foundation` alone and touches no view, no window, and no `NSAccessibility` the phrasing is
/// `AccessibilityPhrases`' job, and pro-m1's message composer will phrase the very same counts
/// differently without either of them knowing about the other.
+5
View File
@@ -65,6 +65,11 @@ public struct WindowFrame: Codable, Sendable, Equatable {
///
/// ### One record, shared by every edition with two per-edition fields
///
/// ** one-app collapse phase 2**: the two per-edition fields collapse to one grant and one flag
/// when the App Group goes (12-editions.md App-side state, re-ruled 2026-07-30) there is no
/// second sandbox to hold a slot for. The section below describes the shipping code, which still
/// works; it is retired, not wrong.
///
/// The record lives in the family App Group container (`AppGroup`), so base, Pro and later Teams all
/// read and write the same one: an upgrader's recents, frames and settings are simply *there*
/// (12-editions.md Distribution, ruled 2026-07-29). Two fields cannot be common, and both are
+5 -5
View File
@@ -344,7 +344,7 @@ public final class BoardStore: HealHost {
/// Its consumers today are the announcer's digest and the vanishing-focus sentence, both
/// through `land`. Pro's auto-committer (06-history-undo.md) becomes the second one without
/// this line changing which is why the type lives in `LiveStore/` beside `BoardDiff` rather
/// than in `KanbanPro/`.
/// than inside the git provider.
@ObservationIgnored
public let echoes = EchoLedger()
@@ -477,7 +477,7 @@ public final class BoardStore: HealHost {
/// raised lock above the completion), and the phrase must not survive to be spoken by some
/// later, unrelated reload.
///
/// **`nil` on every base-edition bracket today.** Base has no git operations, and the design's
/// **`nil` on every free-tier bracket today.** The free tier has no git operations, and the design's
/// examples ("Pulled 3 commits", "Switched to branch 'redesign'") are pro-m1's; the parameter
/// exists so that milestone supplies phrasing rather than re-plumbing the seam.
@ObservationIgnored
@@ -1111,8 +1111,8 @@ public final class BoardStore: HealHost {
/// - Parameter completion: what to announce when the closing reload lands
/// (10-accessibility.md Live board announcements: "bracketed operations announce once, at
/// completion" "Pulled 3 commits", "Switched to branch 'redesign'"). `nil`, the default, is
/// an operation whose completion is not worth speech, which is **every base-edition bracket
/// today**: base has no git operations, and the two app-initiated writes that do reach disk on
/// an operation whose completion is not worth speech, which is **every free-tier bracket
/// today**: no git operations run there, and the two app-initiated writes that do reach disk on
/// their own the loose-file relocation and the legacy-tombstone migration are ordinary
/// `performWrite` calls that already say what they did on the banner strip. The parameter is
/// the seam pro-m1 fills; see `wholesaleCompletion`.
@@ -3881,7 +3881,7 @@ public final class BoardStore: HealHost {
on: self
) { () throws(BoardWriteError) -> Void in
// One bracket over the displacement *and* the write: two files change, one app-mediated
// reload lands, and (in the Pro edition) one honestly-attributed commit records it.
// reload lands, and (under Pro) one honestly-attributed commit records it.
guard let moved = try AgentGuide.install(atBoardRoot: root) else { return }
// The `CLAUDE.user.md` rescue is the settled, silent ownership rule; a squatter's
// displacement is announced.