Collapse the edition split — one app, Pro as a subscription

2026-07-30 design pass (with the user), superseding the 2026-07-27
two-app split and the 2026-07-29 App Group ruling. Rulings:

- One Mac App Store app (dev.rzen.indie.Kanban, free, 2.0 updates the
  existing record); Lanework Pro = auto-renewable subscription (StoreKit
  2) unlocking the git tier; libgit2 compiles in dormant. The KanbanPro
  target, module-alias tests, verify-editions, UTI/About/Types twins all
  retire. Teams stays deferred — probably a separate app, and never on a
  shared app group.
- No grandfathering: 1.x's free git users get the inert-.git posture;
  unsubscribed and lapsed are one state — histories freeze, resume on
  subscribe via the committer's catch-up commit.
- Entitlement = local read of StoreKit's signed transaction store at
  board-session composition; never a network call on the open path;
  subscribe effective at next open (purchase flow offers reopen); lapse
  never interrupts an open session; expiry-while-offline with auto-renew
  on holds in grace until a real refresh.
- App Group removed wholesale: registry, clipboard staging, template
  store, defaults return to ordinary Application Support; grant slots
  collapse to one bookmark, open-now to one flag; freshness stamp,
  pid-liveness line, group-id provisioning risk all dissolve.
- Quiet signposts carry over; Settings gains the Pro section (subscribe/
  manage/restore) as the third named Pro surface.

