aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/local-and-linear-views-spec.org197
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.