Files
vscode-cowriting-plugin-con…/specs/coauthoring-inner-loop.md
T

330 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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).