git-ents.gitmain
⌘K
foforge
commit 3ef4302
spec: 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

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

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. +--