add spec ./specs/coauthoring-inner-loop.md (status: graduated)

This commit is contained in:
Ben Stull
2026-06-10 01:19:00 -07:00
parent a77704e775
commit 976fb48f6a
+329
View File
@@ -0,0 +1,329 @@
---
status: graduated
---
# Solution Design: Coauthoring Inner-Loop Git-Native Model (region-anchored threads)
| | |
| --- | --- |
| **Author(s)** | Ben Stull (with Claude) |
| **Reviewers / approvers** | Ben Stull |
| **Status** | `draft` |
| **Version** | v0.1.0 |
| **Source artifacts** | Epic `benstull/vscode-cowriting-plugin#1` · First consumer: Feature `#4` (F2) · POC: `#2` (shipped, session 0002) · Lineage: `ben.stull/rfc-app#48` (inner-loop rung), `#46` (Gitea substrate rung) — not re-read this session (cross-host) |
**Change log**
| Date | Version | Change | By |
| --- | --- | --- | --- |
| 2026-06-10 | v0.1.0 | Initial draft — brainstorming session 0003 | Ben Stull + Claude |
---
## 1. Business Context
### 1.1 Executive Summary
The VS Code coauthoring plugin (Epic #1) needs a **shared, git-native data model**
underneath every coauthoring capability: provenance, region anchors, threads,
attributed spans, and proposed diffs. This spec designs that model and fully
specifies its **first consumer — region-anchored discussion threads (F2)**
while defining the shared primitives (anchors, provenance envelope) so that F3
(attribution), F4 (propose/accept), and F5 (cross-rung persistence) land without
a format change. It is intentionally focused: threads are designed in depth;
F3F5 are named, accommodated extension points, not built here.
### 1.2 Background
The forge (Gitea / rfc-app) is the *outer* loop — review, deliberation, durable
record. The *inner* loop, where writing happens keystroke by keystroke, has no
home. Epic #1 delivers it as a standalone VS Code extension on `@cline/sdk`
(Approach A, validated by the POC #2). Epic #1 explicitly earmarked the full
Solution Design — the git-native format, anchoring, attribution, where Claude
runs — for this follow-on brainstorming session.
### 1.3 Business Actors / Roles
- **Coauthor (human)** — a writer/engineer authoring prose or specs in VS Code.
- **Coauthor (machine)** — Claude, via `@cline/sdk`. *Not an active participant in
F2 threads* (see §1.7); the model reserves its authorship for F3/F4.
### 1.4 Problem Statement
There is nowhere to *discuss a specific region* of a document durably. Highlighted
context evaporates on the next keystroke or reload; nothing anchors discussion to
a span that survives editing, and nothing persists without a server.
### 1.5 Pain Points
- No region-scoped, persistent discussion in the editor.
- No anchor that survives edits, reload, or an external change (a `git pull`).
- No git-native (serverless, durable, portable) home for coauthoring metadata.
### 1.6 Targeted Business Outcomes
A durable, region-anchored thread surface that reloads across sessions, survives
edits, and is stored as a diffable git artifact — and a model skeleton that
carries F3F5 forward without rework.
### 1.7 Scope (business)
**In scope:** the shared git-native model envelope (anchors + provenance);
region-anchored threads (create, reply, resolve) via the native Comments API;
hybrid anchoring; per-document sidecar persistence with reload.
**Out of scope (deferred, not forgotten):** F3 live attribution; F4 propose/accept
diff flow; F5 cross-rung round-trip to the Gitea substrate (#46); **live
`@cline/sdk` agent turns + provider/auth** (the POC deliberately used only pure,
key-free SDK calls); multi-file orchestration; file rename/move tracking; any
server.
### 1.8 Assumptions · Constraints · Dependencies
- Built on the POC skeleton (#2): CJS extension, esbuild, `@cline/sdk` loaded via
runtime dynamic `import()`. **Threads need no SDK turn** → no provider/auth in F2.
- VS Code **Comments API** (`vscode.comments`) supplies the thread UI natively.
- **Depends on:** Epic #1; first consumer F2 (#4). **Relates to:** F5, rfc-app#46.
### 1.9 Business Use Cases
- **BUC-1** A coauthor attaches a discussion to a region of a document and returns
to it later — across edits and across sessions — and it is still on the right
span (or clearly flagged if it could not be re-anchored).
---
## 2. Solution Proposal
Build software: a VS Code extension feature on the POC skeleton. Persist a
**versioned coauthoring artifact per source document** as a git-committed sidecar
(`.threads/<repo-relative-path>.json`). Render threads through the **native
Comments API**. Maintain anchors with a **hybrid strategy** — exact live offset
tracking while a document is open, plus a durable **content fingerprint** as the
source of truth for re-resolution on load/external change, with an explicit
**orphaned** state when a confident match is not found. The artifact's `anchors`
and `provenance` primitives are shared: F3/F4/F5 reuse them.
---
## 3. Product Personas
- **PP-1 Inner-loop coauthor** — the human writer/engineer (embodies the Coauthor
(human) business role). The only active F2 persona.
- **PP-2 Machine coauthor (Claude)** — reserved in the model (an `agent`
provenance author) but **not active in F2**; embodies Coauthor (machine).
## 4. Product Use Cases
- **PUC-1** Create a thread on the selected text range (Comments gutter).
- **PUC-2** Reply within a thread; mark a thread resolved/reopened.
- **PUC-3** Reopen the workspace → threads reload at their anchors.
- **PUC-4** Edit the document (or pull an external change) → threads re-anchor;
un-resolvable threads are shown as orphaned, never silently moved.
## 5. UX Layout
No custom UI. Threads render in the **native VS Code Comments** gutter/peek. A
contributed command/gutter action creates a thread on the current selection.
Orphaned threads surface with a distinct label (and, where the API allows, a
"detached" context value) at their last-known `lineHint`. UX is described in
text; no bespoke webview to wireframe.
---
## 6. Technical Design
### 6.1 Invariants
- **INV-1** A thread is **never silently moved** to a wrong span. If its anchor
cannot be confidently re-resolved, it becomes **orphaned** (visible, flagged).
- **INV-2** Persistence is **git-native and serverless**: plain, diffable,
human-resolvable JSON committed in the repo. No server, no external store.
- **INV-3** The **fingerprint is the source of truth** for an anchor's location;
live editor offsets are a within-session optimization, not the persisted truth.
- **INV-4** The artifact is **forward-compatible**: `schemaVersion` is present;
threads, attributions (F3), and proposals (F4) all reference the **same**
`anchors` primitive, so later features add arrays without a format break.
- **INV-5** F2 ships **no live `@cline/sdk` turn** and requires **no credentials**.
### 6.2 High-level architecture
Three vscode-free units (unit-testable, mirroring the POC's vscode-free
`cline.ts`) and one thin editor-facing controller:
- **Artifact schema** — the versioned envelope (§6.3).
- **CoauthorStore** — load/save sidecars; `FileSystemWatcher` for external change.
- **Anchorer** — fingerprint build, hybrid resolve/refresh, orphan detection.
- **ThreadController** — wires Store + Anchorer to `vscode.comments`
(create-on-selection, reply, resolve, render, orphaned state).
```
selection ─▶ ThreadController ─▶ Anchorer (build fingerprint) ─▶ CoauthorStore (persist)
document change ─▶ Anchorer (shift live ranges) ─▶ on save: refresh fingerprint
load / external change ─▶ CoauthorStore (read) ─▶ Anchorer (resolve|orphan) ─▶ ThreadController (render)
```
### 6.3 Data model & ownership
One sidecar per document, owned by the extension, committed to git:
```jsonc
// .threads/docs/spec.md.json
{
"schemaVersion": 1,
"document": { "path": "docs/spec.md" }, // repo-relative; the key
"anchors": { // SHARED primitive (INV-4)
"a1": { "fingerprint": {
"text": "the exact anchored text",
"before": "<= ~3 lines / 120 chars leading context",
"after": "<= ~3 lines / 120 chars trailing context",
"lineHint": 42 // tie-breaker, not truth (INV-3)
} }
},
"threads": [
{ "id": "t1", "anchorId": "a1", "status": "open", // open | resolved
"messages": [
{ "id": "m1", "author": <Provenance>, "body": "markdown", "createdAt": "ISO-8601" }
] }
],
"attributions": [], // F3 extension point — reuses anchors[]; not implemented in F2
"proposals": [] // F4 extension point — reuses anchors[] + provenance; not in F2
}
```
**Provenance primitive** (the reusable author field):
```jsonc
{ "kind": "human" | "agent",
"id": "ben", // human: git user; agent: model/agent id
"agent": { "sdk": "@cline/sdk", "model": "…", "sessionId": "…" } // only when kind = agent
}
```
- **Ownership:** the extension is the sole writer of `.threads/`. JSON is
pretty-printed with **stable key ordering** to minimize merge conflicts.
- **Ids:** stable, generated at create time (e.g. `crypto.randomUUID()` /
timestamp+counter); `Date.now()` is available in the extension runtime.
### 6.4 Interfaces & contracts
- **CoauthorStore**: `load(docPath): Artifact | null` · `save(docPath, Artifact)`
· emits `onExternalChange(docPath)` (sidecar changed on disk).
- **Anchorer**: `buildFingerprint(docText, range): Fingerprint` ·
`resolve(docText, Fingerprint): Range | "orphaned"` ·
`shift(ranges, change): ranges` (live offset maintenance).
- **ThreadController**: `createThreadOnSelection()` · `reply(threadId, body)` ·
`setStatus(threadId, status)` · `renderAll(docPath)` — all via `vscode.comments`.
### 6.5 PerProduct-Use-Case design
- **PUC-1 (create):** capture selection → `buildFingerprint` → store anchor +
thread → render. Lazy-create the sidecar on the first thread for a document.
- **PUC-2 (reply/resolve):** append a `human` message / flip `status`; persist.
- **PUC-3 (reload):** on open, `load` → for each anchor `resolve` against current
text → render at resolved range or as orphaned.
- **PUC-4 (re-anchor):** while open, `onDidChangeTextDocument` shifts live ranges
and (on save) refreshes fingerprints; on external change (FileSystemWatcher on
the doc or `.threads/`) re-run `resolve`. Resolution ladder:
**exact-unique → context-disambiguated (lineHint breaks ties) → orphaned.**
### 6.6 Non-functional requirements & cross-cutting concerns
Resolution is O(document) text search per anchor on load/change — fine for
human-scale documents; skip non-text/oversized files. No secrets (INV-5). Sidecar
diffs must stay readable (stable formatting). Concurrency: single editor writer.
### 6.7 Key decisions & alternatives considered
| Decision | Chosen | Alternatives rejected |
| --- | --- | --- |
| Persistence substrate | `.threads/` committed sidecar, per document | git notes (bind to committed objects, weak tooling, no default push); commit trailers (only fit F4 diffs) |
| Anchoring | Hybrid: live offsets + durable fingerprint + orphan fallback | offsets-only (stale on external edit/reload); fingerprint-only (no exact in-session tracking) |
| Model scope | Threads-deep + model-wide skeleton (shared anchors/provenance) | threads-only (reworked at F3/F4); full inner-loop now (large, speculative) |
| Agent in F2 | Human-only; `agent` author reserved | live `@cline/sdk` turn in F2 (pulls provider/auth + "where Claude runs" into F2) |
### 6.8 Testing strategy
- **Unit (vitest, vscode-free):** Anchorer — fingerprint build; resolution
(exact / context-disambiguated / orphaned); live offset shifting. CoauthorStore
— serialize → reload round-trip; stable formatting.
- **Integration/E2E (`@vscode/test-electron`):** create-on-selection → reply →
persist → reload → re-anchor in the Extension Development Host. Where the harness
cannot drive native Comments UI directly, fall back to driving the
`ThreadController` API and asserting Comments-controller state. (No Playwright —
Comments API is native, not a webview.) E2E are first-class plan tasks.
### 6.9 Failure modes, rollback & flags
Orphaned threads (INV-1) are the primary failure mode — handled, recoverable
(auto-re-anchor if the text reappears). Sidecar merge conflicts → plain JSON,
human-resolvable. No feature flag needed (additive, off by absence of `.threads/`).
---
## 7. Delivery Plan
### 7.1 Approach / strategy
Land the shared envelope and the threads consumer together (F2 = #4), as a
just-in-time plan in a follow-on **planning-and-executing** session. This spec is
the reviewed input to that plan.
### 7.2 Slicing plan
- **SLICE-1** Artifact schema + CoauthorStore (load/save, stable formatting,
round-trip tests).
- **SLICE-2** Anchorer (fingerprint build + resolution ladder + orphan; live
offset shift) with unit tests.
- **SLICE-3** ThreadController wiring to `vscode.comments` (create/reply/resolve,
render).
- **SLICE-4** Reload + external-change re-anchoring (FileSystemWatcher) and the
orphaned-thread surface.
- **SLICE-5** E2E (`@vscode/test-electron`) across create→persist→reload→re-anchor.
UI surface ⇒ E2E browser/host tests are first-class plan tasks (handbook §4 /
solution-design §6.8), not a follow-up.
### 7.3 Rollout / launch plan
Non-shippable still (no marketplace publish). "Done" = F2 acceptance met, unit +
host E2E green. No deploy stage (a VS Code extension, not a webapp — §9 has no
applicable stage).
### 7.4 Risks & mitigations
| Risk | Mitigation |
| --- | --- |
| Fingerprint mis-resolves on duplicated/heavily-edited text | context + lineHint disambiguation; orphan rather than guess (INV-1) |
| Native Comments API hard to E2E | fall back to ThreadController API assertions |
| Format churn when F3/F4 land | shared `anchors`/`provenance` + `schemaVersion` (INV-4) |
---
## 8. Traceability matrix
| Requirement | Use case | Design | Slice |
| --- | --- | --- | --- |
| Region-anchored threads | BUC-1, PUC-1/2 | §6.3, §6.5 | SLICE-1/3 |
| Survives edits / reload / external change | PUC-3/4 | §6.5 (hybrid), INV-1/3 | SLICE-2/4 |
| Git-native persistence | BUC-1 | §6.3, INV-2 | SLICE-1 |
| Forward-compat for F3F5 | — | INV-4, §6.3 | SLICE-1 |
## 9. Open Questions & Decisions log
- **RESOLVED (this session):** substrate = `.threads/` sidecar; anchoring = hybrid;
model scope = threads-deep + skeleton; agent participation = none in F2.
- **OPEN → F5:** the cross-rung persistence format for round-trip to the Gitea
substrate (#46) may extend or re-home the sidecar; F2's format is the inner-loop
shape, not yet the cross-rung contract.
- **OPEN → F3/F4:** exact shapes of `attributions[]` and `proposals[]` (reserved
as extension points here).
- **OPEN → later:** file rename/move tracking (path is the key in F2).
- **DEFERRED:** where Claude runs + provider/auth (arrives with the first live
`@cline/sdk` turn, F3/F4).
## 10. Glossary & References
- **Anchor** — a fingerprinted reference to a document span, re-resolvable across
edits. **Fingerprint** — anchored text + bounded context + lineHint.
**Orphaned** — a thread whose anchor could not be confidently re-resolved.
**Sidecar** — the per-document `.threads/<path>.json` artifact.
- Epic #1; F2 (#4); POC #2 (session 0002); rfc-app#48/#46 (lineage).