aboutsummaryrefslogtreecommitdiff
path: root/docs/issue-comment-editing-spec.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/issue-comment-editing-spec.org')
-rw-r--r--docs/issue-comment-editing-spec.org117
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.