diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 07:10:33 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 07:10:33 -0500 |
| commit | 394c75b2f92e2a72390b3417e483849bed13a399 (patch) | |
| tree | a5b92603ecb049381015fb7556f25dcf74fa6c21 /docs | |
| parent | 4767d16423ff5c87cf1222603c7b8f1b59af8ef2 (diff) | |
| download | pearl-394c75b2f92e2a72390b3417e483849bed13a399.tar.gz pearl-394c75b2f92e2a72390b3417e483849bed13a399.zip | |
feat(accounts): multi-account support with tests
I added a named-account layer for working more than one Linear workspace from one Emacs.
Before this, everything that identified a workspace was a single global: pearl-api-key, pearl-graphql-url, pearl-org-file-path, and the lookup caches. Nothing stopped a work command from running under personal credentials or a work fetch from landing in the personal file, and switching accounts meant re-customizing the key, team, and file by hand and clearing the cache.
pearl-accounts maps a name to a per-workspace plist (credential source, org file, default team, optional endpoint), and pearl-switch-account makes one active. Account state flows through an explicit context rather than mutating globals. Every request snapshots its account at dispatch through pearl--graphql-request-async and re-establishes it around the callbacks. A switch mid-fetch can't bleed into a request already in flight: the result finishes into the account it was dispatched under. I centralized this in the one request primitive, so the leak surface is a single function instead of every call site.
Rendered files carry a #+LINEAR-ACCOUNT marker, and a buffer guard refuses a command run from one account's file while another is active, naming both, so a work edit can't push under personal credentials. An unmarked legacy file lets reads and refreshes through and acquires its marker on the first refresh. Mutations wait until then. Credentials resolve through auth-source, an env var, or an inline literal, and a resolved key is never persisted through Customize or logged. The active account shows in the mode line. A saved query can carry an :account so it refuses to run under the wrong workspace before any lookup.
With pearl-accounts unset, everything behaves exactly as before, off the legacy globals.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/multi-account-spec.org | 4 |
1 files changed, 2 insertions, 2 deletions
diff --git a/docs/multi-account-spec.org b/docs/multi-account-spec.org index bf85c29..f60adb1 100644 --- a/docs/multi-account-spec.org +++ b/docs/multi-account-spec.org @@ -62,7 +62,7 @@ Rather than mutating globals and hoping downstream is oblivious, account state f Every API operation *snapshots* its account context at dispatch time. The snapshot is the =pearl--resolve-account= plist captured before the request fires. Render and mutate callbacks act against the *snapshot's* org file / key, never the current global. -*v1 decision (confirm):* if the active account changed before a callback completes, the callback still finishes into its snapshot — writing the work result to the work file, with a status message naming the account ("Updated work: 24 issues"). It does *not* write to whatever file is now current, and it does *not* silently drop the result the user asked for. (The reviewer offered finish-into-snapshot or drop-stale; finish-into-snapshot is chosen because the fetch was an explicit user request whose data belongs to its own account's file regardless of a later switch, and round 2 endorsed it as the right default.) +*v1 decision (final):* if the active account changed before a callback completes, the callback still finishes into its snapshot — writing the work result to the work file, with a status message naming the account ("Updated work: 24 issues"). It does *not* write to whatever file is now current, and it does *not* silently drop the result the user asked for. (The reviewer offered finish-into-snapshot or drop-stale; finish-into-snapshot is chosen because the fetch was an explicit user request whose data belongs to its own account's file regardless of a later switch, and round 2 endorsed it as the right default.) *Implementation boundary (where the snapshot is captured and applied).* So a single missed render path can't reintroduce the leak: @@ -186,7 +186,7 @@ Passive. With =pearl-accounts= unset, the package behaves exactly as today off t - *MP2 (saved queries) — modified to a choice.* The reviewer offered account-scoped lists, an optional =:account= guard, or document-and-warn. Chose the optional =:account= guard plus a shared default and a README warning: it keeps the v1 saved-query store unchanged (one list) while giving a refusal path for queries that must not cross accounts, where full account-scoping would add a persistence/UX layer not yet justified for two workspaces. - *MP3 (cache clearing) — modified the test framing.* Accepted the helper (=pearl--clear-account-scoped-state= as the single source of truth, listing every account-scoped cache including future derived state). Softened the reviewer's "a test must fail when a new account-scoped cache is added without invalidation" — you can't test for an unwritten future cache directly. Instead the helper centralizes the list so a new cache is registered in one place, and a test asserts each cache the helper names is nil after a switch. -Also, the reviewer's three open questions and the HP1 finish-vs-drop choice are baked as proposed v1 decisions (finish-into-snapshot, refuse-on-mismatch, =:account= guard) and surfaced under Open Questions for Craig's confirmation rather than left unresolved. Everything else accepted as written. +Also, the reviewer's three open questions and the HP1 finish-vs-drop choice are baked as v1 decisions (finish-into-snapshot, refuse-on-mismatch, =:account= guard) and adopted as final — see Resolved safety decisions. Everything else accepted as written. *Round 2 (Codex, 2026-05-25).* Rubric moved to =Ready with caveats=. All findings accepted as written, no modifications or rejects: HP1 (the implementation-boundary rule — entry resolves context, callbacks run inside the context macro, shared render fns take an explicit target), HP2 (the precise unmarked-buffer rule — render/list proceed, mutations refuse until a refresh stamps ownership), HP3 (=pearl-active-account= is a runtime =defvar=, not a persisted defcustom — a genuine correction to the round-1 draft), MP1 (=pearl-load-api-key-from-env= legacy-only, refuses under accounts), MP2 (=:account= lives in the saved-query entry and is checked before filter compilation, with an example), MP3 (mode-line lighter is the v1 indicator, header-line optional), plus the UX refusal-message-names-both-accounts and the added tests. The remaining caveats are the safety calls, now endorsed by the reviewer. |
