diff --git a/specs/coauthoring-inner-loop.md b/specs/coauthoring-inner-loop.md new file mode 100644 index 0000000..47c3c8f --- /dev/null +++ b/specs/coauthoring-inner-loop.md @@ -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; +F3–F5 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 F3–F5 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/.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": , "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 Per–Product-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 F3–F5 | — | 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/.json` artifact. +- Epic #1; F2 (#4); POC #2 (session 0002); rfc-app#48/#46 (lineage).