spec: specify conversations and the editor lens
commit
3ef4302spec: specify conversations and the editor lens
Comments become the universal conversational primitive: a comment is about something — an anchor, a context entity, or a parent comment — and issues, reviews, and editor lenses are aggregation views over the same decomposed refs.
spec: broaden model.comment to anchor-or-context-or-parent spec: add model.comment-state, model.comment-context, model.comment-thread spec: add model.review and the refs/meta/reviews/* namespace spec: add anchor.working-tree capture source and projection target spec: add lens.adoc, the LSP editor surface, and the ents-lens crate row Assisted-by: Claude:claude-fable-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
docs/spec/anchor.adoc
@@ -72,3 +72,22 @@
An outdated or deleted projection MUST NOT lose the anchor: the original
anchor remains displayable.
--
+
+[role="requirement", id="anchor.working-tree"]
+.The Working Tree Is a Source and a Target
+--
+Capturing an anchor MUST support the working tree as its source: the
+file's current on-disk bytes are written to the object database as a
+blob and embedded per <<anchor.retention>>, so an anchor to uncommitted
+content survives that content being committed, amended, or discarded.
+Such an anchor MUST record `HEAD`'s commit in its commit field — the
+same best-effort, never-load-bearing data field <<anchor.immutable>>
+already makes it.
+Projection MUST support the working tree as its target, diffing the
+anchored blob against the path's current on-disk bytes — or against a
+caller-supplied buffer standing in for them (<<lens.working-tree>>) —
+and reporting the same four outcomes as <<anchor.projection>>.
+A working-tree projection MAY degrade rename following to the context
+fallback (<<anchor.fuzzy-fallback>>): there is no commit on the target
+side to diff trees against.
+--
docs/spec/meta-ref.adoc
@@ -28,8 +28,8 @@
--
A meta-ref MUST hold exactly one independently-authored entity:
`refs/meta/member/*`, `refs/meta/issues/*`, `refs/meta/comments/*`,
-`refs/meta/effects/*`, and `refs/meta/results/*` each decompose one ref
-per entity.
+`refs/meta/reviews/*`, `refs/meta/effects/*`, and `refs/meta/results/*`
+each decompose one ref per entity.
Repository-global state with a single writer-of-record MUST instead live
on one fixed ref, such as `refs/meta/account` or `refs/meta/config`.
Entities that different actors write concurrently MUST NOT share a ref.
docs/spec/model.adoc
@@ -86,10 +86,14 @@
=== Comment
[role="requirement", id="model.comment"]
-.Comment Carries an Anchor
+.Comment Is About Something
--
-A Comment entity MUST carry a body and an anchor identifying the exact
-content it was written against (<<anchor.definition>>).
+A Comment entity MUST carry a body and MUST identify what it is about:
+an anchor into content (<<anchor.definition>>), a context entity
+(<<model.comment-context>>), a parent comment (<<model.comment-thread>>),
+or any combination — a comment about nothing MUST be refused at creation
+by the writing tool, though never by the gate, which stays
+content-agnostic (<<model.extensibility>>).
A comment's identity MUST be derived from a hash of its genesis content
and MUST NEVER change afterward — edits advance the ref, they do not
rename it.
@@ -99,6 +103,44 @@
specified in `anchor.adoc` and apply to a Comment's anchor unchanged.
--
+[role="requirement", id="model.comment-state"]
+.Comment State
+--
+A Comment entity MUST carry a state; a new comment's state MUST be
+`open`.
+Resolving a comment MUST record state `resolved` as an ordinary mutation
+commit on the comment's own ref — never a deletion, so the conversation
+stays auditable — and reopening MUST be supported the same way.
+Custom states beyond `open` and `resolved` are schema, not platform
+features, exactly as for issues (<<model.issue>>).
+Who changed a state, and when, MUST come from the mutation commit chain
+(<<meta-ref.trailers>>), never from stored fields.
+--
+
+[role="requirement", id="model.comment-context"]
+.Context Aggregates, Never Contains
+--
+A Comment MAY name a context: the canonical ref path below `refs/meta/`
+of the entity it belongs to, such as `issues/<id>` or `reviews/<id>`.
+An entity's thread MUST be an aggregation query over comment refs
+matching that context; a context entity MUST NOT store a list of its
+comments (<<meta-ref.granularity>> — decomposed refs, aggregated views),
+so two people commenting on the same issue concurrently never race a
+shared ref.
+--
+
+[role="requirement", id="model.comment-thread"]
+.Replies Form Threads
+--
+A Comment MAY name a parent comment by id, making it a reply; the parent
+MUST exist when the reply is created.
+A reply MUST NOT be required to repeat its parent's anchor or context:
+its aboutness is inherited from its thread root for display and
+projection.
+A thread MUST be an aggregation query over comment refs naming ancestors
+in the thread; no comment stores a list of its replies.
+--
+
=== Issue
[role="requirement", id="model.issue"]
@@ -114,6 +156,27 @@
typed-tree change, not a platform request (<<model.extensibility>>).
--
+=== Review
+
+[role="requirement", id="model.review"]
+.Review Is a Verdict Plus a Context
+--
+A Review entity MUST be a typed tree under its own ref in
+`refs/meta/reviews/*`, one ref per review (<<meta-ref.granularity>>),
+written, gated, synced, and audited exactly like a comment.
+A Review MUST carry the reviewed commit's id as a plain data field —
+best-effort reachability, the same stance <<anchor.immutable>> takes for
+an anchor's commit — a verdict, and a body; `approve` and
+`request-changes` are conventions, not an enum, because custom verdicts
+are schema, not platform features (<<model.extensibility>>).
+A review's discussion MUST be Comment entities naming the review as
+their context (<<model.comment-context>>), anchored into the reviewed
+code where they concern specific lines (<<anchor.definition>>); the
+review itself MUST NOT store a list of its comments.
+Reviewer and timestamp MUST come from the mutation commit chain
+(<<meta-ref.trailers>>).
+--
+
=== Effect
[role="requirement", id="model.effect-definition"]
docs/spec/overview.adoc
@@ -157,10 +157,15 @@
| `ents-receive`, `ents-model`, `ents-anchor`, `ents-query`, `ents-forge`,
`ents-kiln`
+| `ents-lens`
+| The editor surface: comments projected over open buffers as a language
+server.
+| `ents-receive`, `ents-model`, `ents-anchor`, `ents-forge`
+
| `git-ents` (bin)
| Local composition root.
-| `ents-receive`, `ents-effect`, `ents-web`, `ents-anchor`, `ents-sync`,
-`gix-ref-store`
+| `ents-receive`, `ents-effect`, `ents-web`, `ents-lens`, `ents-anchor`,
+`ents-sync`, `gix-ref-store`
| `git-ents-server` (bin)
| Hosted composition root.
docs/spec/lens.adoc
@@ -1,0 +1,101 @@
+== The Lens
+
+The editor is the third place a conversation surfaces, after the CLI and
+the web UI, and it earns no third mechanism: the lens is a language server
+that projects the same anchored comments (<<model.comment>>,
+<<anchor.projection>>) into whatever buffer the user is reading, and
+writes new ones through the same signed mutation path every other
+frontend uses.
+"Lens" is meant literally: it is a read-time view over `refs/meta/*`,
+owning no state of its own, so a comment left in an editor, on the web,
+or by an agent at the CLI is one and the same entity everywhere else.
+
+[role="requirement", id="lens.serve"]
+.The Lens Is a Frontend of the Local Root
+--
+`git ents lsp` MUST serve the Language Server Protocol over stdio,
+reusing the local composition root's wiring exactly as `git ents serve`
+does (<<roots.local>>), adding only the LSP frontend.
+It MUST NOT bind a network socket and MUST NOT add any git-serving
+transport.
+The lens crate MUST receive its signing identity by injection from the
+composition root, mirroring <<roots.web-agnostic>>: nothing in the crate
+may assume which editor, if any, is attached.
+--
+
+[role="requirement", id="lens.lenses"]
+.Comments Surface as Code Lenses
+--
+Every comment whose anchor projects onto an open document — including
+replies displayed through their thread root (<<model.comment-thread>>) —
+MUST surface as a code lens at its projected range, derived at request
+time via <<anchor.projection>> and never cached across mutations of the
+comment's ref.
+A lens MUST identify the comment, summarize its body, and offer the
+thread's operations — view, reply, resolve — as LSP commands that call
+the same library operations the CLI exposes (<<lens.parity>>).
+Comments whose state is not `open` (<<model.comment-state>>) MUST be
+omitted unless the client asks for them.
+--
+
+[role="requirement", id="lens.diagnostics"]
+.Diagnostics Mirror the Lenses
+--
+The same projected comments MUST also be published as hint-severity
+diagnostics, so clients that do not render code lenses still show the
+conversation inline.
+These diagnostics carry conversation, not judgment: they MUST NEVER use
+warning or error severity, and they MUST be suppressible by client
+configuration without affecting the lenses.
+--
+
+[role="requirement", id="lens.hover"]
+.Hover Shows the Thread
+--
+Hovering a projected comment's range MUST return the full thread —
+bodies, states, and authorship read from each ref's mutation commit
+chain (<<meta-ref.trailers>>) — rendered as markup, so the complete
+conversation is readable without leaving the buffer.
+--
+
+[role="requirement", id="lens.compose"]
+.Composing Uses the Editor-File Flow
+--
+Creating a comment from the editor MUST work through a file, the way git
+itself takes a commit message: a code action on the selection writes a
+template file under `.git/` naming the anchored path and lines, opens it
+in the editor via `window/showDocument`, and creating the comment happens
+when the user saves a non-empty body — lines starting with `#` ignored,
+an empty body aborting.
+This flow MUST require no client-specific extension: any LSP client that
+can execute a code action and open a file can compose a comment.
+A richer input surface (a popup, a webview) MAY be layered on by clients
+that have one, but it MUST be sugar over the same create operation, never
+a second mechanism.
+--
+
+[role="requirement", id="lens.working-tree"]
+.The Lens Reads and Writes the Working Tree
+--
+Projection targets for open documents MUST be the working tree
+(<<anchor.working-tree>>), with the client's in-memory buffer content
+standing in for on-disk bytes when they differ, re-projected on document
+change so ranges track unsaved edits.
+Comments composed from the editor MUST anchor to the working tree's
+content when it differs from `HEAD`, so a remark about uncommitted code
+anchors to exactly the bytes the author was reading.
+--
+
+[role="requirement", id="lens.parity"]
+.One Mechanism for Editors, Agents, and the Web
+--
+Every operation the lens offers — list projected onto the working tree,
+create, reply, resolve — MUST be the same library call the `git ents
+comment` porcelain exposes, so an agent addressing comments through the
+CLI, a human leaving them through an editor, and the web UI rendering
+them are three callers of one mechanism.
+The CLI listing MUST offer a machine-readable form covering id, state,
+projected location, and body, sufficient for an agent to enumerate and
+resolve every open comment in the working tree without any editor
+attached.
+--