aboutsummaryrefslogtreecommitdiff
path: root/docs/labels-as-org-tags-spec.org
blob: a1bb413b34c22a147a9320d97770728bf11063ea (plain)
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
#+TITLE: pearl — Render Linear Labels as Org Tags Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-24
#+STARTUP: showall

* Status

PROPOSED — awaiting review. Implements the =todo.org= task "Org tags should reflect the issue's Linear labels" (filed 2026-05-24 after a hardcoded personal =#+filetags:= value was removed from the header writer in 952cfe7).

* Problem

Pearl used to stamp a hardcoded personal =#+filetags:= value on every fetched file — file-wide, unrelated to any issue. That's gone. What's missing is the useful behavior: an issue's Linear labels should appear as org tags *on that issue's heading*, so the org-native gestures work — filter by tag, build a tag agenda, sparse-tree on =:bug:=. Today labels live only in the =:LINEAR-LABELS:= drawer (=[bug, backend]=), which org's tag machinery can't see.

* Current state

- =pearl--format-issue-as-org-entry= renders the heading as =** <STATE> [#P] <title>= with no tags, and writes the labels into the =:LINEAR-LABELS:= drawer as =[name, name]=.
- =pearl--normalize-issue= gives each issue =:labels= as a list of plists with =:name=.
- =pearl--issue-title-at-point= already extracts the title with =(org-get-heading t t t t)= — the four flags strip the keyword, priority cookie, *tags*, and comment markers. So title sync is already tag-aware; adding heading tags will not corrupt the title round-trip.
- =pearl-set-labels= changes an issue's labels (completing-read over team labels), pushes, and rewrites the =:LINEAR-LABELS:= drawer.

* Proposed design

** Tag slugify: label name → org tag

A new pure helper =pearl--label-name-to-tag=. Org tags are restricted to =[[:alnum:]_@#%]= — notably *no hyphens or spaces* (unlike TODO keywords, which allow hyphens). So this slugify differs from the keyword one:

- Downcase (tags are case-sensitive in Org; lowercase is the convention).
- Replace each run of characters outside =[[:alnum:]_]= with a single =_=.
- Trim leading/trailing =_=.

Examples: =Bug= → =bug=, =Needs Review= → =needs_review=, =P1= → =p1=, =backend/api= → =backend_api=, =UI/UX= → =ui_ux=. A label that slugifies to empty is dropped.

(Contrast with =pearl--state-name-to-keyword= from the workflow-states spec, which upcases and uses hyphens. Two different targets, two different slugify rules — keep them as separate, clearly-named helpers.)

** Render tags on the issue heading

=pearl--format-issue-as-org-entry= appends the issue's label tags to the heading line in org tag syntax:

: ** TODO [#B] Fix the thing :bug:backend:

Slugify each label name, de-duplicate (preserving order), and join as =:t1:t2:=. No labels → no tag string. The =:LINEAR-LABELS:= drawer stays as the canonical structured store (it holds the exact Linear names, which the tag form lossily slugifies); the heading tags are the derived, org-native view.

Only *issue* headings get tags. The parent view heading and the Comments / individual-comment headings carry none.

** Keep the drawer authoritative; tags are a view

The =:LINEAR-LABELS:= drawer remains the source of truth for the issue's labels (it survives slugify collisions and preserves the display names). =pearl-set-labels= continues to push and rewrite the drawer, and additionally re-renders the heading's tag string so the two stay consistent after a label change.

** v1 is render-only (fetch direction)

Editing the heading's tags by hand does *not* push to Linear in v1. The way to change labels stays =pearl-set-labels= (which now updates both the drawer and the heading tags). Bidirectional sync — parse the heading's tags on save and reconcile them against Linear's label set — is deferred (see out of scope); it needs the same kind of conflict gate the description/title/comment syncs have, and label *creation* semantics (a tag with no matching Linear label) to be decided.

* Files touched

- =pearl.el=: new =pearl--label-name-to-tag=; =pearl--format-issue-as-org-entry= (append tag string to the heading); =pearl-set-labels= (re-render heading tags after a label change). The =:LINEAR-LABELS:= drawer line is unchanged.
- Tests: new cases for =pearl--label-name-to-tag= (normal, multiword, punctuation, collision, empty); a render test (issue with labels → heading carries the slugified tags); a regression test that =pearl--issue-title-at-point= / title sync still returns the bare title with tags present; a =pearl-set-labels= test that the heading tag string updates.
- =README.org=: note in the Fields / "active org file" section that labels render as heading tags; mention the drawer remains the structured store.

* Test plan

- *Tag slugify*: =Bug=→=bug=, =Needs Review=→=needs_review=, =UI/UX=→=ui_ux=, =P1=→=p1=, collision de-dup, empty-after-slug dropped.
- *Render*: an issue with labels =("Bug" "Backend")= renders =** … :bug:backend:= and still carries =:LINEAR-LABELS: [Bug, Backend]= in the drawer.
- *Title-sync regression*: with tags on the heading, =pearl--issue-title-at-point= returns the bare title (no tags, no keyword), so a no-op title sync still matches and a title edit still round-trips.
- *set-labels*: after changing labels, the heading tag string reflects the new set (and the drawer too).

* Migration

Additive — no breaking change. Existing files gain heading tags on the next fetch. The removed =#+filetags= is already gone (952cfe7). No defcustom changes.

* Open questions for review

1. *Tag case.* Lowercase (=Bug= → =bug=) for org-tag convention, or preserve Linear's case (=Bug= → =Bug=)? Lowercase is my lean; it matches how most org users tag and avoids =Bug= vs =bug= duplication.
2. *=#+TAGS:= declaration.* Should the file declare =#+TAGS:= with the union of label slugs (for tag-completion in the buffer), the way the workflow-states spec derives =#+TODO:=? Bonus, not required for the feature. My lean: defer to a follow-up to keep v1 focused.
3. *Tag inheritance.* Org tag inheritance is on by default, so an issue's tags apply to its Comments subtree in agenda/inheritance contexts. Harmless (a comment "inheriting" =:bug:= rarely matters), but flagging. Disable inheritance for these files, or accept it? My lean: accept it.
4. *Bidirectional sync* (edit heading tags → push labels) — confirm it's out of scope for v1.

* vNext / out of scope

- Bidirectional tag editing (heading tags → Linear labels) with a conflict gate and label-creation semantics.
- =#+TAGS:= completion declaration.
- Color/face mapping from Linear label colors to org tag faces.