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