diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-01 14:15:28 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-01 14:15:28 -0500 |
| commit | 42370043d61b7f8a80a17785f47df359e4f70dda (patch) | |
| tree | e22f76576c08c918060e6630e918708253f7f6f2 /docs/local-and-linear-views-spec.org | |
| parent | 5f50b28cda2c5c87cbd7af6af4931284ac719843 (diff) | |
| download | pearl-42370043d61b7f8a80a17785f47df359e4f70dda.tar.gz pearl-42370043d61b7f8a80a17785f47df359e4f70dda.zip | |
docs: spec for local views and Linear views
I drafted a spec that renames "saved query" to one noun with a location qualifier: a local view (private, on disk) and a Linear view (published, shared, the CustomView Linear users already know). The lifecycle is git-shaped: a local view is a local branch, a Linear view a remote branch, publish is push, and the stored view-id is the tracking link.
The model already exists in the code. What's missing is a coherent name and command shape. Today the saved-query operations scatter across the verb-first keymap: create is hidden inside the filter builder and filed under fetch, delete has a home, edit doesn't exist. The spec covers the rename map (obsolete aliases so no init.el breaks), new create and edit commands, the keymap/transient/picker reshape, and the config migration. One piece isn't a rename: pulling a Linear view back into a local view needs a reverse-compile of IssueFilter, deferred to vNext.
Status is Draft with six open questions. No code changed.
Diffstat (limited to 'docs/local-and-linear-views-spec.org')
| -rw-r--r-- | docs/local-and-linear-views-spec.org | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/docs/local-and-linear-views-spec.org b/docs/local-and-linear-views-spec.org new file mode 100644 index 0000000..c163526 --- /dev/null +++ b/docs/local-and-linear-views-spec.org @@ -0,0 +1,197 @@ +#+TITLE: Spec: local views and Linear views +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-01 + +* Status + +*Draft — awaiting Craig's review.* Triggered by the 2026-06-01 conversation that started as "how do I create a saved query?" and surfaced that the saved-query operations are scattered inconsistently across the verb-first keymap: creation is hidden inside the filter builder and filed under fetch, deletion has a proper home, and editing doesn't exist at all. The conversation converged on a cleaner mental model — one familiar noun, =view=, qualified by where it lives: a *local view* (private, on your machine) and a *Linear view* (published, shared, the =CustomView= Linear users already know). This spec captures that model and the rename + reshape needed to adopt it. + +This is the successor framing to =docs/saved-query-sync-spec.org= (which shipped the local→Linear sync) and =docs/issue-sources-spec.org= (which unified the fetch surface behind =pearl-pick-source=). It does not redo their work; it renames and re-homes it so the whole lifecycle speaks one vocabulary. + +* Problem + +Pearl already has the machinery for named, reusable filters and for pushing them up to Linear. What it lacks is a coherent *name* and a coherent *command shape*, and the gap shows up the moment a user goes looking for an operation by the noun: + +- The thing is called a "saved query" — a term Pearl invented. Linear has no "query" and no "saved query." A Linear user arrives knowing "filter" (the conditions) and "view" (the saved, shareable thing), and finds neither word. +- The command that *creates* a saved query is named =pearl-list-issues-filtered= ("build a filter"), lives under the *fetch* group (=C-; L f f=), and saves only as a yes/no afterthought at the end of the builder. The *create* group (=C-; L c=) holds "create issue" and "create comment" but no "create saved query." A user looking in the obvious place finds nothing. +- "saved query" appears under *delete* (=C-; L k q=) but not under *create*. It is a deletable noun that is not a creatable one — the asymmetry that sends people hunting. +- There is no *edit* at all. The edit group covers description / state / assignee / labels / comment (all issue-level); a saved query's filter, sort, or order can only be changed by delete-and-recreate or by hand-editing =pearl-saved-queries= in =init.el=. +- "view" is overloaded. =pearl-run-view= ("custom view", =C-; L f v=) runs a Linear =CustomView=; the transient has a "View" column for open-in-browser; and "viewing" a saved query is actually "fetch saved query". The word collides three ways with no home for the saved-filter concept. + +The CRUD lifecycle currently reads C(hidden, misfiled) R(fetch) U(absent) D(delete) — three of four verbs land where a user would not predict. + +* Non-goals + +- *Not* a rewrite of the sync engine. =customViewCreate/Update/Delete=, the collision (Replace/Rename/Cancel) flow, the scope/shared prompt, and the =:linear-view-*= tracking metadata all stay as built in =saved-query-sync-spec.org=. They get renamed, not redesigned. +- *Not* a change to how issues render or save. This spec touches the source/view surface only. +- *Not* OR-logic filters. Local views stay AND-only in v1, same as saved queries today; OR lives in a Linear Custom View authored on Linear's side. +- *Not* syncing =:sort= / =:order= up to Linear. Still unsupported by =CustomViewCreateInput= (probe-confirmed 2026-05-28); sort/order stay local-only conveniences. + +* The model + +One noun — *view* — with a qualifier that names where it lives. The qualifier is the only real difference: visibility and location. + +| Concept | Name | Linear's own term | +|--------------------+---------------+-------------------------------------------| +| The conditions | filter | filter (=IssueFilter=) | +| Saved, on disk | local view | (no analog — a local filter you named) | +| Published, shared | Linear view | View (=CustomView=, wraps an =IssueFilter=) | + +The lifecycle is git-shaped, which is the analogy that makes it self-explanatory: + +| Pearl | git | +|------------------------------------+---------------------------| +| local view | local branch | +| Linear view | remote branch | +| publish (local view → Linear view) | push | +| pull (Linear view → local view) | fetch + checkout (vNext) | +| the =:linear-view-id= link | the upstream tracking ref | + +A view can have a local copy, a Linear copy, or both. Pearl already stores exactly this: a =pearl-saved-queries= entry with no =:linear-view-id= is local-only; one *with* a =:linear-view-id= is a local view that has been published and now tracks a Linear view. The data model is already git-shaped — this spec names it. + +Two facts the model has to keep honest: + +1. *=filter= is not retired.* It stays the conditions-building step (Linear's "Filter" button) and the on-disk authoring plist. You *filter* to assemble conditions; when you name and keep the result, it is a *local view*. An unsaved run is an *ad-hoc filter* — not yet any kind of view. +2. *Not every Linear view has a local view behind it.* A =CustomView= authored in Linear's web UI, or a teammate's favorite, is a Linear view with no local tracking copy — a remote branch with no local branch. Pearl runs those via the picker today; the model accommodates them as Linear-only views. + +* Current state (what exists, what's missing) + +** Exists + +- =pearl-saved-queries= (defcustom, alist =(NAME . SPEC)=) — the local-view store. SPEC carries =:filter=, optional =:sort= / =:order=, and (when published) =:linear-view-id=, =:linear-view-team-id=, =:linear-view-shared=, =:linear-view-url=. +- =pearl-list-issues-filtered= (=C-; L f f=) — builds an ad-hoc filter interactively, runs it, and offers to save it (=pearl--save-query=, persisted via =customize-save-variable=). This is the de-facto *create* path. +- =pearl-run-saved-query= (=C-; L f q=) — runs a named local view's stored filter. +- =pearl-delete-saved-query= (=C-; L k q=) — deletes a local view, optionally its linked Linear view too. +- =pearl-sync-saved-query-to-linear= (=C-; L f S=) — publishes a local view as a =CustomView=; first sync prompts scope + visibility, re-sync updates in place, name collisions prompt Replace/Rename/Cancel; stamps the =:linear-view-*= keys back. +- =pearl-publish-current-source= (=C-; L f P=) — reads the buffer's =#+LINEAR-SOURCE=; if it names a local view, publishes that one. +- =pearl-run-view= (=C-; L f v=, "custom view") — runs a Linear =CustomView= by id. +- =pearl-pick-source= (=C-; L f s=) — the unified runner: lists Linear favorites first, then local views, each tagged (=[saved]=, =[saved → SCOPE]=, =[KIND]=). Already spans the local/Linear split and dispatches synced entries through the view branch. +- =pearl--saved-query-scope-label= / =pearl--pick-source-candidates= — render the kind/scope labels. + +** Missing + +- No *create* command under the create group — creation is a side effect of the filter builder. +- No *edit* command at all — a local view's filter/name/sort/order can't be changed in Emacs. +- No *pull* — a Linear view authored on Linear's side can be *run* but not saved as a local view (requires reverse-compiling =IssueFilter= back into Pearl's authoring plist; the compiler is currently one-way). +- The vocabulary: nothing says "view"; everything says "saved query". + +* Proposed design + +** Vocabulary + +Adopt =filter= / =local view= / =Linear view= everywhere user-facing: command names, labels, prompts, transient groups, README, docstrings. "saved query" disappears from the surface (kept only as obsolete aliases, below). + +** Rename map + +User-facing renames, each with a back-compat alias (see Migration): + +| Today | Proposed | Kind | +|------------------------------------+--------------------------------+------------------------------| +| =pearl-saved-queries= (defcustom) | =pearl-local-views= | =define-obsolete-variable-alias= | +| =pearl-run-saved-query= | =pearl-run-local-view= | =define-obsolete-function-alias= | +| =pearl-delete-saved-query= | =pearl-delete-local-view= | =define-obsolete-function-alias= | +| =pearl-sync-saved-query-to-linear= | =pearl-publish-local-view= | =define-obsolete-function-alias= | +| =pearl-run-view= | =pearl-run-linear-view= | =define-obsolete-function-alias= | +| =pearl-publish-current-source= | =pearl-publish-current-view= | =define-obsolete-function-alias= | + +New commands: + +| New | What it does | +|------------------------------+------------------------------------------------------------------------------| +| =pearl-create-local-view= | Build a filter interactively and save it as a named local view (create slot) | +| =pearl-edit-local-view= | Edit an existing local view: re-run the builder pre-seeded with its filter, and adjust name / sort / order | +| =pearl-pull-linear-view= (vNext) | Save a Linear view (custom view / favorite) as a local view — needs the reverse-compile (see below) | + +=pearl-list-issues-filtered= stays as the *ad-hoc, save-less* filter run (rename optional; see Open question 2). =pearl-create-local-view= becomes the deliberate create path; it may share the builder internals with =pearl-list-issues-filtered= and =pearl-edit-local-view=. + +** Command surface, by verb + +Every view operation hangs off the *view* noun under the verb where a user would look: + +- *create* a local view — =pearl-create-local-view= +- *edit* a local view — =pearl-edit-local-view= +- *run* a view (local or Linear) — =pearl-pick-source= (unified) plus direct =pearl-run-local-view= / =pearl-run-linear-view= +- *delete* a local view — =pearl-delete-local-view= +- *publish* a local view → Linear view — =pearl-publish-local-view= (and =pearl-publish-current-view=) +- *pull* a Linear view → local view — =pearl-pull-linear-view= (vNext) + +** Keymap reshape (proposed — keys open to review) + +Goal: the =view= noun reachable under each verb, with a consistent sub-key. The =f= (fetch) group's existing keys collide if both local and Linear view runs want =v=, so one proposal: + +| Chord | Command | +|-------------+-------------------------------| +| =C-; L c v= | =pearl-create-local-view= | +| =C-; L e v= | =pearl-edit-local-view= | +| =C-; L k v= | =pearl-delete-local-view= | +| =C-; L f s= | =pearl-pick-source= (run any view) | +| =C-; L f l= | =pearl-run-local-view= | +| =C-; L f v= | =pearl-run-linear-view= | +| =C-; L f p= | =pearl-publish-local-view= | +| =C-; L f P= | =pearl-publish-current-view= | +| =C-; L f u= | =pearl-pull-linear-view= (vNext) | + +The old =q= (saved query) bindings stay live via the obsolete aliases during the deprecation window; the help labels switch to the new vocabulary immediately. Exact key letters are the most reviewable detail here — the principle (one noun, present under every applicable verb) is the load-bearing part. + +** Transient reshape + +The =pearl-menu= "Workspace" section currently splits Fetch / View / Setup. Reword so the run/publish/create/edit/delete of views read as one family. A "Views" sub-group listing create / edit / run local / run Linear / publish / delete keeps the noun together, while ad-hoc filtering and the by-dimension fetches stay in Fetch. + +** Picker labels + +Reword =pearl--pick-source-candidates= / =pearl--saved-query-scope-label= so the local/Linear split is legible at a glance (exact wording is Open question 6): + +- local-only: =[local] NAME= +- published local: =[local → Linear:SCOPE] NAME= (shows the tracking link and where it lives) +- native Linear view / favorite: =[linear] TITLE= or =[KIND] TITLE= + +This directly answers the "which of my views are public?" question Craig wanted to see — the label *is* the public/private indicator. + +** The pull direction (the one piece that isn't cosmetic) + +=pearl-pull-linear-view= is the only proposed operation that needs new engine work, not a rename. Pearl's filter compiler is one-way: authoring plist → =IssueFilter= JSON (=pearl--build-issue-filter=). Pulling a Linear view into a *local* view means reading that view's =filterData= (an =IssueFilter=) back into a Pearl authoring plist — a reverse-compile that does not exist. Two honest options: + +1. *Full reverse-compile* — map every =IssueFilter= shape Pearl emits back to the authoring plist. Bounded but real; round-trip tests required. +2. *Opaque local view* — store the pulled =filterData= verbatim and run it as-is, without making it editable in Pearl's builder. Cheaper, but an opaque local view can't be edited via =pearl-edit-local-view=, which dents the model's symmetry. + +Recommend deferring pull to vNext and shipping the rename + create + edit + publish reshape first (Open question 3). + +* Migration + +- =pearl-saved-queries= is a public defcustom set in users' =init.el=. Rename to =pearl-local-views= with =define-obsolete-variable-alias 'pearl-saved-queries 'pearl-local-views "pearl 1.1"= so existing configs keep working and Customize forwards transparently. The alias reads and writes the same storage; no data migration of the alist contents is needed (the =:filter= / =:linear-view-*= shape is unchanged). +- Each renamed command gets =define-obsolete-function-alias= so muscle memory and any user keybindings survive the deprecation window. +- =#+LINEAR-SOURCE= file headers are unaffected — they record a source descriptor, not the command name. Internal source plists (=:type 'view= / =:type 'filter=) may be renamed in lockstep or left as-is; they are not user-facing. +- README, =package-summary.md=, and the transient help strings switch to the new vocabulary in the same change. + +* Open questions (for Craig) + +1. *Variable name:* =pearl-local-views= (recommended), =pearl-saved-views=, or keep =pearl-saved-queries= as the storage name and only relabel the UI? Recommend the rename with an obsolete-alias — the whole point is that the vocabulary is consistent end to end. +2. *Ad-hoc builder:* does =pearl-list-issues-filtered= keep its inline "save this filter?" prompt, or does creation move entirely to =pearl-create-local-view= (leaving =pearl-list-issues-filtered= as a pure save-less ad-hoc run)? Recommend a dedicated create command + keep the inline prompt too (two doors to the same create), unless you'd rather have exactly one create path. +3. *Pull in v1 or vNext?* Recommend vNext — it is the only non-cosmetic piece and needs the reverse-compile. The rest (rename + create + edit + publish reshape) is shippable without it. +4. *Edit semantics:* re-run the builder pre-seeded with the stored filter (recommended, reuses the builder), or a field-by-field editor? Pre-seeded re-run is simpler and matches how filters are authored. +5. *Keymap letters* for local-view run vs Linear-view run in the =f= group (the =v= collision). The table above is a proposal; your call on the letters. +6. *Picker label wording* — exact strings for local / published / native-Linear entries. + +* Acceptance criteria + +- Every view operation (create, edit, run, delete, publish) is reachable under the verb a user would predict, all hanging off the =view= noun. +- =pearl-saved-queries= and every renamed command keep working via obsolete aliases; an existing =init.el= needs no edit. +- The picker, transient, keymap help, prompts, README, and docstrings all speak =filter= / =local view= / =Linear view=; "saved query" appears only in the obsolete-alias declarations. +- =pearl-create-local-view= and =pearl-edit-local-view= exist and round-trip a local view (create → edit → run → publish → delete) with tests. +- Full ERT suite green, =make lint= and byte-compile clean. + +* Implementation phases (commits) + +1. *Rename + aliases* — rename the defcustom and the four commands, add obsolete aliases, reword docstrings. No behavior change. (=refactor:=) +2. *Create + edit* — =pearl-create-local-view=, =pearl-edit-local-view=, builder extraction shared with =pearl-list-issues-filtered=. Tests. (=feat:=) +3. *Keymap + transient reshape* — the view noun under each verb; help labels. Tests for the keymap. (=feat:=) +4. *Picker labels* — reword =pearl--pick-source-candidates= / scope-label; tests. (=refactor:= / =feat:=) +5. *README + package-summary* — document filter / local view / Linear view, the git-style lifecycle, and the create/edit/run/publish/delete surface. (=docs:=) +6. *vNext (separate spec or follow-up):* =pearl-pull-linear-view= + the reverse-compile. + +* Out of scope (vNext) + +- =pearl-pull-linear-view= and the =IssueFilter= reverse-compile. +- Project / initiative scope on publish (still team/personal only, per =saved-query-sync-spec.org=). +- OR-logic local views. +- Syncing =:sort= / =:order= to Linear. |
