Files
vscode-cowriting-plugin-con…/specs/coauthoring-attribution.md

398 lines
20 KiB
Markdown
Raw Permalink 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: 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<boolean>`
— applies + registers; also contributed as an internal command (E2E entry
point).
- **LiveTurn**: `editSelection(doc, selection, instruction): Promise<void>`
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 PerProduct-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).