add spec ./specs/coauthoring-inner-loop.md (status: graduated)
This commit is contained in:
@@ -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/<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 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/<path>.json` artifact.
|
||||||
|
- Epic #1; F2 (#4); POC #2 (session 0002); rfc-app#48/#46 (lineage).
|
||||||
Reference in New Issue
Block a user