12-editions.md rewritten as the tiers doc; edition vocabulary swept to
tiers across 00-13, README, WISHLIST.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-07-30 15:04:09 -04:00
parent c741b02016
commit 0bec9a6be5
14 changed files with 62 additions and 53 deletions
+37 -27
View File
@@ -1,45 +1,51 @@
# Editions
# Tiers
Lanework ships as three editions built from one codebase and one on-disk format. This doc owns the edition axis: what each edition is, how the split is engineered (targets, the provider seam), and which features land where. Individual docs stay edition-agnostic where they can — they conditionalize on **board mode** (none / git / git+remote — 07-sync-collab.md), and this doc defines which modes each edition ships.
Lanework ships as **one Mac App Store app**`dev.rzen.indie.Kanban`, free, 2.0 updating the existing record — built from one codebase and one on-disk format, with **Lanework Pro as an auto-renewable subscription** unlocking the git tier. This doc owns the tier axis: what each tier is, how the gate is engineered (the provider seam, the entitlement), and which features land where. Individual docs stay tier-agnostic where they can — they conditionalize on **board mode** (none / git / git+remote — 07-sync-collab.md), and this doc defines which modes each tier ships.
## The three editions
**Re-ruled 2026-07-30 — the one-app collapse.** This supersedes the 2026-07-27 two-app split (separate base and Pro targets) and the 2026-07-29 App Group ruling that served it. The split's compile-time purity (base never links libgit2, no network entitlement) dragged permanent coexistence machinery behind it: a shared App Group, per-edition grant slots (security-scoped bookmarks never cross sandboxes), registry freshness stamping between two live processes, UTI-ownership twins, a both-apps-installed rulebook — a tax on every layer that generated a steady stream of design findings, all serving a state (two sandboxed apps sharing app-side state) that existed only because the packaging created it. One app makes that state unrepresentable. Costs accepted with eyes open: libgit2 rides dormant in the free download, and the one app declares the network-client entitlement (exercised only under Pro) — the "free app provably has no network access" story is traded for "no network use until you subscribe," which is honest but weaker.
- **Lanework** (base) — no git integration. Boards are plain folders (mode `none` everywhere); undo/redo is macOS-native (13-native-undo.md). The full board experience: lanes, cards, styling, trash, attachments, card window, templates, agents, accessibility.
- **Lanework Pro** — git integration as designed in 06-history-undo.md and 07-sync-collab.md: opt-in init, adoption, git-backed undo/history, branches, remotes, pull/push, auth. Plus Pro-only differentiators (matrix below).
- **Lanework Teams** — Pro plus tracker integration over the reserved enhanced schema (`remote`/`remote-state`, and tracker-*synced* comment threads — comments themselves ship in every edition, 01-storage-format.md / 05-card-window.md). **Deferred** — months out, no design pass yet; this doc only keeps the seam honest.
## The tiers
Why a real split and not feature flags: the editions differ at the binary level — base ships without libgit2 and without the network-client entitlement; Pro links git machinery and talks to remotes. And the strategic reason (settled): Teams' card sync must be **backend-agnostic** — it has to work over git and over a range of trackers — so history and sync must sit behind a genuine provider seam. Base's native undo is the first proof the seam is real: two working history providers before a third arrives.
- **Lanework** (free) — no git integration. Boards are plain folders (mode `none` everywhere); undo/redo is macOS-native (13-native-undo.md). The full board experience: lanes, cards, styling, trash, attachments, card window, templates, comments (when they ship), agents, accessibility.
- **Lanework Pro** (subscription) — git integration as designed in 06-history-undo.md and 07-sync-collab.md: opt-in init, adoption, git-backed undo/history, branches, remotes, pull/push, auth. Plus Pro-only differentiators (matrix below).
- **Lanework Teams** — tracker integration over the reserved enhanced schema (`remote`/`remote-state`, tracker-*synced* comment threads — comments themselves ship in every tier). **Deferred** — no design pass; probably a separate app when it comes. Whatever shape it takes, it will **never share an app group or any cross-app state** with Lanework (ruled 2026-07-30) — files are the only interchange this family recognizes.
## Distribution (settled)
The strategic reason for the seam stands unchanged (settled): Teams' card sync must be **backend-agnostic** — it has to work over git and over a range of trackers — so history and sync sit behind a genuine provider seam. The free tier's native undo is the first proof the seam is real: two working history providers before a third arrives.
Separate Mac App Store apps — real Xcode targets, not an IAP unlock. Both editions declare the same `.kanban` package UTI (`dev.rzen.indie.kanban-board`) verbatim, so any board opens in either app; the format is one format, owned by 01-storage-format.md, with no edition-specific keys. Upgrade path is buying the other app; boards need no migration whatsoever (files are the interchange). **App-side state crosses the boundary through a shared App Group** (ruled 2026-07-29): every edition declares the family group — `group.dev.rzen.indie.Kanban` — and the board registry with its Application Support peers (02-architecture.md ▸ Per-board app state) homes in the group container **from day one**, so there is no migration and no ordering dependency; the entitlement must be in base's entitlements before base 2.0 ships, since base ships first. A paying upgrader launches Pro onto their full recents, frames, and settings — never an empty home screen. The honest caveat, stated: **security-scoped bookmarks never cross apps** (minted per sandbox, App Group or not), so the *list* transfers while *access* re-grants per board — a record another edition minted resolves unavailable-until-reopened, and the first click runs an open panel pre-anchored at the recorded path: one click + Grant per board, once per edition. Teams joins the same group when it arrives — the container is team-scoped, and a future edition's bundle id declares the same group id with zero further ceremony. Secrets stay Keychain-only as ever (07-sync-collab.md — the keychain access group is its own, existing mechanism); the clipboard's staging store joins the group container too (Both editions installed below).
## Distribution (re-ruled 2026-07-30)
**Both editions installed** (ruled 2026-07-29) — a supported steady state, not a transition to hurry past:
One record: `dev.rzen.indie.Kanban`, free, all territories, 2.0 as an update — the 1.x listing simply grows the subscription. The `.kanban` package UTI (`dev.rzen.indie.kanban-board`) is declared and exported once, by the one app — no ownership twins, no default-claim choreography. **Lanework Pro is an auto-renewable subscription** (StoreKit 2), purchased and managed in a **Pro section of Settings (⌘,)** — subscribe, manage, restore purchases. Teams' eventual monetization is deferred with Teams.
- **Pro claims the `.kanban` default on first launch, once.** The purchase is the consent — `.kanban` is the family's own exported UTI, not a shared public type, so the premium edition claiming its own documents is legitimate, and never asking would make the upgrade feel broken (boards keep opening in base). The claim is one-time: a user who later reassigns the default back to base in Finder is respected — Pro never re-asserts. Base never claims and never asks; until Pro's claim, LaunchServices' own resolution stands.
- **The same board open in both apps at once is fine — it is the designed foreign-writer story, not a new hazard.** Base has no git (the inert-`.git` posture above): it never commits and never touches the repo, so there is no committer contention — base writing into a Pro-open board is exactly an agent writing from a terminal, which Pro's machinery (index.lock retry, flush-before-overwrite, abort-own-leftovers, "both versions exist as commits" — 06/07) was designed for; base sees Pro's writes as ordinary foreign reloads. No board-level lock — 02's files-first rule is absolute (no sidecar files), and no gate: "Open with…" into the second edition just works. Two conveniences ride the shared registry (concurrent writes reconciled by the freshness stamp — 02 ▸ Per-board app state): **open-now flags are per-edition** (like the grant slots — an edition restores only the boards *it* had open), and the board popover carries a contextual awareness line ("Also open in Lanework Pro") read from the other edition's flag, pid-liveness-checked so crash residue never lies — a line, never a gate.
- **The clipboard staging store homes in the group container** beside the registry, so ⌘C in one edition pastes **full-fidelity** in the other — snapshot, attachments and all (04-interactions.md's staging lifecycle unchanged; both editions' launch/copy sweeps compute the same answer from the same pasteboard). A genuinely missing snapshot **refuses the paste loudly** (04-interactions.md's refuse-don't-degrade ruling, 2026-07-29) — with the shared store, no longer a structural cross-edition outcome.
**No grandfathering** (ruled 2026-07-30): 1.x shipped git-backed undo free; 2.0's free tier is native undo over the inert-`.git` posture (below). Existing users' boards keep working untouched, their histories stay intact and inspectable in any git client — the app just stops *extending* them until Pro is subscribed, and git resumes exactly where it left off (the committer's whole-root staging collapses the gap into one catch-up commit). No receipt-date logic exists.
## The entitlement (ruled 2026-07-30)
- **A local read, never a network call.** Pro state is read from StoreKit's signed on-device transaction store at **board-session composition** — the open path gains no network dependency (02-architecture.md's hang-avoidance doctrine extends here). Offline with an active subscription is indistinguishable from online.
- **Subscribe takes effect at each board's next open** — the provider binding is a composition-time fact, the design the seam was built for. The purchase flow offers to reopen open boards so the upgrade feels immediate.
- **A lapse never interrupts an open session**: an open board finishes with the provider it composed; the next open composes the native stack over inert `.git`. Unsubscribed and lapsed are **one state** — the inert posture, nothing lost, histories frozen not forfeited.
- **Offline grace resolves toward the paying user**: an on-disk expiry passing while offline, with the last known state *active and auto-renew on*, holds the entitlement until StoreKit actually refreshes and answers. A cancellation (auto-renew off) lapses at expiry, offline or not. Either wrong-for-a-window direction costs nothing: a wrong lapse pauses auto-commits into one catch-up commit; a wrong hold gives away days of local commits — Apple's own billing grace makes the same trade.
- **A fresh install that has never been online** has no cached transactions and reads as the free tier until the first refresh — honest and self-correcting.
## The provider seam
History (and later sync) is a provider behind one protocol boundary, chosen per edition at the composition root:
History (and later sync) is a provider behind one protocol boundary, bound per board session at composition from the entitlement:
- **HistoryProviding** — the undo/redo substrate. Base binds the native undo stack (13-native-undo.md: NSUndoManager over inverse `WriteOperation`s). Pro binds the git provider (06-history-undo.md: undo as forward restore commits over HEAD's first-parent ancestry). Teams inherits Pro's.
- **HistoryProviding** — the undo/redo substrate. The free tier binds the native undo stack (13-native-undo.md: NSUndoManager over inverse `WriteOperation`s). Pro binds the git provider (06-history-undo.md: undo as forward restore commits over HEAD's first-parent ancestry). Teams inherits Pro's.
- **Sync/tracker providers** — deferred with Teams; the reserved schema keys and the one-way file flow (02-architecture.md) are the format-level seam already in place.
What is shared across providers (settled): **06's Undo routing is edition-independent** — focus decides text-undo vs board-undo; only the substrate behind board-undo differs. The command surface is identical (⌘Z/⇧⌘Z, dynamically retitled menu items — both providers use NSUndoManager's title rewriting); menu titles draw on the same semantic vocabulary (06 ▸ Commit messages). A user moving between editions relearns nothing.
What is shared across providers (settled): **06's Undo routing is tier-independent** — focus decides text-undo vs board-undo; only the substrate behind board-undo differs. The command surface is identical (⌘Z/⇧⌘Z, dynamically retitled menu items — both providers use NSUndoManager's title rewriting); menu titles draw on the same semantic vocabulary (06 ▸ Commit messages). A user subscribing (or lapsing) relearns nothing.
## Base and `.git` — the inert posture (settled)
## The free tier and `.git` — the inert posture (settled; now also the lapsed posture)
Base Lanework generalizes the repo-nested stance to every `.git` it meets: **any `.git` is inert**. Opening a board that has one (a Pro user's board, a repo-nested board) works normally — files read and write as on any board, native undo runs, the trash works — but the app never reads history, never commits, never touches `.git` in any way. To base, `.git` at the board root is a stray like any other, preserved verbatim. Pro's external-writer machinery (06 ▸ Interaction with external writers) already reconciles the uncommitted drift a base session leaves behind — a base edit is just a foreign change to Pro. The watcher's `.git` event filtering stays in base (it exists to ignore git churn, which mixed households will produce).
The free tier generalizes the repo-nested stance to every `.git` it meets: **any `.git` is inert**. Opening a board that has one (a formerly-subscribed user's board, a 1.x board, a repo-nested board) works normally — files read and write as on any board, native undo runs, the trash works — but the app never reads history, never commits, never touches `.git` in any way. To the free tier, `.git` at the board root is a stray like any other, preserved verbatim. Pro's external-writer machinery (06 ▸ Interaction with external writers) already reconciles the uncommitted drift a free-tier session leaves behind — a free-tier edit is just a foreign change to the git provider's next composition. The watcher's `.git` event filtering is unconditional (it exists to ignore git churn, which lapsed-and-resumed boards will produce).
Base's popover git slot (03-board-ui.md ▸ Board popover) does not offer add-git. **Its posture is contextual** (settled — ruled 2026-07-27): on ordinary boards the section is simply absent — the popover is rename + style, complete in itself. Only when the board carries an inert `.git` does a calm info line appear: "This board has a git history. Lanework Pro works with it." — an honest explanation of what the folder is, surfacing exactly where the question arises, never a standing ad. The card window's absent History section follows the same pattern: absent, no placeholder.
The free tier's popover git slot (03-board-ui.md ▸ Board popover) does not offer add-git. **Its posture is contextual** (settled — ruled 2026-07-27, carried through the collapse): on ordinary boards the section is simply absent — the popover is rename + style, complete in itself. Only when the board carries an inert `.git` does a calm info line appear: "This board has a git history. Lanework Pro works with it." — an honest explanation of what the folder is, surfacing exactly where the question arises, never a standing ad; it is also the one in-context pointer to Settings' Pro section. The card window's absent History section follows the same pattern: absent, no placeholder.
## Edition matrix
## Tier matrix
The feature sort. Everything not listed rides with "board experience" and is identical everywhere.
| Feature | Lanework | Pro | Teams |
| Feature | Lanework (free) | Pro | Teams |
|---|---|---|---|
| Board experience: lanes, cards, drag & drop, keyboard map, clipboard, search, styling, trash, attachments, card window, templates, welcome screen | ✓ | ✓ | ✓ |
| Agent integration: agent guide, `modified-by` attribution, tolerance rules | ✓ | ✓ | ✓ |
@@ -54,16 +60,20 @@ The feature sort. Everything not listed rides with "board experience" and is ide
| "While you were away" digest (WISHLIST item 1, requires git) | — | ✓ (future) | ✓ (future) |
| Tracker integration (`remote`/`remote-state` sync, tracker-backed boards) | — | — | ✓ (future) |
Docs 06 and 07 are **Pro-edition docs**; every other doc applies to all editions, with mode-conditioned passages (undo availability, popover git surface, the permanent-delete confirmation branch) resolving per the modes the edition ships. Base ships exactly one mode: `none` (with the inert-`.git` posture above); Pro ships the full state machine.
Docs 06 and 07 are **Pro-tier docs**; every other doc applies to all tiers, with mode-conditioned passages (undo availability, popover git surface, the permanent-delete confirmation branch) resolving per the modes the tier ships. The free tier ships exactly one mode: `none` (with the inert-`.git` posture above); Pro ships the full state machine.
## Targets (settled shape; details at implementation)
## The target (re-ruled 2026-07-30)
Three app targets sharing sources: `Lanework` (base — excludes Pro-only source dirs, no libgit2, current minimal entitlements), `LaneworkPro` (adds git sources, libgit2 dependency, network-client entitlement, Keychain access), Teams deferred. **Bundle ids** (settled — ruled 2026-07-27): base keeps `dev.rzen.indie.Kanban` — it is the current app and ships first, so nothing re-wires; Pro mints `dev.rzen.indie.KanbanPro` with matching test-target ids; Teams reserves nothing yet. Shared UTI declaration in both Info.plists (base remains the exporter); distinct icon assets; parallel test targets and schemes. Milestones: the current plan's m7-git-undo and m8-remote-sync become Pro milestones, built as the git HistoryProvider behind the seam after base ships; a new native-undo milestone takes their place for base.
**One app target.** The 2026-07-27 target split retires wholesale: the `KanbanPro` target, scheme, bundle id, module-alias test arrangement, `verify-editions.sh`, and the edition-twin files (EditionAbout, EditionTypes — Info.plist-posture twins existed only because two bundles claimed different ownership) all come out; the UTI is exported once. libgit2 links into the one target when the git provider is built (pro-m1) — dormant code behind the entitlement gate, not a second binary. Entitlements: the current minimal set plus `network-client` (exercised only under Pro; Keychain needs no access group — groups exist for sharing across apps). The pro-m1/pro-m2 milestones are unchanged in content — the git HistoryProvider and remote sync, built behind the seam — they now compile into the one target and activate by subscription.
## Edition naming in base (settled — ruled 2026-07-27)
## App-side state (re-ruled 2026-07-30)
**Quiet signposts.** Base presents as a complete app, not a demo: Pro is named in exactly two places — one line in the About box, and the contextual popover line on `.git` boards (above). Nothing on the welcome screen, nothing in banners. The iCloud/network-volume warning (07-sync-collab.md) is rewritten for base without the git recommendation — it warns on its own merits (eviction, silent forks) and recommends a local folder; there is no git to recommend and no Pro pitch in a warning (a warning that sells reads as manufactured).
One sandbox: the board registry and its Application Support peers (02-architecture.md ▸ Per-board app state) home in the app's **ordinary sandbox container** — the App Group is removed wholesale, superseding the 2026-07-29 group ruling. No group entitlement, no per-edition grant slots (one bookmark per record), no per-edition open-now flags (one flag), no cross-process freshness stamping (one process — macOS apps are single-instance), no "Also open in…" awareness line, no group-id provisioning risk. The clipboard staging store, template store, and scalar defaults follow the same collapse.
## Tier naming in the free app (settled — ruled 2026-07-27, carried through the collapse)
**Quiet signposts.** The free tier presents as a complete app, not a demo: Pro is named in exactly three places — one line in the About box, the contextual popover line on `.git` boards (above), and the Settings Pro section where the subscription actually lives. Nothing on the welcome screen, nothing in banners. The iCloud/network-volume warning (07-sync-collab.md) is written for the free tier without a git recommendation — it warns on its own merits (eviction, silent forks) and recommends a local folder; no Pro pitch in a warning (a warning that sells reads as manufactured).
## Open questions
None currently — bundle ids, the popover slot posture, and edition naming were ruled 2026-07-27 and are settled above.
None currently — the one-app collapse, subscription shape, entitlement semantics, App Group removal, and no-grandfathering were ruled 2026-07-30; the popover slot posture and quiet signposts carry from 2026-07-27.