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
+6
View File
@@ -2,6 +2,12 @@ import Foundation
import Testing
@testable import Kanban
/// ** 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: one app, one sandbox, one grant, one
/// open-now flag. This whole file is the two-app arrangement's proof, and it retires with the code
/// it pins it is kept green in the meantime rather than deleted ahead of the machinery, because
/// the machinery is what still ships.
///
/// **App-side state in the shared App Group container** (12-editions.md Distribution and Both
/// editions installed, ruled 2026-07-29; 02-architecture.md § Per-board app state).
///
+3 -3
View File
@@ -371,7 +371,7 @@ struct BoardAnnouncerSpeechTests {
#expect(BoardAnnouncer.speech(for: facts) == "Pulled 3 commits")
}
/// Every base-edition bracket today. The seam exists; pro-m1 supplies the phrases.
/// Every free-tier bracket today. The seam exists; pro-m1 supplies the phrases.
@Test("A bracket with no phrase to say stays silent rather than falling back to the digest")
func bracketWithoutAPhrase() {
var facts = BoardAnnouncer.ReloadFacts()
@@ -733,7 +733,7 @@ struct BoardAnnouncerStoreTests {
#expect(log.lines == ["Card 'Fix login' was deleted externally"])
}
/// The seam pro-m1 fills. No base-edition operation passes a phrase today, so this is the one
/// The seam pro-m1 fills. No free-tier operation passes a phrase today, so this is the one
/// place the completion path is exercised end to end including that the phrase is *consumed*
/// rather than left armed for whatever reload comes next.
@Test("A bracketed operation announces at completion, once, and never again")
@@ -755,7 +755,7 @@ struct BoardAnnouncerStoreTests {
#expect(log.lines == ["Board changed: 1 card edited"], "the phrase was consumed, not carried forward")
}
@Test("A bracket with no phrase to say stays quiet — every base-edition bracket today")
@Test("A bracket with no phrase to say stays quiet — every free-tier bracket today")
func silentBracket() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
+3 -2
View File
@@ -2,8 +2,9 @@ import Foundation
import Testing
@testable import Kanban
/// **The board popover's git-note detection** (12-editions.md § Base and `.git`, ruled 2026-07-27):
/// base's whole git story is one line, shown only on a board that carries an inert `.git`. The seam
/// **The board popover's git-note detection** (12-editions.md The free tier and `.git`, ruled
/// 2026-07-27): the free tier's whole git story is one line, shown only on a board that carries an
/// inert `.git`. The seam
/// that decides *whether* to show it `BoardGitNote.hasGitDirectory(at:)` is a pure, read-only
/// `FileManager` check, pinned here against real bytes on disk. Everything else about the note (its
/// wording, its placement in `BoardInfoView`) is SwiftUI rendering and deliberately untested.
+13 -7
View File
@@ -2,9 +2,15 @@ import Foundation
import Testing
@testable import Kanban
/// **Base's inert-`.git` posture, stated against real bytes on disk** (12-editions.md Base and
/// `.git`): "any `.git` is inert opening a board that has one works normally, but the app never
/// reads history, never commits, never touches `.git` in any way."
/// **The inert-`.git` posture, stated against real bytes on disk** (12-editions.md The free tier
/// and `.git`): "any `.git` is inert opening a board that has one works normally, but the app
/// never reads history, never commits, never touches `.git` in any way."
///
/// It is unconditional app behaviour, not a build-time posture: the app that ships is the only app
/// there is (12 The target, re-ruled 2026-07-30), and it runs this way for everyone without a
/// subscription and for everyone whose subscription has lapsed "unsubscribed and lapsed are one
/// state". So these tests assert what `Kanban` does, full stop; nothing here is conditioned on a
/// tier, and the git provider that will one day touch `.git` composes over its own boards.
///
/// Two halves of that posture are already pinned elsewhere and are referenced, not repeated:
/// `FolderWatcherTests` ".git filtering" proves the watcher ignores churn under a `.git` at any
@@ -13,8 +19,8 @@ import Testing
/// what the app does with events and entries it is handed.
///
/// This file states the **output** claim, which no existing test covers, and which is the one the
/// edition split has to be able to demonstrate: a full session of ordinary base editing leaves
/// every byte and every mtime under `.git` exactly as it found them. It is asserted the only way
/// posture has to be able to demonstrate: a full session of ordinary editing leaves every byte and
/// every mtime under `.git` exactly as it found them. It is asserted the only way
/// that is worth anything by snapshotting the whole `.git` subtree from the filesystem before
/// the edits and re-reading it afterwards, never through the app's own read path
/// (`WriterTestSupport.swift`'s standing rule).
@@ -140,7 +146,7 @@ private struct InertGitBoard {
// MARK: - The posture
struct BaseInertGitTests {
struct InertGitTests {
@Test("A full session of ordinary edits leaves every byte and mtime under .git untouched")
func anEditingSessionNeverTouchesGit() throws {
let board = try InertGitBoard()
@@ -149,7 +155,7 @@ struct BaseInertGitTests {
let before = try snapshotSubtree(board.root, ".git")
#expect(before.count == 12, "the fixture's own shape — files, directories and the root")
// A session's worth of every write the base app can make, in one go. `.git` is at the
// A session's worth of every write the app can make, in one go. `.git` is at the
// board root, so anything that walks or renumbers the root's children walks past it.
try BoardWriter.updateIndex(inItemFolder: board.url(""), operation: .rename(title: nil)) { document in
document.set(FrontmatterKeys.title, to: .string("Renamed board"))