aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/refine-source-spec.org105
1 files changed, 105 insertions, 0 deletions
diff --git a/docs/refine-source-spec.org b/docs/refine-source-spec.org
new file mode 100644
index 0000000..998a37d
--- /dev/null
+++ b/docs/refine-source-spec.org
@@ -0,0 +1,105 @@
+#+TITLE: Spec: refine the current source
+
+* Status
+
+Draft (2026-06-23). Pending Craig's sign-off on the six decisions below — the mutate-vs-layer call (Decision 1) is load-bearing for refresh semantics and should be settled before any code.
+
+* Problem
+
+When the buffer is showing a project, a view, a label, or any rendered source, there's no command to say "now show me just the open ones assigned to Vrezh from this set." The only path is to abandon the current source and rebuild the whole filter from scratch through =pearl-list-issues-filtered= (=C-; L f f=). The user's mental model is "I'm on this page, narrow it." Pearl's model is "start over with a new filter."
+
+Org has =org-match-sparse-tree=, which can client-side hide non-matching headings, but it's an org trick the user has to know, it isn't pearl-aware, and the next refresh blows it away.
+
+Triggered by: 2026-05-27 Craig during the issue-sources Test 2 walk-through ("when I'm on a project page and I want to scope the filter further, is there a way to do that?").
+
+* Current state (the machinery this builds on)
+
+- =pearl--read-active-source= reads =#+LINEAR-SOURCE= back into a source plist; =pearl--write-linear-source-header= writes one back. =pearl--source-with-grouping= is the precedent for "modify a source in place and rewrite the header."
+- The filter compiler =pearl--build-issue-filter= merges single keys into one IssueFilter: =:project=, =:open=, =:state= / =:state-type=, =:label-id=, =:assignee-id= / =:assignee :me=, =:priority=, =:cycle=, =:team=. Adding one constraint is one more key on the plist.
+- =pearl--query-view-async= already takes an optional =filter= arg that the API AND-combines with the view's Linear-side =filterData= (the same mechanism the show-completed-issues work used). So a *view* can be refined server-side without touching its Linear definition.
+- =pearl--read-filter-interactively= is the full builder. It prompts every dimension; refining wants exactly one.
+
+* The six decisions
+
+** Decision 1 — mutate the source (sticky refine), via a refinement stack [LOAD-BEARING]
+
+*Recommend: mutate.* The source grows an ordered =:refinements= list, each entry a single-dimension constraint plist (=(:assignee-id "...")=, =(:state-type "started")=, etc.). The effective fetch is the base source plus every refinement AND-combined. Refresh (=C-; L g=) re-applies base + refinements, so a narrow is sticky — exactly the "I'm on this page" mental model.
+
+The layer alternative (refinement is a session-local overlay, refresh reverts to the base) was considered and rejected: it makes refresh surprising (the narrow vanishes) and can't survive a buffer reopen, which is most of the value.
+
+The refinement *stack* (not a single slot) settles Decision 3 in the same stroke.
+
+** Decision 2 — server-side re-fetch, not client-side narrow
+
+*Recommend: server-side.* Pearl's whole model is "the buffer mirrors Linear," and a client-side hide can't see issues added since the original fetch. Compile the refinements to an IssueFilter and re-fetch. The machinery exists for both source types (Decision 4).
+
+Client-side (=org-match-sparse-tree=) is rejected for the same reasons it's inadequate today: not pearl-aware, blown away on refresh.
+
+** Decision 3 — stackable, with an un-refine
+
+*Recommend: stackable.* Because the source carries a =:refinements= list, the user can narrow repeatedly (project → assignee → priority). =pearl-unrefine-current-source= pops the last refinement and re-fetches; running the base source again (or a dedicated reset) clears all. Each refine and un-refine rewrites the header and re-renders.
+
+** Decision 4 — view sources refine server-side too (no new probing)
+
+*Recommend: server-side for views via the existing =filter= arg.* A =:type view= source keeps its base filter Linear-side; the refinements compile to an IssueFilter passed as =pearl--query-view-async='s optional =filter=, which the API AND-combines with the view's =filterData=. This is the proven show-completed mechanism, so no live probe is needed. The refinements must be expressible as IssueFilter, which our compiler guarantees.
+
+So both =:type filter= and =:type view= refine through one code path: compile the =:refinements= to a filter, then fetch (filter source merges into its own filter; view source passes it as the extra =filter= arg).
+
+** Decision 5 — keep the header readable
+
+*Recommend: structured data in the header, a short summary in the title.* =#+LINEAR-SOURCE= carries the base source plus the =:refinements= list verbatim (it drives refresh; it's allowed to be long, and =pearl--linear-source-string= already prints in full). The human-facing =#+title= gets a short appended summary, e.g. =Linear — Orchestration Dashboard + @vrezh · open=, so the H1 stays scannable. The refine command composes that summary from the refinement labels.
+
+** Decision 6 — factor a one-dimension reader out of the builder
+
+*Recommend: extract =pearl--read-one-dimension=.* It prompts "Refine by: " over the refinable dimensions (state / label / assignee / priority, plus cycle / project as natural extensions), then prompts the value for the chosen dimension reusing the existing per-dimension =completing-read= helpers, and returns a one-key plist to push onto =:refinements=. =pearl--read-filter-interactively= can then be expressed as a loop over the same reader, or left as-is and the reader shared — either way no duplicated dimension logic.
+
+* Proposed commands
+
+- =pearl-refine-current-source= (=C-; L f r=) — read =#+LINEAR-SOURCE=, prompt one dimension + value via =pearl--read-one-dimension=, push it onto =:refinements=, rewrite the header, re-fetch + re-render.
+- =pearl-unrefine-current-source= (=C-; L f R=, tentative) — pop the last refinement, rewrite, re-fetch. A no-op with a message when there are none.
+
+Both refuse with a clear message when the buffer has no =#+LINEAR-SOURCE= (not a pearl buffer) or when the source type can't be refined (a =:type issue= single-issue buffer — refining one issue is meaningless).
+
+* Source model
+
+A refined source:
+
+#+begin_example
+ (:type filter :name "Orchestration Dashboard"
+ :filter (:project "..." :open t)
+ :refinements ((:assignee-id "vrezh-uuid") (:state-type "started")))
+#+end_example
+
+Effective filter = base =:filter= with each refinement merged in (filter source), or the AND-combined refinements passed as the view =filter= arg (view source). =:refinements= absent or nil = today's behavior, so every existing buffer round-trips unchanged.
+
+* Acceptance criteria
+
+- On a filter source, =pearl-refine-current-source= narrows by one added dimension, the header records the refinement, and refresh re-runs the narrowed filter (sticky).
+- On a view source, the same works via the AND-combined =filter= arg without altering the Linear view.
+- Refinements stack; =pearl-unrefine-current-source= peels the last; clearing all returns the base result.
+- The =#+title= shows a short refinement summary; =#+LINEAR-SOURCE= carries the structured refinements and round-trips.
+- A non-pearl buffer and a =:type issue= buffer both refuse with a message and no fetch.
+- A refinement on a dimension already present replaces rather than duplicates (e.g. refine assignee twice = the last assignee wins), or stacks as an explicit decision — see Open questions.
+
+* Implementation phases (commits)
+
+1. =pearl--read-one-dimension= extracted + unit tests (pure-ish: mock the completing-reads).
+2. The refinement model: compile =:refinements= into the effective filter for both source types; round-trip tests.
+3. =pearl-refine-current-source= + =pearl-unrefine-current-source=, header/title rewrite, dispatch for filter vs view; the keymap + transient bindings. Tests + a live manual-verify entry.
+
+* Out of scope (vNext)
+
+- Saving a refined source as a named local view (the existing "save current source" task already covers capturing a buffer's filter; a refined source is just a filter and rides that).
+- Multi-value refinement on one dimension in a single step (the multi-state filter already makes =:state= a list; refining adds one value at a time for now).
+- A transient-style one-screen refine UI (the ad-hoc-builder-UX task owns that surface; this spec stays prompt-based).
+
+* Open questions for Craig
+
+1. Decision 1 (mutate vs layer) — confirm sticky-refine-via-mutation is the model.
+2. Replace-vs-stack on a repeated dimension (Acceptance, last bullet): if you refine assignee, then refine assignee again, does the second replace the first (recommend: replace, since two assignee constraints rarely both apply) or stack as an AND (which for assignee would usually yield nothing)?
+3. The un-refine key =C-; L f R= — acceptable, or prefer a different binding / a reset-all instead of a pop?
+
+* Review and iteration history
+
+** 2026-06-23 Tue — Claude Code (pearl) — author
+Drafted from the todo task's six design questions, grounded in the filter compiler and the view =filter= arg. Recommends mutate + server-side + stackable + one-dimension reader. Pending Craig's calls on the three open questions.