From d17c112110aacf012bc3dfa7224448887a98af55 Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Tue, 15 Sep 2026 04:27:02 -0700 Subject: [PATCH] docs: correct the stale AI provisioning comments and record the list-folders decision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pieces of bookkeeping whose absence is what makes a wrong conclusion get re-derived by the next sweep. PR #83 fixed the AI copy for #39 — it no longer tells subscribers to add a provider key that does not exist — but five surrounding comments still describe the disproved model, saying AI is gated on "a provider key the user supplied on the web Integrations page" and that a run costs "a call against their own provider key". The user-facing string was right and the code around it argued the opposite, which is exactly how the wrong copy gets written back. Corrected against a live probe: GET /api/ai/status -> 200 {"subscriber":true,"providers":["anthropic"], "quota":{"usedToday":0,"dailyLimit":50}} AI is provided by the platform as part of the subscription; the scarce thing to show the user is quota, not money, and an empty providers[] is a service outage they can do nothing about. The list-folders decision (#49) was recorded only as an issue comment, where that issue's own acceptance criterion asked for docs/decisions/. Written up as 0007, including the two consequences that are easy to lose: the capability gate ships 11 Feature cases rather than 12 because .listFolderCreation was dropped, and a list's folderId must be preserved rather than managed — a list filed into a folder on the web stays filed there after a macOS edit. Refs #39, #49 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gSWb3scYobtxLJioV1qF9 --- App/Features/AI/AIAssistantViewModel.swift | 12 ++-- App/Features/AI/AIComposerMenu.swift | 7 +- .../Sources/InterlinedDomain/Models/AI.swift | 30 ++++++-- ...007-list-folders-not-returning-to-macos.md | 69 +++++++++++++++++++ 4 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 docs/decisions/0007-list-folders-not-returning-to-macos.md diff --git a/App/Features/AI/AIAssistantViewModel.swift b/App/Features/AI/AIAssistantViewModel.swift index deb095a..6bae8f5 100644 --- a/App/Features/AI/AIAssistantViewModel.swift +++ b/App/Features/AI/AIAssistantViewModel.swift @@ -5,11 +5,13 @@ // views stay declarative: a view asks for an action, watches `phase`, and applies // an accepted result through `AIAssistantOutcome`. // -// Two rules shape this type. First, **every run costs the user money** — a quota -// unit and a call against their own provider key — so nothing runs implicitly: -// no run on appear, no retry on failure, no speculative prefetch beyond the free -// availability read. Second, a preview is *never* applied on its own; the user -// confirms, and for series the confirm is what persists anything. +// Two rules shape this type. First, **every run spends a unit of the user's daily +// quota** — 50 on the probed account — so nothing runs implicitly: no run on +// appear, no retry on failure, no speculative prefetch beyond the free +// availability read. (The cost is quota, not money: AI is included in the +// subscription and the user holds no provider key — GitHub #39.) Second, a +// preview is *never* applied on its own; the user confirms, and for series the +// confirm is what persists anything. // // Per Decision 0003 this view model consumes only `InterlinedDomain`. diff --git a/App/Features/AI/AIComposerMenu.swift b/App/Features/AI/AIComposerMenu.swift index cdea750..960f66d 100644 --- a/App/Features/AI/AIComposerMenu.swift +++ b/App/Features/AI/AIComposerMenu.swift @@ -5,9 +5,10 @@ // // The menu is always visible but disables itself with a *reason* when AI is off, // because "greyed out with no explanation" is the worst version of a gated -// feature — a subscriber with no provider key would otherwise have no way to -// learn what is missing. The reason comes from the server, not from a local -// guess at entitlements. +// feature. The reason comes from the server, not from a local guess at +// entitlements — which matters because the reasons are not all the user's to +// fix: a lapsed subscription is, a quota is until tomorrow, and an empty +// `providers[]` is a service outage they can do nothing about (GitHub #39). // // Per Decision 0003 this view consumes only `InterlinedDomain`. diff --git a/Packages/InterlinedDomain/Sources/InterlinedDomain/Models/AI.swift b/Packages/InterlinedDomain/Sources/InterlinedDomain/Models/AI.swift index 6c1a2b8..c7f1e58 100644 --- a/Packages/InterlinedDomain/Sources/InterlinedDomain/Models/AI.swift +++ b/Packages/InterlinedDomain/Sources/InterlinedDomain/Models/AI.swift @@ -103,10 +103,21 @@ public enum AIDocumentMode: Sendable, Equatable { // MARK: - Availability -/// Whether AI can be used right now, and if not, why. AI is gated on **two** -/// things the server owns: an active subscription and a provider key the user -/// supplied on the web Integrations page. Asking the server beats guessing from -/// `customerStatus`, because a paying user with no key still cannot call. +/// Whether AI can be used right now, and if not, why. +/// +/// AI is gated on the **subscription alone**. There is no user-supplied provider +/// key and no separate AI bill: per `/help/ai` and `/help/settings`, AI is +/// powered by Claude and provided by InterlinedList as part of the subscription. +/// Live probe 2026-09-15 on a subscriber account: +/// +/// ``` +/// GET /api/ai/status → 200 {"subscriber":true,"providers":["anthropic"], +/// "quota":{"usedToday":0,"dailyLimit":50,"remaining":50}} +/// ``` +/// +/// So an empty `providers[]` is a **service-side** state, not a user +/// misconfiguration — see `unavailableReason`. Asking the server still beats +/// guessing from `customerStatus`, because it also carries the quota. public struct AIAvailability: Sendable, Equatable { public let isSubscriber: Bool /// Provider slugs the account has configured, e.g. `["anthropic"]`. @@ -172,8 +183,11 @@ public struct AIQuota: Sendable, Equatable { public var hasRemaining: Bool { dailyLimit <= 0 || remaining > 0 } } -/// Token spend and model for one AI call. Shown next to a preview so the cost of -/// the user's own provider key is visible rather than hidden. +/// Token spend and model for one AI call. +/// +/// Shown next to a preview so the run is legible rather than opaque. It is **not** +/// a bill: the user has no provider key and pays nothing per call beyond their +/// daily quota. Quota, not money, is the scarce thing to show them. public struct AIUsage: Sendable, Equatable { public let inputTokens: Int? public let outputTokens: Int? @@ -359,7 +373,9 @@ public enum AIGenerationResult: Sendable, Equatable { /// Failures the AI surface can present meaningfully. Anything else propagates as /// the underlying `APIError`. public enum AIError: Error, Sendable, Equatable { - /// The account is not a subscriber, or has no provider key configured. + /// AI cannot be used right now: no subscription, no quota left, or the + /// service reporting no providers. The `reason` is the sentence to show — + /// it never asks the user to configure something that does not exist. case unavailable(reason: String) /// The draft is shorter than the feature's minimum. case inputTooShort(minimumWords: Int) diff --git a/docs/decisions/0007-list-folders-not-returning-to-macos.md b/docs/decisions/0007-list-folders-not-returning-to-macos.md new file mode 100644 index 0000000..99ce628 --- /dev/null +++ b/docs/decisions/0007-list-folders-not-returning-to-macos.md @@ -0,0 +1,69 @@ +# 0007 — List folders do not return to macOS + +- **Status:** Accepted (2026-09-13) +- **Date:** 2026-09-13 +- **Decided by:** the repository owner, on [GitHub issue #49](https://github.com/CompositeCode/interlinedlist-macos-native/issues/49) +- **Context:** PR #19 (`refactor/lists-remove-list-folders`, merged 2026-09-06) removed the List Folders feature from the macOS client on the understanding that it had been retired platform-wide. The 2026-09-07 parity sweep established that it had **not** been. +- **Supersedes / superseded by:** re-opens and closes the old **G6**. + +## Context + +List folders are a **live, documented, current web feature**. Verified live on +2026-09-07: + +``` +GET /api/folders → 200 {"folders":[]} +POST /api/folders (subscribers only) +PUT /api/folders/{id} +DELETE /api/folders/{id} +``` + +`/help/lists` documents them in detail — arbitrary nesting, 1–80 character names +unique among siblings, no-move-under-your-own-descendant, and a delete that +cascades to subfolders while re-parenting their lists to the root. Every list row +on the wire carries a `folderId`. + +So after PR #19 the macOS client has a **one-way divergence**: lists organised +into folders on the web appear flat on the Mac, and a `folderId` set elsewhere is +invisible and uneditable here. + +Two positions were defensible: + +- **Rebuild it.** The web ships it and the API serves it; restore parity. +- **Keep the divergence deliberately.** macOS already has owned-list + `parentID` parent/child nesting — untouched by PR #19 and still shipping — + which covers much of the same organising need. + +What was *not* defensible was the state as found, where the divergence was +undocumented and read as an oversight. + +## Decision + +**macOS does not rebuild list folders.** The divergence is deliberate. The web +keeps its folder tree; the Mac organises lists with parent/child nesting. + +## Consequences + +**The capability gate ships 11 cases, not 12.** The `fix/account-status-gating` +branch carried a `.listFolderCreation` case in both `GatedAction` and `Feature`, +written while the client still had the feature. It was dropped before that branch +merged as PR #83. A gate for a capability the client does not have is dead code +that reads like a promise. + +**Parent/child list nesting is a different feature and is unaffected.** It was +never removed, it is not folders, and a future sweep should not conflate them. + +**A list's `folderId` is preserved, not managed.** macOS neither reads nor writes +it, and must not clear it — a list filed into a folder on the web stays filed +there after a macOS edit. (`UpdateListRequest` carries no `folderId`, so this +holds by construction today; anything that adds one must keep it.) + +**Issue #49 stays open on purpose**, labelled `wontfix`, as the standing product +record. Closing it would make the divergence look like an oversight again, which +is the exact failure this decision exists to prevent. + +## Notes + +GitHub issue **#28** ("New Folder still shows up in Lists") was closed as stale +against a pre-PR-#19 binary. That was correct and is unrelated: #28 was about a +*leftover control*, this is about the *absent feature*.