diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-01 23:50:29 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-01 23:50:29 -0500 |
| commit | 93c70b2b42174caa101d3de1fc46630dcdbcf941 (patch) | |
| tree | 752f098350b2d85ce62022979bff16ee2ededa23 /docs/local-and-linear-views-spec.org | |
| parent | 92a3be1085359dfb60d4861670398ee3f496fe99 (diff) | |
| download | pearl-93c70b2b42174caa101d3de1fc46630dcdbcf941.tar.gz pearl-93c70b2b42174caa101d3de1fc46630dcdbcf941.zip | |
docs(filter): document multi-state selection and reconcile the views boundary
I documented the new multi-state capability and brought the views spec and README back in sync with it. The README's filter section notes the State prompt is multi-select and shows the (:state ("Todo" "In Review")) form.
The local/Linear views spec's reverse-compile dimension table, singular-vs-plural rule, and refusal list now list state.name.in as a plural-capable exception alongside :labels and :state-type, and the README copy-down paragraph no longer claims a multi-value filter on any single dimension refuses. The views vNext note records that the named-states case shipped here.
Phase 3 of docs/multi-state-filter-spec.org. Docs only.
Diffstat (limited to 'docs/local-and-linear-views-spec.org')
| -rw-r--r-- | docs/local-and-linear-views-spec.org | 10 |
1 files changed, 5 insertions, 5 deletions
diff --git a/docs/local-and-linear-views-spec.org b/docs/local-and-linear-views-spec.org index 4ff9035..0a836ee 100644 --- a/docs/local-and-linear-views-spec.org +++ b/docs/local-and-linear-views-spec.org @@ -238,7 +238,7 @@ Copy-down forks a Linear view into a new, editable, renamable local view. Becaus | =:assignee :me= | =assignee.isMe.eq = true= (after unwrapping =or=) | primary | | =:assignee-id= ID | =assignee.id.in=[ID]= / =.eq= (after unwrapping =or=) | primary (builder default) | | =:assignee= EMAIL | =assignee.email.eq= | legacy / back-compat | -| =:state= NAME | =state.name.eq= / single =state.name.in=[NAME]= | primary (=pearl--compile-state-filter=) | +| =:state= NAME or NAMES | =state.name.eq= / =state.name.in=[NAME…]= (one or a list) | primary (=pearl--compile-state-filter=) | | =:state-type= TYPE(S) | =state.type.eq= / =state.type.in=[TYPE…]= (one or many) | primary (=pearl--compile-state-filter=) | | =:open t= | =state.type.nin=["completed","canceled","duplicate"]= (order-insensitive, exactly that set) | primary — Pearl's open predicate | | =:project= ID | =project.id.in=[ID]= / =.eq= | primary | @@ -252,11 +252,11 @@ The id-based forms are what the current interactive builder writes (=:assignee-i Three current compiler shapes need a precise rule, because the compiled Linear filter loses information the authoring plist had: -- *=:state= vs =:state-type= are distinct keys.* =:state= NAME= compiles to =state.name.eq=; =:state-type= TYPE-OR-LIST= compiles to =state.type.in=. The reverse must emit the matching key — =state.type.in= becomes =:state-type=, never =:state= — and both still take precedence over =:open= as =pearl--compile-state-filter= does. (The first draft collapsed both into one =:state= row, which couldn't round-trip a type filter.) +- *=:state= vs =:state-type= are distinct keys.* =:state= (a name or a list of names) compiles to =state.name.eq= / =state.name.in=; =:state-type= TYPE-OR-LIST= compiles to =state.type.in=. The reverse must emit the matching key — =state.type.in= becomes =:state-type=, never =:state= — and both still take precedence over =:open= as =pearl--compile-state-filter= does. (=:state= is plural-capable per the multi-state-filter spec: a multi-name =state.name.in= reverse-compiles to =(:state (list))=.) - *=:priority= canonicalizes to the integer.* =pearl--compile-priority= maps both =(:priority high)= and =(:priority 2)= to =priority.eq=2=, so the symbol-vs-integer choice is not recoverable. Copy-down therefore emits the *canonical numeric* form: =priority.eq=N= reverse-compiles to =(:priority N)= for N in 0..4; any other value refuses. The "identical plist" round-trip promise holds with this one documented canonicalization — a symbol-authored filter round-trips to its numeric equivalent, which compiles to the same Linear filter. - *=:label-id= is singular.* Pearl has no plural =:label-ids= authoring key, so =labels.some.id= with one id (=.eq= or one-element =.in=) emits =:label-id=, and =labels.some.id.in= with more than one id *refuses* in v1 (refuse-don't-guess — collapsing to one id or inventing a key would both be wrong). =labels.some.name.in= with any number of names stays representable as =:labels=. -*The general singular-vs-plural =in= rule.* The label case is one instance of a rule that covers every dimension: *a singular authoring key accepts only =eq= or a one-element =in=; a multi-value =in= on a singular key refuses.* Pearl's singular keys are =:assignee-id=, legacy =:assignee= (email), =:project=, =:cycle=, =:team=, and =:label-id= — so =assignee.id.in=[A,B]=, =assignee.email.in=[A,B]=, =project.id.in=[P1,P2]=, =cycle.id.in=[C1,C2]=, =team.key.in=[T1,T2]=, and multi-id =labels.some.id.in= all refuse: no =:assignee-ids= / =:project-ids= / … key exists, and collapsing to one value would silently narrow the view. The only keys that accept a multi-value =in= are the two genuinely plural ones — =:labels= (from =labels.some.name.in=[NAMES…]=) and =:state-type= (from =state.type.in=[TYPES…]=). Plural authoring keys for the singular dimensions are vNext. +*The general singular-vs-plural =in= rule.* The label case is one instance of a rule that covers every dimension: *a singular authoring key accepts only =eq= or a one-element =in=; a multi-value =in= on a singular key refuses.* Pearl's singular keys are =:assignee-id=, legacy =:assignee= (email), =:project=, =:cycle=, =:team=, and =:label-id= — so =assignee.id.in=[A,B]=, =assignee.email.in=[A,B]=, =project.id.in=[P1,P2]=, =cycle.id.in=[C1,C2]=, =team.key.in=[T1,T2]=, and multi-id =labels.some.id.in= all refuse: no =:assignee-ids= / =:project-ids= / … key exists, and collapsing to one value would silently narrow the view. The keys that accept a multi-value =in= are the genuinely plural ones — =:labels= (from =labels.some.name.in=[NAMES…]=), =:state-type= (from =state.type.in=[TYPES…]=), and =:state= (from =state.name.in=[NAMES…]=, added by the multi-state-filter spec). Plural authoring keys for the remaining singular dimensions are vNext. *=:open= is the one recognized negation.* Pearl's =:open t= is the common "not closed" predicate and compiles (via =pearl--compile-state-filter=) to =state.type.nin=["completed","canceled","duplicate"]= — and Pearl's favorite-derived source filters (=(:project ID :open t)=, =(:label-id ID :open t)=, =(:assignee-id ID :open t)=) lean on it heavily. The reverse-compiler matches *exactly* that =state.type.nin= set, order-insensitive, back to =:open t=. This is the lone =nin= the matcher accepts; every other =nin= still refuses (see below). When a normalized view also carries an explicit =state.name= or =state.type= condition, that takes precedence over =:open= exactly as the compiler does, so the round-trip stays faithful. @@ -265,7 +265,7 @@ Three current compiler shapes need a precise rule, because the compiled Linear f - a *real* multi-branch =or= or nested =and= / =or= that survives normalization (genuine disjunction); - a dimension Pearl doesn't model — =parent=, =creator=, =subscribers=, =dueDate=, =estimate=, project milestone, and the rest; - an operator outside the matched set — =neq=, generic =nin= (except Pearl's exact open-state predicate, matched to =:open t= above), =null=, date/number comparators, =labels.every= / =labels.none=; -- a multi-value =in= on any *singular* key — =assignee.id.in=, =assignee.email.in=, =project.id.in=, =cycle.id.in=, =team.key.in=, and multi-id =labels.some.id.in= — refuses, because Pearl has no plural authoring key for those dimensions and collapsing to one value would silently narrow the view. Only =labels.some.name.in= (=:labels=) and =state.type.in= (=:state-type=) accept multiple values. +- a multi-value =in= on any *singular* key — =assignee.id.in=, =assignee.email.in=, =project.id.in=, =cycle.id.in=, =team.key.in=, and multi-id =labels.some.id.in= — refuses, because Pearl has no plural authoring key for those dimensions and collapsing to one value would silently narrow the view. The keys that *do* accept multiple values are =labels.some.name.in= (=:labels=), =state.type.in= (=:state-type=), and =state.name.in= (=:state=, plural-capable per the multi-state-filter spec). A diagnostic helper returns a *structured reason* (the offending path + construct), and the command formats it: "this Linear view filters on a label parent / uses OR logic / filters by due date, which Pearl's local views can't represent yet — run it directly with =pearl-run-linear-view= instead." A local view that looks like the Linear view but quietly matches a different issue set, diverging on every refresh, is the worst outcome — so copy-down never produces a lossy local view. The probe's own sample bears this out: of seven real Custom Views, the team/project/state/assignee ones normalize cleanly, the "Chore" label-parent-OR view refuses, and the three empty grouping views (={}=) copy down to a no-constraint filter that =pearl-create-local-view='s own "needs at least one constraint" guard then rejects (so an empty view refuses too, with that message). @@ -370,7 +370,7 @@ Unit: direct renames without aliases, absence of user-facing "query" vocabulary - *All-Custom-Views copy-down* — enumerating every Custom View (not just favorited ones) via a paged =filterData= query, with its own cache path. v1 copies down favorited views only; this is the part that needs pagination. - *Richer local authoring model* — OR-logic, nested =and=/=or=, and unmodeled dimensions (label =parent=, due date, estimate, creator). Each one added shrinks copy-down's refused set. Its own spec. - *Broader negative operators* — generic =nin= / =neq= / =null= beyond Pearl's recognized =:open t= predicate. These need authoring-UI semantics (how a user builds a "not X" filter), not just parser support, so they stay vNext; v1 represents only the exact open-state predicate. -- *Plural authoring keys* — =:assignee-ids=, =:assignees= (emails), =:project-ids=, =:cycle-ids=, =:team-keys=, and =:label-ids= — would let multi-value =in= Linear views ("assignee in A or B", "project in P1 or P2", multi-id labels) copy down instead of refusing. Each needs builder support for selecting several values; v1 keeps the singular keys and refuses the multi-value cases. +- *Plural authoring keys for the remaining singular dimensions* — =:assignee-ids=, =:assignees= (emails), =:project-ids=, =:cycle-ids=, =:team-keys=, and =:label-ids= — would let multi-value =in= Linear views ("assignee in A or B", "project in P1 or P2", multi-id labels) copy down instead of refusing. Each needs builder support for selecting several values; v1 keeps the singular keys and refuses the multi-value cases. (The named-states case shipped separately as the multi-state-filter spec: =:state= now accepts a list, so =state.name.in= copies down.) - *Richer local filter editor* — exposing priority symbols, negative operators, and broader state logic as first-class authoring choices (rather than the canonical numeric priority and AND-only model v1 ships). - *Unpublished-changes indicator* — a label like =[local → Linear:Engineering, unpublished]= when a tracked local view's local filter differs from its last-published Linear mirror, so the divergence window after edit (before publish) is visible. - *Remote-drift detection before copy-up* — warn when the tracked Linear view changed on Linear's side before copy-up overwrites it (today copy-up updates in place unconditionally). |
