diff --git a/specs/coauthoring-attribution.md b/specs/coauthoring-attribution.md new file mode 100644 index 0000000..ed5f2ec --- /dev/null +++ b/specs/coauthoring-attribution.md @@ -0,0 +1,397 @@ +--- +status: graduated +--- +# Solution Design: Live Human/Claude Attribution in the Buffer (F3) + +| | | +| --- | --- | +| **Author(s)** | Ben Stull (with Claude) | +| **Reviewers / approvers** | Ben Stull | +| **Status** | `draft` | +| **Version** | v0.1.0 | +| **Source artifacts** | Epic `benstull/vscode-cowriting-plugin#1` · Feature `#6` (F3) · Parent spec: `vscode-cowriting-plugin-content/specs/coauthoring-inner-loop.md` (graduated; §6.3 `attributions[]`/`provenance`, §9 OPEN→F3/F4) · F2 shipped: `#4` (PR #5, session 0004) · Lineage: `ben.stull/rfc-app#48` | + +**Change log** + +| Date | Version | Change | By | +| --- | --- | --- | --- | +| 2026-06-10 | v0.1.0 | Initial draft — brainstorming session 0006 | Ben Stull + Claude | + +--- + +## 1. Business Context + +### 1.1 Executive Summary + +F3 makes the buffer an **honest record of authorship**: as a document is +coauthored, the editor shows — live — which spans were written by the **human** +and which by **Claude**, and persists that record git-natively in the same +sidecar F2 shipped, filling the reserved `attributions[]` extension point on the +shared `anchors`/`Provenance` primitives. It also lands the project's **first +live `@cline/sdk` turn** — the minimal machine-edit ingress that gives +attribution something real to attribute — behind a programmatic apply-edit +**seam** that F4's propose/accept will later drive. + +### 1.2 Background + +F2 (#4) gave regions durable *discussion*; nothing yet records who *wrote* what. +Epic #1's second acceptance pillar is "see, live in the buffer, which spans are +human vs. Claude." The parent spec deliberately reserved `attributions: []` +(INV-4) and deferred "where Claude runs + provider/auth" to "the first live +`@cline/sdk` turn, F3/F4" — i.e., to exactly this design. Issue #6 routed the +open fork (what counts as a machine edit before F4 exists; the `attributions[]` +shape; mixed-edit granularity) to this brainstorming session. + +### 1.3 Business Actors / Roles + +- **Coauthor (human)** — the writer/engineer authoring prose/specs in VS Code. +- **Coauthor (machine)** — Claude via `@cline/sdk`; **active for the first time** + (F2 reserved the `agent` provenance author; F3 uses it). + +### 1.4 Problem Statement + +Once a machine edit lands in the buffer it is indistinguishable from human +typing. There is no basis for trust ("did I write this claim or did it?"), no +provenance to carry up the ladder, and no substrate for F4's propose/accept to +render against. + +### 1.5 Pain Points + +- No visible, live distinction between human-authored and Claude-authored spans. +- No durable authorship record: attribution (if any) dies on reload. +- No machine-edit pathway at all yet — F2 is human-only by design. + +### 1.6 Targeted Business Outcomes + +Human and Claude spans visibly distinguished as edits happen; the attribution +survives further editing (re-anchor) and reload (sidecar persistence); a real +Claude edit can be invoked from the editor; F4 inherits both the attribution +substrate and a clean apply-edit seam. + +### 1.7 Scope (business) + +**In scope:** live in-buffer rendering of human/Claude span attribution; +capturing human keystrokes and applied machine edits as attributed anchored +spans; the `attributions[]` shape; character-precise split/merge of mixed edits; +attribution survival across edits (re-anchor / orphan); git-native persistence + +reload; the programmatic apply-edit seam; a minimal live `@cline/sdk` turn +(edit-selection command) on the `claude-code` provider. + +**Out of scope (deferred, not forgotten):** F4 propose/accept (keep/reject UX, +inline diff — it will drive this seam); F5 cross-rung format + Gitea round-trip +(#46); attribution of edits made outside the session (external editors, git +merges) beyond the orphan rule; per-character blame depth (the span is the +unit); multi-file orchestration; Anthropic API-key auth (config option later); +chat-panel UX; any server. + +### 1.8 Assumptions · Constraints · Dependencies + +- **Parent:** Epic #1. **Blocked by:** F2 #4 (shipped). **Feeds:** F4 + (propose/accept renders into this substrate and drives the seam); relates to + F5. +- Builds on the graduated parent spec's primitives: shared `anchors` + (INV-4), `Provenance` (`kind: human | agent`), the Anchorer's + resolve/orphan ladder (INV-1/INV-3), the per-document sidecar (INV-2). +- **F2's no-credentials invariant (parent INV-5) ends here by design** — but + with zero key handling: the live turn uses the SDK's built-in **`claude-code` + provider** ("Use Claude Code SDK with Claude Pro/Max subscription"), riding + the operator's existing local Claude Code login. The extension stores no + secret of any kind. +- Requires Claude Code installed + signed in for the live turn; everything else + (tracking, rendering, persistence, tests) works without it. +- Native primitives: `vscode.window.createTextEditorDecorationType` (rendering), + `onDidChangeTextDocument` (edit deltas — the Anchorer already consumes it), + `WorkspaceEdit` (the seam's application path). + +### 1.9 Business Use Cases + +- **BUC-1** A coauthor watches authorship accumulate honestly as they and Claude + write: each author's spans are visibly theirs, live, and the record survives + editing and reopening the workspace. +- **BUC-2** A coauthor selects a region, asks Claude to edit it, and the + resulting machine edit lands visibly Claude-attributed. + +--- + +## 2. Solution Proposal + +Fill the sidecar's reserved `attributions[]` with **anchored, author-attributed +spans** maintained live by a pure span-algebra tracker: human keystrokes produce +human spans; edits applied through a new programmatic **apply-edit seam** +produce agent spans. Mixed edits split character-precisely; adjacent same-author +spans coalesce. Spans persist via the existing Store/Anchorer (fingerprint on +save; resolve-or-orphan on load — never guess). A minimal live turn — an +edit-selection command on the SDK's `claude-code` provider — drives the seam so +there is a real machine author from day one. Rendering is the native Decorations +API: Claude spans tinted, human spans gutter-marked, unattributed text plain. + +--- + +## 3. Product Personas + +- **PP-1 Inner-loop coauthor** — the human writer/engineer (as in F2). +- **PP-2 Machine coauthor (Claude)** — now active: authors edits via the live + turn; its spans carry `agent` provenance. + +## 4. Product Use Cases + +- **PUC-1** Type in a tracked document → the new/changed span renders + human-attributed, live. +- **PUC-2** Select a region, run "Ask Claude to edit selection," give an + instruction → the replacement lands Claude-attributed, live. +- **PUC-3** Edit inside a Claude-attributed span → the span splits + character-precisely; the human's characters are human-attributed; the + remainder stays Claude's. +- **PUC-4** Reopen the workspace → attributed spans reload from the sidecar at + their re-resolved anchors; un-resolvable spans surface as orphaned, never + silently moved. +- **PUC-5** Toggle attribution decorations on/off. + +## 5. UX Layout + +No new UI surface. Attribution renders as **decorations**: a subtle background +tint on Claude-attributed spans, a thin gutter bar on human-attributed spans, +nothing on unattributed text (text predating tracking — absence of a span is +the honest record). Two contributed commands: **"Cowriting: Ask Claude to edit +selection"** (selection + instruction input box → machine edit) and +**"Cowriting: Toggle attribution"**. Orphaned attributions are dropped from +decoration and surfaced as a **status-bar count** (details in the output +channel) — visible, flagged, recoverable, per F2's orphan precedent. + +--- + +## 6. Technical Design + +### 6.1 Invariants + +Parent-spec invariants INV-1..INV-4 carry over unchanged. F3 adds: + +- **INV-6** An attribution span is **never silently moved or re-authored**: its + location is re-resolved by fingerprint (or orphaned), and its author only + changes by the explicit split/clip rules of the span algebra. +- **INV-7** Attribution is **char-honest**: the span algebra attributes exactly + the characters each author produced (split character-precisely; coalesce only + adjacent same-author spans). No threshold heuristics, no whole-span flips. +- **INV-8** The extension **stores no credentials**. The live turn delegates + auth entirely to the local Claude Code installation (`claude-code` provider); + if it is absent or signed out, the live turn fails gracefully with guidance — + tracking, rendering, and persistence are unaffected. +- **INV-9** Machine edits enter the buffer **only through the seam** + (`applyAgentEdit`), so attribution can never mistake an agent edit for typing; + F4 inherits the same guarantee. + +### 6.2 High-level architecture + +Two new vscode-free units (unit-testable, like the POC's `cline.ts` and F2's +model/anchorer) plus thin editor-facing wiring: + +- **`attributions` model** — the typed `attributions[]` shape on the artifact + (replaces the `unknown[]` placeholder; `schemaVersion` stays 1 — additive). +- **AttributionTracker** — pure span algebra over offset ranges: + `applyChange(spans, edit, author) → spans` (shift/split/clip/coalesce). +- **Apply-edit seam** — `applyAgentEdit(doc, range, newText, provenance)`: + applies a `WorkspaceEdit` and registers it (keyed by document version) so the + tracker attributes the resulting change event to the agent. Exposed as an + internal API + a command, so tests drive it with no LLM. +- **LiveTurn** — the edit-selection command: instruction → `@cline/sdk` + (`claude-code` provider, building on the POC's vscode-free `cline.ts` + driver) → replacement text → the seam. +- **AttributionRenderer** — two `TextEditorDecorationType`s + the toggle; + re-renders on tracker change. +- **CoauthorStore / Anchorer — reused unchanged.** + +``` +keystroke ─────────────▶ AttributionTracker (author = human) +applyAgentEdit (seam) ─▶ WorkspaceEdit + pending-edit registry ─▶ AttributionTracker (author = agent) +live turn ─▶ selection + instruction ─▶ @cline/sdk (claude-code) ─▶ seam +document change ───────▶ shift/split/clip/coalesce ─▶ AttributionRenderer (decorations) +save ──────────────────▶ buildFingerprint per span ─▶ sidecar attributions[] (CoauthorStore) +load / external change ▶ resolve per span ─▶ render | orphaned (Anchorer ladder, INV-1) +``` + +### 6.3 Data model & ownership + +Same sidecar, same `anchors` map, same `Provenance` — `attributions[]` is filled, +not reshaped (parent INV-4; no `schemaVersion` bump): + +```jsonc +"attributions": [ + { + "id": "at1", + "anchorId": "a7", // shared anchors map — same Fingerprint primitive + "author": { "kind": "agent", "id": "claude", + "agent": { "sdk": "@cline/sdk", "model": "…", "sessionId": "…" } }, + "createdAt": "ISO-8601", + "updatedAt": "ISO-8601", // bumped when the span's extent/fingerprint changes + "turnId": "turn-…" // optional; groups all spans applied by one live turn + } +] +``` + +- **Orphanhood is not persisted** — exactly like F2 threads, it is a + resolve-time outcome: fingerprint fails → orphaned (visible, flagged), never + guessed. +- **Unattributed text is the absence of a span** — text predating tracking has + no attribution record, honestly. +- Ownership/format rules unchanged: extension is the sole writer; stable key + ordering; ids via `newId()`. + +### 6.4 Interfaces & contracts + +- **AttributionTracker** (pure): `applyChange(spans, edit, author): spans` · + `coalesce(spans): spans` · span = `{ range: OffsetRange, author: Provenance, + id, turnId? }`. Reuses `anchorer.shift` semantics for untouched spans. +- **Seam**: `applyAgentEdit(doc, range, newText, provenance): Promise` + — applies + registers; also contributed as an internal command (E2E entry + point). +- **LiveTurn**: `editSelection(doc, selection, instruction): Promise` — + resolves the `claude-code` provider, runs one SDK turn producing replacement + text for the selection, calls the seam with `agent` provenance (model + + sessionId from the turn). +- **AttributionRenderer**: `render(editor, spans)` · `setVisible(boolean)`. +- **Store/Anchorer**: existing contracts, now also carrying attributions. + +### 6.5 Per–Product-Use-Case design + +- **PUC-1 (type):** `onDidChangeTextDocument` → for each content change, the + tracker shifts spans past the edit, clips/deletes spans overlapping a + deletion, splits a different-author span around an insertion, attributes the + inserted range to **human** (no pending seam registration for this document + version), then coalesces. Render. +- **PUC-2 (live turn):** command guards (selection non-empty, document tracked, + Claude Code available) → input box → SDK turn (replacement text only — + single-range, the selection) → `applyAgentEdit` → the change event arrives + with a matching registration → attributed to **agent** with `turnId`. Render. +- **PUC-3 (mixed edit):** the insertion-inside-a-span case of PUC-1's algebra: + split at the insertion point, new human span between the two agent halves + (INV-7). Same rule symmetric for agent edits inside human spans. +- **PUC-4 (reload / external change):** on open or sidecar/document external + change: `load` → per attribution `resolve(fingerprint)` → live span or + orphaned (counted, not rendered). On save: refresh fingerprints from live + spans (`updatedAt` bumped only for changed spans), persist. +- **PUC-5 (toggle):** flips renderer visibility; tracking continues regardless. + +### 6.6 Non-functional requirements & cross-cutting concerns + +Tracking is O(spans) per change event — fine at human scale; coalescing bounds +span growth. Persistence happens on save (not per keystroke), as F2 does. +Resolution cost on load matches F2 (O(document) per anchor). No secrets at all +(INV-8). Skip non-text/oversized files (F2 rule). Single editor writer. +Live-turn latency is user-visible: show progress notification; the buffer is not +locked (edits during a turn are handled by the version-keyed registration — a +stale turn result fails to apply and reports, rather than corrupting). + +### 6.7 Key decisions & alternatives considered + +| Decision | Chosen | Alternatives rejected | +| --- | --- | --- | +| Machine-edit ingress | Programmatic seam **+** minimal live SDK turn driving it | seam-only (no real machine author; demo is simulated); live-turn-only (couples tracking to the SDK; F4 has no clean ingress to drive) | +| Provider/auth | `claude-code` provider (local Claude Code Pro/Max login; zero key handling) | Anthropic API key in SecretStorage (ignores the plan already paid for; adds key-handling surface — deferred as a config option); Cline-account OAuth (couples UX to Cline's auth service); env var (no user story) | +| Mixed-edit granularity | Character-precise split + same-author coalescing (INV-7) | whole-span re-attribution (dishonest at the edges); threshold-based (arbitrary, not strictly honest) | +| Attribution record | Span-list on shared `anchors` (state, not history) | event log + replay (truthful history but heavy sidecar churn; re-anchoring a log is hard); hybrid (YAGNI) | +| Rendering | Claude tint + human gutter bar + plain unattributed; toggle | Claude-only (human record invisible); both tinted (visually heavy prose) | +| Live-turn UX | Edit-selection command (selection + instruction → replacement) | whole-document multi-range edits (big bite for "minimal ingress"); chat panel (new surface F4 will reshape) | +| Orphan persistence | Resolve-time (not persisted), per F2 threads | persisted status field (duplicates what resolution derives; risks stale state) | + +### 6.8 Testing strategy + +- **Unit (vitest, vscode-free):** AttributionTracker span algebra exhaustively — + insert/delete at/around/inside spans, split, clip, coalesce, multi-edit + sequences, both author directions; model round-trip (serialize → reload, + stable formatting); resolve→orphan for attribution fingerprints. +- **Host E2E (`@vscode/test-electron`, F2 pattern):** type → human span → seam + command edit → agent span → save → reload → spans restored → edit above → + re-anchored → mangle anchored text → orphaned. **No LLM in CI** — E2E drives + the seam. +- **Live-turn smoke (manual, documented):** select → instruct → Claude edit + lands attributed; run on a signed-in Claude Code machine. Failure-path check: + signed-out / missing Claude Code → graceful error (INV-8). + +### 6.9 Failure modes, rollback & flags + +Orphaned attributions (INV-1/INV-6) — handled, recoverable (re-resolve if the +text reappears). Live-turn failures (no Claude Code, signed out, model error, +stale document version) — surfaced as notifications; never partial-applied. +Sidecar merge conflicts — plain JSON, human-resolvable (unchanged). No feature +flag: additive; decorations have a toggle; absence of `attributions[]` entries +is the off state. + +--- + +## 7. Delivery Plan + +### 7.1 Approach / strategy + +One planning-and-executing session (F3 = #6), plan written just-in-time from +this spec, per the F2 precedent. + +### 7.2 Slicing plan + +- **SLICE-1** Typed `attributions[]` model (+ round-trip tests). +- **SLICE-2** AttributionTracker span algebra (+ exhaustive unit tests). +- **SLICE-3** Seam (`applyAgentEdit` + registry) and human-edit wiring + + decoration rendering (tint/gutter/toggle). +- **SLICE-4** Persistence on save; reload / external-change resolve; orphan + surfacing. +- **SLICE-5** Live `claude-code` turn (edit-selection command) + documented + manual smoke + graceful-failure paths. +- **SLICE-6** Host E2E (type → attribute → seam edit → attribute → reload → + re-anchor → orphan). + +E2E are first-class plan tasks (handbook §4 / parent §6.8), not a follow-up. + +### 7.3 Rollout / launch plan + +Still non-shippable (no marketplace publish). "Done" = issue #6 acceptance met: +unit + host E2E green, live smoke performed once on this machine. + +### 7.4 Risks & mitigations + +| Risk | Mitigation | +| --- | --- | +| Span churn from heavy typing (many tiny spans) | coalescing (INV-7's merge half); persistence only on save | +| Misattributing a seam edit to the human (or vice versa) | version-keyed pending-edit registry; seam is the only machine ingress (INV-9); algebra unit-tested both directions | +| `claude-code` provider/SDK surface shifts under us (SDK 0.0.x) | provider use isolated in vscode-free LiveTurn module; seam keeps everything else LLM-free | +| Buffer edited mid-turn | stale-version application fails gracefully, reports, never partial-applies | +| Fingerprint mis-resolution on duplicated text | unchanged F2 ladder: context + lineHint, orphan over guess | + +--- + +## 8. Traceability matrix + +| Requirement (issue #6 acceptance) | Use case | Design | Slice | +| --- | --- | --- | --- | +| Human spans recorded/rendered live | PUC-1 | §6.2, §6.5, INV-7 | SLICE-2/3 | +| Machine edits recorded/rendered as Claude's | PUC-2 | seam + LiveTurn, INV-9 | SLICE-3/5 | +| Attribution survives edits (re-anchor / orphan) | PUC-4 | Anchorer ladder, INV-1/6 | SLICE-4 | +| Git-native persistence + reload | PUC-4 | §6.3, parent INV-2/4 | SLICE-1/4 | +| Mixed-edit split/merge | PUC-3 | INV-7, §6.5 | SLICE-2 | +| Unit + host E2E coverage | — | §6.8 | SLICE-2/6 | + +## 9. Open Questions & Decisions log + +- **RESOLVED (this session):** ingress = seam + minimal live turn; auth = + `claude-code` provider only (no extension-held credentials; Anthropic-key + fallback deferred — not needed for testing since tests drive the seam); + granularity = character-precise split + coalesce; record = span-list on shared + anchors, orphanhood resolve-time; rendering = Claude tint + human gutter + + toggle; live-turn UX = edit-selection command. +- **OPEN → F4:** propose/accept drives the same seam; accepted diffs render into + this attribution substrate. The seam's contract (single-range apply) may grow + multi-range then. +- **OPEN → F5:** cross-rung persistence format may re-home `attributions[]` + alongside the rest of the sidecar. +- **OPEN → later:** attribution of out-of-session edits (git merges, external + editors) beyond the orphan rule; span-level → finer blame; Anthropic API-key + config option for machines without Claude Code. + +## 10. Glossary & References + +- **Attribution span** — an anchored, author-attributed range of a document; the + unit of provenance. **Seam** — `applyAgentEdit`, the single programmatic + ingress for machine edits (INV-9). **Unattributed** — text with no span (it + predates tracking). **Live turn** — one `@cline/sdk` invocation producing a + machine edit. **`claude-code` provider** — the SDK's built-in provider that + rides the local Claude Code Pro/Max login. +- Epic #1 · Feature #6 (F3) · F2 #4 (PR #5) · parent spec + `coauthoring-inner-loop.md` · POC #2 · rfc-app#48/#46 (lineage).