diff options
Diffstat (limited to 'docs/issue-comment-editing-spec.org')
| -rw-r--r-- | docs/issue-comment-editing-spec.org | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/docs/issue-comment-editing-spec.org b/docs/issue-comment-editing-spec.org new file mode 100644 index 0000000..6a1b0c8 --- /dev/null +++ b/docs/issue-comment-editing-spec.org @@ -0,0 +1,117 @@ +#+TITLE: pearl — Comment Editing Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-05-24 +#+STARTUP: showall + +* Status + +*APPROVED — open questions resolved 2026-05-24 (see [[*Resolved decisions][Resolved decisions]]). Implementation in progress.* Nothing in =pearl.el= had changed at the time of writing. + +Companion to [[file:issue-representation-spec.org][issue-representation-spec.org]] (rendering + description/title editing) and [[file:issue-query-spec.org][issue-query-spec.org]] (fetching). This doc covers the one editing path those two parked as vNext: editing an existing comment. It reuses their org→Linear write path, their conflict-gate pattern, and the single active-file model. + +* Problem + +Comments are render-and-add only. You can read the thread and post a new comment, but you can't fix a typo in your own comment without leaving Emacs for the Linear web UI. Linear lets a user edit only their own comments, so the feature has to carry a permission check: a comment authored by someone else (or by a bot or integration) must not be editable from Emacs, and the attempt must fail clearly rather than bounce off the server with an opaque error. + +The representation spec already parked this (its decision 2): "editing existing comments is vNext, and then only comments authored by the current Linear user, matching Linear's permissions." This is that vNext. + +* Current state (what exists today) + +- *Fetch.* =pearl--fetch-issue-async= (=pearl.el:~737=) pulls each comment as =id=, =body=, =createdAt=, =user { id name displayName }=, =botActor { name }=, =externalUser { name }=. The single-issue fetch carries comments; the bulk list omits them. +- *Normalize.* =pearl--normalize-comment= (=l.568=) returns =(:id :body :created-at :author)=. The =:author= is the *display name only* — the user's =id= is fetched but dropped. There is no viewer identity anywhere in the package. +- *Render.* =pearl--format-comment= (=l.1612=) renders =***** <author> — <timestamp>= followed by the body (markdown → org). The comment =id= is not written into the org; nothing per-comment is recoverable after render. +- *Add.* =pearl--create-comment-async= (=l.1949=, =commentCreate=) + =pearl--append-comment-to-issue= (=l.1975=). +- *Conflict pattern to reuse.* Description sync (representation spec) hashes the last-fetched body into =LINEAR-DESC-SHA256=, compares last-fetched / current-org / current-remote, and does no-op / push / refuse-on-both-changed. + +Three things are therefore missing for editing: the *viewer's identity*, per-comment *id + author id + provenance* in the org, and a =commentUpdate= write path with the same conflict gate. + +* Proposed design + +** 1. Viewer identity + +Add an async =viewer { id name }= query with a cached id, mirroring the team/state caches: + +- =pearl--viewer-async (callback)= → normalized =(:id :name)=. +- =pearl--viewer-id= → cached id, fetched once per session. +- Add the viewer cache to =pearl-clear-cache=. + +This is the identity the permission check compares against. + +** 2. Retain the comment author id + +Extend =pearl--normalize-comment= to keep =:author-id= (the =user.id=). Bot and external comments have no editable user, so =:author-id= is nil for them — which the permission check reads as "not editable." + +** 3. Per-comment provenance in the org + +To target a comment for =commentUpdate= and to decide editability, each rendered comment heading needs its id, its author id, and a body hash. A small property drawer under each =*****= comment heading, mirroring the issue drawer: + +#+begin_src org +**** Comments +***** Craig — 2026-05-24T10:00:00.000Z +:PROPERTIES: +:LINEAR-COMMENT-ID: <uuid> +:LINEAR-COMMENT-AUTHOR-ID: <user-uuid or empty> +:LINEAR-COMMENT-SHA256: <hash of the last-fetched body> +:END: +The comment body renders here as org, edited in place. +#+end_src + +=org-tidy= folds the drawer the same way it folds the issue drawer, so the thread still reads cleanly. The =SHA256= is the last-fetched-body provenance for the conflict gate, exactly like =LINEAR-DESC-SHA256=. + +** 4. The edit command + +=pearl-edit-current-comment= (name is an open question), run from anywhere inside a comment's subtree: + +1. Locate the enclosing =*****= comment heading and read its drawer. +2. *Permission gate.* If =LINEAR-COMMENT-AUTHOR-ID= is empty or ≠ the viewer id, =user-error= "You can only edit your own comments" and stop. No network call. +3. Render the comment's current org body to markdown (the description sync's org→md path). +4. *Conflict gate* (mirrors description sync, v1 = detect / refuse / message): + - current org-rendered hash = =LINEAR-COMMENT-SHA256= → unchanged → no-op, no API call. + - changed locally, remote unchanged since fetch → =commentUpdate= push. + - both changed (re-fetch the remote comment body; its hash ≠ the stored last-fetched hash) → refuse, report, suggest refresh. +5. On success, update =LINEAR-COMMENT-SHA256= and re-render the comment body from the returned comment. + +** 5. The write path + +=pearl--update-comment-async (comment-id body callback)= over =commentUpdate(id: $id, input: { body: $body }) { success comment { ... } }=, normalizing the returned comment. (*Exact mutation shape to be live-verified during implementation*, the way =commentCreate= and the issue mutations were verified against the real workspace.) + +** 6. Editability highlighting (own = green, others = grey) + +"Comments by other users must not appear editable." The permission gate in step 4.2 enforces the behavior; this section makes it *visible* so a user sees what's editable before trying. + +Each comment heading is colored by editability when the active file is displayed and after every refresh: + +- the viewer's own comments → =pearl-editable-comment= face (green), +- everyone else's, plus bot and external comments → =pearl-readonly-comment= face (greyed, inherits =shadow=, reads as disabled). + +Two custom faces so users can theme them. Because the active file is generated and written to disk, faces can't be stored in the file — they're applied at *display time*. Mechanism (proposed): an overlay pass that runs during render and re-runs on the refresh / find-file hook, reading each comment's =LINEAR-COMMENT-AUTHOR-ID= drawer and comparing it to the cached viewer id. Overlays are preferred over a font-lock matcher because they don't contend with org's own fontification and the highlighted set is small. The viewer id must be resolved before the highlight pass — fetch it alongside the single-issue fetch that already pulls comments, so it's in hand at render. + +** 7. Refresh interaction + +Refreshing the issue (=refresh-current-issue=) replaces the subtree, so an unpushed comment edit would be lost — the same risk description edits already carry. The existing dirty-buffer guard covers it; no new merge logic in v1. + +* Proposed v1 decisions + +1. Only the viewer's own comments are editable. Others' comments (and bot/external comments) refuse with a =user-error=, no network call. +2. Each rendered comment carries a drawer with its id, author id, and last-fetched body hash. +3. Conflict handling is detect / refuse / message — identical to description sync v1. Interactive merge is vNext. +4. Edit-in-place: edit the comment's org body, then run the command from inside the comment subtree (consistent with how descriptions sync). No separate prompt buffer. +5. Comment *deletion* stays out of scope (read / add / edit only). Deletion is its own vNext item if wanted. +6. Editability is shown by color: own comments green, others greyed (decision from the 2026-05-24 review). +7. The edit command is named =pearl-edit-current-comment= and is added to the transient menu under "Issue at point." + +* vNext (out of scope here) + +- Comment deletion. +- Interactive conflict resolution (diff / local-wins / remote-wins) — shared with the description/title conflict vNext. +- Editing via a dedicated prompt buffer instead of in-place. +- Threaded replies (parent comment id). + +* Resolved decisions + +Settled with Craig, 2026-05-24: + +1. *Per-comment drawer* — yes. Each comment heading carries =LINEAR-COMMENT-ID= / =-AUTHOR-ID= / =-SHA256=, consistent with the issue drawer. +2. *Editability visibility* — refuse is enough for behavior, plus color: others' comments render greyed (disabled-looking), the viewer's own render green (see [[*6. Editability highlighting (own = green, others = grey)][Editability highlighting]]). +3. *Command name* — =pearl-edit-current-comment=. +4. *Transient* — yes, add it under "Issue at point" once implemented. |
