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