1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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.
|