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:
@@ -38,7 +38,7 @@ The **one named exception** is transient UI state rendering things that don't ex
|
||||
- **AgentGuide** — writes/upgrades the board-root `CLAUDE.md` (see 08-agent-integration.md). Its refresh is a scheduled heal riding the HealScheduler (below).
|
||||
- **IntegrityRules** — the one pure vocabulary of object validity (01-storage-format.md ▸ Validation and healing; settled 2026-07-29): the identity predicate and its canonical form (one rule shared by `ItemID` and the Writer's string-level checks — today's parallel `canonicalIdentity` derivation folds in), the per-field coercion rulebook, shape classification (the readable-but-uneditable shapes), per-kind index validation (the card validator generalized per kind — board, lane, card, the enhanced schema's comment when it lands), the reserved-name tables (card children, board-root claimed names — today scattered), the trash `kind` discriminator, and the typed **Defect** vocabulary the loader reports. `LoadResult`'s ad-hoc repair channels (loose files, legacy tombstones) become one typed defect stream; tolerate-tier warnings stay warnings — information, not work. **Loader and Writer remain the enforcement points and call in** — the service consolidates rules and policy, never relocates enforcement; a service smeared across the read/write/orchestration boundaries would be worse than the current discipline.
|
||||
- **HealScheduler** — the scheduled-heal engine: the six-step pattern today re-derived per healer in BoardStore (loose-file relocation, tombstone migration, agent-guide refresh), expressed once — compute work from the latest defects → resting-clear when empty → lock-and-writability gate (the read-only-lock deferral plus the guide's narrow `isWritableFile` defense, generalized to every healer) → signature compare → arm the memo *before* attempting → one write bracket whose write half re-verifies each defect against disk → post per one banner-posture table (each defect class declares loss row / silent / failure-only once; BannerCenter still owns all phrasing) → **clear the memo explicitly on success** (today only the guide does; the others' resting states merely happen to converge). Fires uniformly at the reload tail and at registry acquire — closing today's asymmetry where tombstone migration never fires at open. Inline heals (the midpoint-exhaustion renumber-and-retry, the import-boundary remint) stay gesture-scoped, with the renumber's ask-renumber-ask-again two-step as one shared helper instead of today's nine hand-rolled copies; on-touch heals live at the Writer's `updateIndex` seam, which consults IntegrityRules for pending on-touch work on the file it is rewriting (`kind` backfill; the span editor's duplicate-key twin removal and quote-on-first-write are the same class, named).
|
||||
- **HistoryStore** — the history provider behind the edition seam (12-editions.md): the board session binds one `HistoryProviding` implementation at composition — base Lanework's native undo stack (13-native-undo.md, inverse `WriteOperation`s over NSUndoManager) or Pro's git plumbing (06-history-undo.md). Pro-only source; the base target never links git machinery.
|
||||
- **HistoryStore** — the history provider behind the tier seam (12-editions.md): the board session binds one `HistoryProviding` implementation at composition, chosen by the entitlement's local read — the free tier's native undo stack (13-native-undo.md, inverse `WriteOperation`s over NSUndoManager) or Pro's git plumbing (06-history-undo.md). One target since the 2026-07-30 collapse: libgit2 and the git provider compile in dormant, and nothing outside the seam touches git machinery.
|
||||
|
||||
### Live-reload resilience
|
||||
|
||||
@@ -80,7 +80,7 @@ The non-modal banner named throughout the read- and write-side rules above is on
|
||||
### Launch and window lifecycle (settled)
|
||||
|
||||
- **Restoration is a preference** — "Restore open boards at launch" in Settings (⌘, — 11-command-nexus.md), default on. On: boards open at last quit reopen (bookmark-resolved), with their per-board frames and 05's card-window restoration. Off: every launch starts at welcome.
|
||||
- **The restoration set is a live open marker, never an at-quit write** (settled): each registry record (Per-board app state below) carries an open-now flag — **per-edition**, like the grant slots (12-editions.md): an edition restores only the boards *it* had open — set when the board's window opens and cleared on *user-initiated* close — quit's teardown closes deliberately leave it standing (the boards were open at quit by definition; teardown distinguishes user-close from quit-close, and that distinction is the whole mechanism). Restoration reads the flagged records, reopening by `lastOpened` order. Crash recovery falls out for free: after a crash the flags describe what was open at crash time, so relaunch restores it — no separate recovery logic, no once-at-quit stamp to race teardown or miss on a crash. The preference gates only whether the flagged set is consulted; the flags are maintained regardless.
|
||||
- **The restoration set is a live open marker, never an at-quit write** (settled): each registry record (Per-board app state below) carries an open-now flag — set when the board's window opens and cleared on *user-initiated* close — quit's teardown closes deliberately leave it standing (the boards were open at quit by definition; teardown distinguishes user-close from quit-close, and that distinction is the whole mechanism). Restoration reads the flagged records, reopening by `lastOpened` order. Crash recovery falls out for free: after a crash the flags describe what was open at crash time, so relaunch restores it — no separate recovery logic, no once-at-quit stamp to race teardown or miss on a crash. The preference gates only whether the flagged set is consulted; the flags are maintained regardless.
|
||||
- **Welcome appears only when nothing restores** — restoration off, nothing was open, or every restoration failed. Always reachable via Window ▸ Welcome to Lanework. Opening a board from welcome closes welcome.
|
||||
- **Every open passes through a pre-snapshot loading state** (ruled 2026-07-29): the board window appears **immediately** — welcome click, File ▸ Open…, Finder double-click, restoration alike — at its saved frame, its chrome carrying the registry record's cached title and icon (the same no-scan sources the welcome row reads; a first-ever open shows the folder name, the record's provisional display name). The content area holds a quiet loading surface: a centered system spinner appearing only after a short grace (~200 ms) so ordinary fast opens never flash it — no skeleton lanes, the motion language animates real data only. The first snapshot replaces the surface in place (a snap — there is no prior arrangement to animate from). **The walk is cancellable**: ⌘W during loading cancels it and closes the window, an ordinary user-initiated close clearing the open-now flag. **Restoration is parallel**: every flagged window appears at once in loading state (stacked by `lastOpened` order), each walk independent — a slow network board never delays the others and stays closeable while it loads. Failure keeps its settled surface: a fail-fast walk retires its loading window and lands row-level on welcome — welcome returning if the open came from it or from Finder — never a loading window morphing into an error display. The open walk earns no in-progress info row: the loading state is the surface, and the info-row class stays scoped to copy-shaped work.
|
||||
- **Closing the last board window leaves the app windowless** (menu bar alive) — the close is respected. Reactivation (Dock click) with no windows shows welcome.
|
||||
@@ -92,15 +92,14 @@ The non-modal banner named throughout the read- and write-side rules above is on
|
||||
|
||||
## Per-board app state
|
||||
|
||||
State that belongs to the app, not the user's files — the recents list, per-board window frames, the open-now restoration flag (Launch and window lifecycle above), the push-on-commit setting and the once-per-board iCloud warning flag (07-sync-collab.md), and whatever accumulates later — lives in a **board registry in the shared App Group container** (ruled 2026-07-29 — the edition-crossing home, declared by every edition from base 2.0 onward; 12-editions.md): one record per known board, anchored by the **security-scoped bookmark** the sandboxed app keeps anyway for reopening boards — with one per-edition field: bookmarks never cross sandboxes (12), so each edition holds its own grant slot on the shared record, and a record whose grant another edition minted resolves unavailable-until-reopened, its first click running an open panel pre-anchored at the recorded path.
|
||||
State that belongs to the app, not the user's files — the recents list, per-board window frames, the open-now restoration flag (Launch and window lifecycle above), the push-on-commit setting and the once-per-board iCloud warning flag (07-sync-collab.md), and whatever accumulates later — lives in a **board registry in the app's Application Support container** (re-ruled 2026-07-30 — the one-app collapse removed the App Group wholesale; 12-editions.md ▸ App-side state): one record per known board, anchored by the **security-scoped bookmark** the sandboxed app keeps anyway for reopening boards.
|
||||
|
||||
- **Keyed by file identity, never by path.** Bookmarks track renames and moves on the same volume; an opened URL is matched to its record by bookmark resolution / file identity, so a moved board keeps its settings. The recents list *is* this registry sorted by last-opened. **One bookmark per open board** (settled): the open flow mints it once and threads it through — the persisted record and the live store's mid-session re-resolution share the same bookmark, never two independent mints.
|
||||
- **Recents counts are registry-cached.** The lane/card counts in the welcome window come from the record, stamped at last close — no directory scan at welcome time (which would be slow or hang on big/unavailable boards). Staleness until the next open is accepted. **The counts are working items only** (settled, re-grounded 2026-07-28; extended for the lanes-era trash 2026-07-29): items in `.trash/` don't count — a trashed lane subtracts from the lane count and its nested cards subtract from the card count; the row advertises the board's working size, and the trash is an errand, not inventory. Records that can't be counted show without counts: unavailable boards per Graceful orphaning below; a board that fails to load just fails on open, fail-fast — the welcome row doesn't pre-detect it. **A first open that fails fail-fast still records** (settled): the registry record is created before loading, so the failed board lands in recents carrying fail-fast's specifics — retry after fixing the file is one click, uniform with the restored-board failure row. **The record's provisional display name is the folder name** (settled): fail-fast means the frontmatter can't be trusted, and the folder name is the Finder document name the user just picked; the first successful load replaces it with the cached title. A never-successfully-opened record is not a special class — it lingers in recents like any other, and Forget is the eraser for a genuinely mistaken open. **Records that collapse onto one file identity merge silently** (settled — a restored registry file, or an orphan whose bookmark re-resolves onto a recreated board): on detection, the record with the newest `lastOpened` wins wholesale and the others retire — recents never shows one board twice, and per-board settings are conveniences that don't earn a merge UI.
|
||||
- **The row's title and icon are registry-cached too — with live write-through** (settled): the record carries the board's `title`, `icon`, and `iconColor` beside the counts, and the welcome row reads only the record — it never opens any board's `index.md` (the same hang-avoidance that motivated the counts rule). Unlike the counts' at-close stamp, these three refresh **whenever an open board's reload changes them**: the store already holds the new snapshot, so an in-app Board rename (03-board-ui.md) lands in the record instantly — never a welcome row showing a name the user just changed away from — and a foreign rename of an *open* board rides the same path for free. The honest residual: renaming a board that isn't open (an agent editing its root `index.md`) stays stale until the next open — accepted, the counts' staleness class. The title falls back to the folder name per 01-storage-format.md, cached at the same moments.
|
||||
- **Two editions, one file — freshness by stamp** (ratified 2026-07-30): the registry is a wholesale-rewritten JSON shared by two long-lived processes (12-editions.md ▸ Both editions installed), so every registry read *and* mutation first checks a stat-cheap freshness stamp — mtime + size, read via `FileManager.attributesOfItem` (`URL.resourceValues` caches per-instance and never observes the change) — and a moved stamp re-reads the file before proceeding: a mutation always rewrites on top of the other edition's latest state. Disk is the truth, the in-memory array only a cache, every mutation still saves immediately. The residual check-to-write window is last-writer-wins — microseconds wide, costing at most one convenience field, the loss class these settings already accept. Deliberately **no `NSFileCoordinator`** (blocking presenter machinery on a file a wedged group container can hang — observed in development on this very file) and **no per-record split** (multiplies files and complicates the silent identity-collapse merge for no loss class it closes).
|
||||
- **Files-first stays absolute**: nothing app-private is ever written into the board folder — no frontmatter keys, no sidecar files, no xattrs. Two machines sharing a board via a remote each keep their own record (push-on-commit and window frames are genuinely per-machine choices).
|
||||
- **Graceful orphaning**: a record whose bookmark no longer resolves (board deleted, or moved across volumes where bookmarks can't follow) is orphaned — recents surface it as unavailable with Forget; its settings are conveniences and die with it (accepted).
|
||||
- **App-wide state has the same home.** Not everything app-side is board-scoped: quick-style recents (03-board-ui.md), the SSH host-key assignment table and TOFU fingerprint store (07-sync-collab.md — host-scoped), the last-used card-window size (05-card-window.md), the user template store (09-templates.md — plain board folders, deliberately edition-crossing), and their peers live beside the registry in the App Group container (or the group's shared `UserDefaults` suite where a scalar fits — 12-editions.md) — no per-board record involved. Secrets are the named exception: Keychain only, never here (07).
|
||||
- **App-wide state has the same home.** Not everything app-side is board-scoped: quick-style recents (03-board-ui.md), the SSH host-key assignment table and TOFU fingerprint store (07-sync-collab.md — host-scoped), the last-used card-window size (05-card-window.md), the user template store (09-templates.md — plain board folders), and their peers live beside the registry in Application Support (or standard `UserDefaults` where a scalar fits) — no per-board record involved. Secrets are the named exception: Keychain only, never here (07).
|
||||
|
||||
## Caching and search
|
||||
|
||||
|
||||
Reference in New Issue
Block a user