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