add spec ./specs/coauthoring-propose-accept.md (status: graduated)
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
---
|
||||
status: graduated
|
||||
---
|
||||
# Solution Design: Propose/Accept Diff Flow (F4)
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **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 `#12` (F4) · Parent specs: `vscode-cowriting-plugin-content/specs/coauthoring-inner-loop.md` (graduated; §6.3 reserved `proposals[]`, §9 OPEN→F3/F4) and `specs/coauthoring-attribution.md` (graduated; §9 OPEN→F4) · F3 shipped: `#6` (PR #7, session 0007) · F2 shipped: `#4` (PR #5) · Lineage: `ben.stull/rfc-app#48` |
|
||||
|
||||
**Change log**
|
||||
|
||||
| Date | Version | Change | By |
|
||||
| --- | --- | --- | --- |
|
||||
| 2026-06-10 | v0.1.0 | Initial draft — brainstorming session 0011 | Ben Stull + Claude |
|
||||
|
||||
---
|
||||
|
||||
## 1. Business Context
|
||||
|
||||
### 1.1 Executive Summary
|
||||
|
||||
F4 inverts the trust model of machine edits: instead of Claude's edits landing
|
||||
in the document and being reviewed *after the fact* via the attribution tint,
|
||||
they arrive as **proposals** — visible, anchored, diff-rendered — that the human
|
||||
explicitly **accepts or rejects**. Accepting applies the edit through the
|
||||
established `applyAgentEdit` seam, so accepted text lands Claude-attributed with
|
||||
zero new attribution machinery; rejecting leaves the document untouched. Pending
|
||||
proposals are first-class git-native state in the sidecar's reserved
|
||||
`proposals[]` extension point, persisting and re-anchoring exactly like threads
|
||||
and attributions. This delivers Epic #1's third acceptance pillar —
|
||||
*"accept / reject Claude's proposed diffs (propose-by-default)"* — and completes
|
||||
the inner-loop triad: threads (F2) · attribution (F3) · propose/accept (F4).
|
||||
|
||||
### 1.2 Background
|
||||
|
||||
F3 (#6) shipped the seam — `applyAgentEdit`, the single machine-edit ingress
|
||||
(INV-9) — and a minimal live `@cline/sdk` turn (the edit-selection command) that
|
||||
drives it **directly**: the turn completes and the buffer changes in the same
|
||||
gesture. Both parent specs reserved exactly this moment: the inner-loop spec
|
||||
holds `proposals: []` open in the sidecar ("F4 extension point — reuses
|
||||
`anchors[]` + provenance"), and the attribution spec's §9 routes "OPEN → F4:
|
||||
propose/accept drives the same seam; accepted diffs render into this attribution
|
||||
substrate." Issue #12 carried five design forks to this session: the
|
||||
`proposals[]` shape, the rendering primitive, propose-by-default vs. a retained
|
||||
direct mode, multi-range seam growth, and accept/reject granularity. All five
|
||||
are resolved here (§6.7, §9).
|
||||
|
||||
### 1.3 Business Actors / Roles
|
||||
|
||||
- **Coauthor (human)** — the writer/engineer; now also the **ratifier**: the
|
||||
only actor who can move proposed text into the document.
|
||||
- **Coauthor (machine)** — Claude via `@cline/sdk`; from F4 on it **proposes**
|
||||
rather than writes.
|
||||
|
||||
### 1.4 Problem Statement
|
||||
|
||||
The machine writes first and the human objects later. A completed live turn
|
||||
mutates the buffer immediately; the human's only recourse to an unwanted machine
|
||||
edit is undo. There is no ratify-before-landing gesture — no inline diff to
|
||||
inspect, no keep/reject decision point — so the epic's propose-by-default trust
|
||||
model is unmet.
|
||||
|
||||
### 1.5 Pain Points
|
||||
|
||||
- No way to see *what the machine intends to change* before it changes it.
|
||||
- No explicit keep/reject gesture; review is post-hoc (attribution tint + undo).
|
||||
- A machine edit and a human ratification are indistinguishable events — the
|
||||
provenance story stops at "agent wrote this," with no record that a human
|
||||
chose to let it stand.
|
||||
|
||||
### 1.6 Targeted Business Outcomes
|
||||
|
||||
A live Claude turn produces a visible, anchored proposal (current text vs.
|
||||
proposed replacement) and the document is untouched; the human accepts — the
|
||||
edit applies through the seam and lands Claude-attributed — or rejects — the
|
||||
proposal disappears and nothing changed. Pending proposals survive reload and
|
||||
re-anchor across edits. F5 inherits a complete provenance story:
|
||||
proposed → ratified → attributed.
|
||||
|
||||
### 1.7 Scope (business)
|
||||
|
||||
**In scope:** the `proposals[]` shape on the shared `anchors`/`Provenance`
|
||||
primitives; flipping the live turn propose-by-default; in-editor rendering of
|
||||
pending proposals as anchored diffs; the accept/reject UX; apply-on-accept
|
||||
through the unchanged single-range seam; git-native persistence + reload +
|
||||
re-anchor/orphan of pending proposals; a programmatic propose ingress so tests
|
||||
drive the whole flow with no LLM.
|
||||
|
||||
**Out of scope (deferred, not forgotten):** hunk-level / partial acceptance
|
||||
within one proposal (the model makes it additive later — §6.7); F5 cross-rung
|
||||
format + Gitea round-trip (#46); multi-range growth of the seam contract
|
||||
(not needed — §6.7); chat-panel UX; multi-file orchestration; turn-taking
|
||||
choreography; attribution of out-of-session edits; any server.
|
||||
|
||||
### 1.8 Assumptions · Constraints · Dependencies
|
||||
|
||||
- **Parent:** Epic #1. **Blocked by:** F3 #6 (shipped) and F2 #4 (shipped).
|
||||
**Feeds:** F5 (cross-rung provenance story).
|
||||
- Builds on shipped, unchanged machinery: the shared `anchors` map +
|
||||
`Fingerprint` + `Provenance` primitives (parent INV-4), the Anchorer's
|
||||
resolve/orphan ladder (INV-1/INV-3), `CoauthorStore.update` (sole-writer
|
||||
read-modify-write with anchor prune + counted self-writes), the seam
|
||||
(`applyAgentEdit`, INV-9), the F3 attribution substrate, and the F2
|
||||
Comments-API pattern.
|
||||
- The sidecar's `proposals: unknown[]` is already declared and serialized
|
||||
pass-through (`src/model.ts`); `SCHEMA_VERSION` stays **1** — filling a
|
||||
reserved extension point is additive, the F3 precedent.
|
||||
- `CoauthorStore.update`'s anchor prune currently references only thread +
|
||||
attribution anchorIds (noted F4 TODO at `src/store.ts:65`) — F4 extends it.
|
||||
- No new auth surface: the live turn's `claude-code` provider arrangement (F3
|
||||
INV-8) is untouched; accept/reject and all tests are LLM-free.
|
||||
|
||||
### 1.9 Business Use Cases
|
||||
|
||||
- **BUC-1** A coauthor asks Claude to edit a selection and **reviews the
|
||||
proposed diff before anything lands**: accept applies it (attributed),
|
||||
reject discards it, the document never changes without their say-so.
|
||||
- **BUC-2** A coauthor leaves proposals pending — closes the workspace, comes
|
||||
back, edits other parts of the document — and the proposals are still there,
|
||||
re-anchored, still decidable.
|
||||
|
||||
---
|
||||
|
||||
## 2. Solution Proposal
|
||||
|
||||
Fill the sidecar's reserved `proposals[]` with **pending machine edits**: each
|
||||
proposal is one anchored target range + one proposed replacement + agent
|
||||
provenance, born when a live turn (or the programmatic ingress) completes.
|
||||
The document is untouched at propose time (INV-10). A **ProposalController**
|
||||
renders each pending proposal two ways at once: an **amber tint** on the target
|
||||
range (the in-buffer signal) and an anchored **proposal thread** — a second
|
||||
Comments-API controller — whose body is a fenced unified diff (current →
|
||||
proposed) with **Accept** / **Reject** actions. Accept re-resolves the anchor
|
||||
(exact fingerprint text match — the existing ladder doubles as the staleness
|
||||
guard, INV-11), then applies through the unchanged single-range
|
||||
`applyAgentEdit` seam: the F3 substrate attributes the spans with zero new
|
||||
attribution code, and the proposal leaves `proposals[]`. Reject removes the
|
||||
proposal; nothing else happens. A multi-edit turn simply files N single-range
|
||||
proposals sharing a `turnId` — the seam contract does not grow.
|
||||
|
||||
---
|
||||
|
||||
## 3. Product Personas
|
||||
|
||||
- **PP-1 Inner-loop coauthor** — the human writer/engineer (as in F2/F3), now
|
||||
the ratifier of machine edits.
|
||||
- **PP-2 Machine coauthor (Claude)** — proposes edits; its text enters the
|
||||
document only via human acceptance.
|
||||
|
||||
## 4. Product Use Cases
|
||||
|
||||
- **PUC-1 (propose)** Select a region, run "Ask Claude to edit selection," give
|
||||
an instruction → a proposal appears (amber-tinted range + diff thread); the
|
||||
document text is unchanged.
|
||||
- **PUC-2 (accept)** Accept a pending proposal → the replacement applies, the
|
||||
new spans render Claude-attributed (F3), the proposal and its UI disappear.
|
||||
- **PUC-3 (reject)** Reject a pending proposal → the document is untouched, the
|
||||
proposal and its UI disappear.
|
||||
- **PUC-4 (persist / re-anchor / stale)** Pending proposals survive save +
|
||||
reopen at their re-resolved anchors. Editing **around** a proposal shifts it;
|
||||
editing the **target text itself** makes it stale — flagged, not acceptable,
|
||||
recoverable (undo restores acceptability), never silently applied.
|
||||
- **PUC-5 (coexist)** Several pending proposals on distinct ranges coexist;
|
||||
each is independently decidable, in any order.
|
||||
|
||||
## 5. UX Layout
|
||||
|
||||
No bespoke UI surface — native VS Code primitives only (per the
|
||||
Claude-Design-vs-Claude-Code rubric this is squarely **Claude Code** territory:
|
||||
code-resident native surfaces, no visual design artifacts to produce). Pending
|
||||
proposals render as: (1) an **amber background tint** on the target range
|
||||
(sibling of F3's indigo agent tint / green human gutter), and (2) a **proposal
|
||||
thread** anchored at the range — a second Comments controller
|
||||
(`cowriting.proposals`, distinct from `cowriting.threads` so discussion and
|
||||
ratification never mix) whose single comment body shows the instruction (when
|
||||
present) and a fenced diff of current → proposed text, with **✓ Accept** and
|
||||
**✗ Reject** as thread title actions. The status bar surfaces a count of
|
||||
stale/orphaned proposals (F3's orphan-count pattern). The existing
|
||||
"Ask Claude to edit selection" command keeps its name and gesture — it now ends
|
||||
in a proposal instead of a buffer mutation.
|
||||
|
||||
---
|
||||
|
||||
## 6. Technical Design
|
||||
|
||||
### 6.1 Invariants
|
||||
|
||||
Parent invariants INV-1..INV-9 carry over unchanged. F4 adds:
|
||||
|
||||
- **INV-10** A proposal **never mutates the document**. Proposed text lives only
|
||||
in the sidecar and the review UI until accepted; the document changes only via
|
||||
the seam, only on accept. (INV-9 still holds: the seam remains the single
|
||||
machine-edit ingress — F4 moves its *trigger* from turn-completion to human
|
||||
acceptance.)
|
||||
- **INV-11** Accept is **fingerprint-guarded**: a proposal applies only when its
|
||||
anchor re-resolves with an exact text match at decision time. A proposal whose
|
||||
target text changed (stale) or cannot be found (orphaned) is surfaced and
|
||||
undecidable-as-accept — never applied by guess (extends INV-1).
|
||||
- **INV-12** Accept and reject are **human-only gestures**: no code path lets
|
||||
the machine ratify its own proposal.
|
||||
- **INV-13** `proposals[]` holds **pending proposals only** — state, not
|
||||
history (the F3 span-list precedent). Accept converts a proposal into
|
||||
attribution spans (that substrate plus git history is the durable record);
|
||||
reject removes it.
|
||||
|
||||
### 6.2 High-level architecture
|
||||
|
||||
One new vscode-free model slice plus one new controller, mirroring the F2/F3
|
||||
unit pattern; everything else is reused unchanged:
|
||||
|
||||
- **`proposals` model** — the typed `Proposal` shape replacing the `unknown[]`
|
||||
placeholder (`schemaVersion` stays 1). Pure helpers for diff-body formatting
|
||||
and stale-detection are vscode-free and unit-testable.
|
||||
- **ProposalController** — owns the proposal lifecycle: programmatic ingress
|
||||
(`proposeAgentEdit`), persistence via `CoauthorStore.update`, live range
|
||||
tracking + resolve/stale/orphan on load and external change (Anchorer ladder),
|
||||
rendering (amber decoration + proposal threads on its own
|
||||
`CommentController`), and the accept/reject actions.
|
||||
- **LiveTurn — retargeted, not rewritten:** `editSelection` keeps its
|
||||
guards/turn/replacement-extraction and now ends in `proposeAgentEdit` instead
|
||||
of `applyAgentEdit`.
|
||||
- **Seam, AttributionController, CoauthorStore, Anchorer — reused unchanged**,
|
||||
except the store's anchor prune learns about proposal anchorIds (the noted F4
|
||||
TODO).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
sel["selection + instruction"] --> turn["@cline/sdk live turn\n(claude-code)"]
|
||||
turn --> prop["proposeAgentEdit\n(anchor + persist, INV-10)"]
|
||||
e2e["E2E / programmatic ingress\n(cowriting.proposeAgentEdit)"] --> prop
|
||||
prop --> ui["amber tint + proposal thread\n(fenced diff, ✓/✗)"]
|
||||
ui -- "✓ accept (human, INV-12)" --> guard{"re-resolve exact?\n(INV-11)"}
|
||||
guard -- yes --> seam["applyAgentEdit seam\n(unchanged, INV-9)"]
|
||||
seam --> attr["F3 attribution substrate\n(spans, Claude tint)"]
|
||||
guard -- "no (stale/orphan)" --> flag["flagged, undecidable-as-accept,\nrecoverable"]
|
||||
ui -- "✗ reject (human)" --> gone["proposal removed,\ndocument untouched"]
|
||||
seam --> gone
|
||||
```
|
||||
|
||||
### 6.3 Data model & ownership
|
||||
|
||||
Same sidecar, same `anchors` map, same `Provenance` — `proposals[]` is filled,
|
||||
not reshaped (parent INV-4; no `schemaVersion` bump):
|
||||
|
||||
```jsonc
|
||||
"proposals": [
|
||||
{
|
||||
"id": "pr1",
|
||||
"anchorId": "a9", // shared anchors map; fingerprint.text IS the
|
||||
// exact target text — the staleness oracle
|
||||
"replacement": "…", // the full proposed text for the anchored range
|
||||
"author": { "kind": "agent", "id": "claude",
|
||||
"agent": { "sdk": "@cline/sdk", "model": "…", "sessionId": "…" } },
|
||||
"createdAt": "ISO-8601",
|
||||
"turnId": "turn-…", // groups N proposals born of one live turn
|
||||
"instruction": "…" // optional: what the human asked for (review context)
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- **Pending-only** (INV-13): accept/reject remove the entry; there is no status
|
||||
field to go stale.
|
||||
- **Staleness/orphanhood are not persisted** — resolve-time outcomes, exactly
|
||||
like F2 threads and F3 attributions: the anchor's `fingerprint.text` is
|
||||
compared at render/decision time.
|
||||
- **Persisted at propose time** (the F2 thread-mutation precedent, not the F3
|
||||
save-time one): a proposal is the product of a completed paid turn — losing it
|
||||
to a crash wastes the turn. `CoauthorStore.update` is the only writer; the
|
||||
anchor prune's referenced-set gains `proposals[].anchorId`.
|
||||
- Ownership/format rules unchanged: extension sole writer; stable key ordering;
|
||||
ids via `newId()`.
|
||||
|
||||
### 6.4 Interfaces & contracts
|
||||
|
||||
- **ProposalController**:
|
||||
`proposeAgentEdit(doc, range, replacement, provenance, opts?: { turnId?, instruction? }): Promise<Proposal>`
|
||||
— fingerprints the range (Anchorer), persists, renders; **does not touch the
|
||||
document** (INV-10). Also contributed as the hidden command
|
||||
`cowriting.proposeAgentEdit` (the E2E entry point, mirroring
|
||||
`cowriting.applyAgentEdit`).
|
||||
`accept(proposalId): Promise<boolean>` — re-resolve → exact-text guard
|
||||
(INV-11) → `applyAgentEdit(doc, resolvedRange, replacement, author,
|
||||
{ expectedVersion: doc.version, turnId })` → on success remove + dispose UI.
|
||||
`reject(proposalId): Promise<void>` — remove + dispose UI.
|
||||
`getPending(docPath)` / `getStaleCount()` — test-facing, F3's
|
||||
`getSpans`/`getOrphanCount` pattern.
|
||||
- **Seam — contract unchanged** (single-range `applyAgentEdit`); F4 is its
|
||||
intended driver, exactly as the F3 spec reserved.
|
||||
- **LiveTurn**: `runEditTurn` unchanged; the `editSelection` wiring calls
|
||||
`proposeAgentEdit` with the turn's model/sessionId provenance + the
|
||||
instruction.
|
||||
- **Commands/menus**: `cowriting.acceptProposal` / `cowriting.rejectProposal`
|
||||
on the proposal thread's title menu (palette-hidden); `editSelection` and
|
||||
`createThread` context-menu entries unchanged.
|
||||
|
||||
### 6.5 Per–Product-Use-Case design
|
||||
|
||||
- **PUC-1 (propose):** existing `editSelection` guards + turn → on completion,
|
||||
`proposeAgentEdit` anchors the selection (buildFingerprint), persists via
|
||||
`store.update`, creates the proposal thread (body = instruction + fenced
|
||||
diff of fingerprint.text → replacement) and applies the amber decoration.
|
||||
Document version untouched; no pending-edit registration occurs.
|
||||
- **PUC-2 (accept):** thread action → re-resolve the anchor on the current
|
||||
document; require an exact resolve (the ladder's text match — INV-11); call
|
||||
the seam with the resolved range, the stored replacement, the stored agent
|
||||
provenance, `expectedVersion = document.version` captured at that instant,
|
||||
and the stored `turnId`. Seam success → attribution spans land (F3, zero new
|
||||
code) → `store.update` removes the proposal → thread + decoration disposed.
|
||||
Seam `false` (version race) → report, proposal stays pending.
|
||||
- **PUC-3 (reject):** thread action → `store.update` removes the proposal →
|
||||
UI disposed. No document interaction at all.
|
||||
- **PUC-4 (persist / re-anchor / stale):** on load/external change, resolve
|
||||
each proposal's anchor: clean resolve → render at the new range; text
|
||||
mismatch or no match → stale/orphaned — thread labeled
|
||||
("⚠ stale — target text changed"), accept action disabled-by-guard, status
|
||||
bar counts it; a later edit restoring the text re-resolves it back to
|
||||
decidable (recoverable, INV-1 spirit). Within a session, the thread's live
|
||||
range shifts with edits (Comments API + `anchorer.shift`, the F2 pattern);
|
||||
the exact-text guard runs at decision time regardless.
|
||||
- **PUC-5 (coexist):** proposals are independent records on independent
|
||||
anchors; accepting one shifts the others' live ranges via the normal change
|
||||
event; order-independence falls out of per-proposal resolve-at-decision.
|
||||
|
||||
### 6.6 Non-functional requirements & cross-cutting concerns
|
||||
|
||||
O(pending proposals) render/resolve cost — trivially small at inner-loop scale.
|
||||
Proposal bodies add sidecar bytes proportional to proposed text (bounded: one
|
||||
selection's replacement each; pending-only keeps the array short-lived).
|
||||
Accept-time work is one resolve + one seam call. No new credential surface
|
||||
(INV-8 untouched); proposed text is operator content, same sensitivity class as
|
||||
the document itself. Skip non-text/oversized files (F2 rule). Live-turn
|
||||
latency UX unchanged (progress notification); the version-race window narrows
|
||||
to accept-click → applyEdit, guarded by `expectedVersion` as today.
|
||||
|
||||
### 6.7 Key decisions & alternatives considered
|
||||
|
||||
| Decision | Chosen | Alternatives rejected |
|
||||
| --- | --- | --- |
|
||||
| `proposals[]` shape | Pending-only: anchored range + replacement + provenance (+ turnId, instruction); accept→attribution, reject→remove (INV-13) | status-field lifecycle records (state that can go stale; duplicates what removal expresses); event log of decisions (history is git's + the transcript's job; heavy churn) |
|
||||
| Rendering primitive | Second Comments controller (anchored thread, fenced-diff body, ✓/✗ title actions) + amber range decoration | ghost-text decorations (effectively single-line; no action surface; can't show multi-line replacements); `vscode.diff` editor (leaves the buffer; heavyweight; no inline anchoring); refactor-preview panel (not inline; no persistence story; partial-confirm mismatches the seam registration); writing conflict-markers into the buffer (violates INV-10 — document must stay untouched) |
|
||||
| Propose-by-default vs. direct mode | Wholesale flip: `editSelection` always proposes; **no user-facing direct mode** (the seam stays direct as accept path + hidden E2E command) | a `cowriting.machineEdits` setting (two modes to test/explain for no demonstrated need — YAGNI; revisit on friction); separate propose-vs-edit commands (same cost, plus gesture ambiguity) |
|
||||
| Multi-range seam growth | **Not grown.** One proposal = one anchored range + one replacement; a multi-edit turn files N proposals sharing `turnId`; each accept is one single-range seam call | widening `applyAgentEdit` to ranges[] (touches the registry/minimization machinery F3 hardened, for a capability N-proposals already provides); atomic multi-range accept (no user story at 1-selection turns) |
|
||||
| Accept/reject granularity | Whole-proposal (= per anchored range). Hunk-splitting deferred: the N-proposals-per-`turnId` model makes "split one turn into independently-decidable pieces" purely additive later | per-hunk decisions inside one proposal now (needs a real LCS/Myers diff + sub-hunk anchoring today, for the single-selection turn that mostly yields one coherent rewrite) |
|
||||
| Staleness guard | The existing Anchorer exact-text resolve at decision time (INV-11) — fingerprint.text doubles as the base-text record | storing a separate baseText + custom comparison (duplicates the fingerprint); applying onto whatever the range now holds (silently overwrites newer human edits — unacceptable) |
|
||||
| Persistence moment | At propose time (thread precedent) | save-time only, like attribution spans (a crash between turn and save discards a paid turn's output) |
|
||||
|
||||
### 6.8 Testing strategy
|
||||
|
||||
- **Unit (vitest, vscode-free):** `Proposal` model round-trip (serialize →
|
||||
reload, stable formatting, prune interplay: proposal anchors retained,
|
||||
decided-proposal anchors pruned when unreferenced); diff-body formatting;
|
||||
stale-detection given (fingerprint, current text) pairs.
|
||||
- **Host E2E (`@vscode/test-electron`, F2/F3 pattern, no LLM):** programmatic
|
||||
`cowriting.proposeAgentEdit` → thread + decoration present **and document
|
||||
text unchanged** (INV-10) → accept → text replaced, attribution spans agent
|
||||
(`getSpans`), proposal gone → reject path → untouched + gone → propose →
|
||||
save → reopen → still pending, re-anchored → edit target text → stale
|
||||
(accept refused, status-bar count) → undo → decidable again → edit *above*
|
||||
the target → shifted, still decidable → multiple pending proposals decided
|
||||
out of order.
|
||||
- **Live smoke (manual, documented — extends `docs/MANUAL-SMOKE-F3.md`):**
|
||||
select → instruct → proposal appears (document untouched) → accept → lands
|
||||
Claude-attributed. Signed-out failure path unchanged from F3.
|
||||
|
||||
### 6.9 Failure modes, rollback & flags
|
||||
|
||||
Stale/orphaned proposals — flagged, undecidable-as-accept, recoverable
|
||||
(INV-11); reject always works. Accept-time version race — seam returns `false`,
|
||||
reported, proposal stays pending (retryable). Crash mid-accept (seam applied,
|
||||
removal not yet persisted) — benign on reload: the proposal resolves **stale**
|
||||
against its own accepted replacement, is flagged, and the human rejects the
|
||||
husk; the attribution record is already correct; never double-applies (INV-11).
|
||||
Sidecar merge conflicts — plain JSON, human-resolvable (unchanged). No feature
|
||||
flag: additive schema fill; an empty `proposals[]` is the off state; the
|
||||
propose-by-default flip is the feature itself and shipping it is the rollout.
|
||||
|
||||
---
|
||||
|
||||
## 7. Delivery Plan
|
||||
|
||||
### 7.1 Approach / strategy
|
||||
|
||||
One planning-and-executing session (F4 = #12), plan written just-in-time from
|
||||
this spec, per the F2/F3 precedent.
|
||||
|
||||
### 7.2 Slicing plan
|
||||
|
||||
- **SLICE-1** Typed `proposals[]` model + store prune extension (+ round-trip
|
||||
and prune unit tests).
|
||||
- **SLICE-2** ProposalController core: programmatic ingress
|
||||
(`proposeAgentEdit` + hidden command), persistence at propose time,
|
||||
resolve/stale/orphan ladder on load/external change (+ unit tests on the
|
||||
pure parts).
|
||||
- **SLICE-3** Review UI: proposal threads (second Comments controller,
|
||||
diff-fenced body, ✓/✗ title actions), amber pending-range decoration,
|
||||
accept (fingerprint-guarded seam call) / reject wiring, status-bar
|
||||
stale count.
|
||||
- **SLICE-4** LiveTurn flip to propose-by-default; manual smoke doc updated
|
||||
(propose → accept → attributed); graceful-failure paths re-verified.
|
||||
- **SLICE-5** Host E2E per §6.8 (propose/accept/reject/persist/stale/shift/
|
||||
coexist; INV-10 asserted) + README update.
|
||||
|
||||
E2E are first-class plan tasks (handbook §9 / §4; this app's required tier is
|
||||
host E2E — a VS Code extension has no browser surface or deploy stage, the
|
||||
F2/F3 precedent).
|
||||
|
||||
### 7.3 Rollout / launch plan
|
||||
|
||||
Still non-shippable (no marketplace publish). "Done" = issue #12 acceptance
|
||||
met: unit + host E2E green, live smoke performed once on this machine showing
|
||||
propose → accept → Claude-attributed.
|
||||
|
||||
### 7.4 Risks & mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
| --- | --- |
|
||||
| Accept applies over text that changed since proposing | INV-11: exact fingerprint-text resolve at decision time; stale = undecidable-as-accept, recoverable |
|
||||
| Proposal UI confused with discussion threads | separate `CommentController` + distinct labels/icons + amber (not indigo) tint |
|
||||
| Crash between seam apply and proposal removal | reload resolves the husk as stale against its own replacement; flagged, rejectable; never double-applies |
|
||||
| Comments-API rendering limits for large diffs | fenced-diff markdown body (native rendering); selection-scoped turns bound size; diff body is presentation-only (truth is `fingerprint.text` → `replacement`) |
|
||||
| Flipping propose-by-default breaks F3 E2E/smoke assumptions | seam + `cowriting.applyAgentEdit` command unchanged (F3 E2E untouched); only the `editSelection` tail moves; smoke doc updated in the same slice |
|
||||
| Sidecar churn from abandoned proposals | pending-only array (INV-13); reject is one gesture; stale proposals are visible debt in the status bar, not silent rot |
|
||||
|
||||
---
|
||||
|
||||
## 8. Traceability matrix
|
||||
|
||||
| Requirement (issue #12 acceptance) | Use case | Design | Slice |
|
||||
| --- | --- | --- | --- |
|
||||
| Live turn yields a proposal, not a mutation; inline diff rendered | PUC-1 | INV-10, §6.2, §6.5 | SLICE-2/3/4 |
|
||||
| Accept applies via the seam; spans land Claude-attributed | PUC-2 | INV-9/11/12, §6.4, §6.5 | SLICE-3 |
|
||||
| Reject leaves the document untouched | PUC-3 | §6.5 | SLICE-3 |
|
||||
| Pending proposals persist git-natively + reload | PUC-4 | INV-13, §6.3 | SLICE-1/2 |
|
||||
| Proposals survive edits: re-anchor; stale/orphan never applied | PUC-4 | INV-11, Anchorer ladder | SLICE-2/3 |
|
||||
| Unit + host E2E coverage, no LLM in CI | — | §6.8 | SLICE-1/2/5 |
|
||||
|
||||
## 9. Open Questions & Decisions log
|
||||
|
||||
- **RESOLVED (this session — the five forks routed by #12):**
|
||||
(a) `proposals[]` = pending-only anchored-range + replacement + provenance
|
||||
records (INV-13); (b) rendering = second Comments controller with fenced-diff
|
||||
thread + amber range decoration; (c) propose-by-default **wholesale** — no
|
||||
user-facing direct mode, the seam stays as accept path + E2E ingress;
|
||||
(d) seam stays **single-range** — multi-edit turns are N proposals sharing
|
||||
`turnId`; (e) granularity = whole-proposal accept/reject, hunk-splitting
|
||||
additive later via the same N-proposals model. Plus: staleness guard =
|
||||
fingerprint-text resolve at decision time (INV-11); persistence at propose
|
||||
time.
|
||||
- **OPEN → F5:** the cross-rung format may re-home `proposals[]` with the rest
|
||||
of the sidecar; the proposed→ratified→attributed story is what crosses rungs.
|
||||
- **OPEN → later:** hunk-splitting one turn into multiple proposals (needs a
|
||||
real diff algorithm); proposal **counter-edits** (human amends the proposed
|
||||
text before accepting — today: reject and re-instruct); a queue/peek UX when
|
||||
many proposals are pending (status-bar count suffices at inner-loop scale);
|
||||
whether rejected proposals deserve an optional history trail for F5.
|
||||
|
||||
## 10. Glossary & References
|
||||
|
||||
- **Proposal** — a pending machine edit: one anchored target range + one
|
||||
proposed replacement + agent provenance; never in the document until
|
||||
accepted. **Stale** — a pending proposal whose target text no longer matches
|
||||
its fingerprint (undecidable-as-accept, recoverable). **Ratify** — the
|
||||
human's accept gesture, the only path from proposal to document (INV-12).
|
||||
**Husk** — a proposal left behind by a crash mid-accept; resolves stale
|
||||
against its own replacement and is rejected by the human.
|
||||
- Epic #1 · Feature #12 (F4) · F3 #6 (PR #7) · F2 #4 (PR #5) · parent specs
|
||||
`coauthoring-inner-loop.md` + `coauthoring-attribution.md` · POC #2 ·
|
||||
rfc-app#48/#46 (lineage).
|
||||
Reference in New Issue
Block a user