spec(f6): v0.2.0 — diff-view on any file + untitled; global storage (#19)

Broaden F6 beyond workspace files: own diffable predicate (file:/untitled:)
decoupled from F2 isTracked; baseline → global storage keyed by URI hash;
untitled buffers in-memory. Adds §11 amendment + updates §1.7/§6.3/§6.5/§6.7/
INV-19 and the change log. Built in session 0019.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 07:52:43 -07:00
parent 805fd8078b
commit cc67643989
+81 -26
View File
@@ -8,14 +8,15 @@ status: graduated
| **Author(s)** | Ben Stull (with Claude) |
| **Reviewers / approvers** | Ben Stull |
| **Status** | `draft` |
| **Version** | v0.1.0 |
| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#17` (F6) · Epic `#1` (closed — F6 is the first post-epic UX feature) · Parent specs: `vscode-cowriting-plugin-content/specs/coauthoring-inner-loop.md`, `specs/coauthoring-attribution.md`, `specs/coauthoring-propose-accept.md`, `specs/coauthoring-cross-rung-format.md` + `specs/coauthoring-sidecar-contract.md` (all graduated) · F5 shipped: `#14` (PR #15, session 0015) · Lineage: `ben.stull/rfc-app#48` |
| **Version** | v0.2.0 |
| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#17` (F6) · Epic `#1` (closed — F6 is the first post-epic UX feature) · Parent specs: `vscode-cowriting-plugin-content/specs/coauthoring-inner-loop.md`, `specs/coauthoring-attribution.md`, `specs/coauthoring-propose-accept.md`, `specs/coauthoring-cross-rung-format.md` + `specs/coang-sidecar-contract.md` (all graduated) · F5 shipped: `#14` (PR #15, session 0015) · Lineage: `ben.stull/rfc-app#48` |
**Change log**
| Date | Version | Change | By |
| --- | --- | --- | --- |
| 2026-06-11 | v0.1.0 | Initial draft — brainstorming session 0017 | Ben Stull + Claude |
| 2026-06-11 | v0.2.0 | Broaden to **any file + untitled buffers** (not just workspace files); baseline → **global** storage keyed by a URI hash; F6 decoupled from F2 `isTracked` with its own diffable predicate. `#19`, session 0019. See §11. | Ben Stull + Claude |
---
@@ -23,7 +24,7 @@ status: graduated
### 1.1 Executive Summary
F6 gives the human coauthor the review surface the machine already has. F4
F6 gives the human coauthor the review surface the already has. F4
made every machine change reviewable-before-landing (proposal diffs); F3
paints who owns the current text; but the operator's **own** contribution is
visible only as final text. F6 adds a one-gesture **diff-view toggle**: flip
@@ -86,7 +87,8 @@ Self-review during coauthoring becomes as cheap as accepting a proposal.
**In scope:** the baseline lifecycle (initialize at first track, advance at
machine landings, pin on demand); persistence of the baseline across window
reloads (extension workspace storage); the toggle command + keybinding; a
reloads (extension **global** storage — v0.2.0, §11); the toggle command +
keybinding; a
native `vscode.diff` view (readonly baseline left, the **live document**
right — editing continues inside the diff view); test-facing surface; host
E2E + unit coverage; manual smoke.
@@ -167,10 +169,11 @@ seam owner: `AttributionController.onDidApplyAgentEdit`.
now); everything the operator changes from here shows against the pin.
- **PUC-4 (survive reload)** Reload the window mid-session → the baseline
(and therefore the diff) is the same as before the reload.
- **PUC-5 (graceful edges)** An untracked document refuses the toggle with
a warning; a missing/ancient baseline is visible in the diff title
(epoch + when) and one pin gesture recovers; external file changes (e.g.
a git pull) show up as changes — pin recovers there too.
- **PUC-5 (graceful edges)** The toggle works on **any** text document — a
file in or out of the workspace, or an untitled buffer (v0.2.0, §11); only a
non-text-editor focus warns. A missing/ancient baseline is visible in the
diff title (epoch + when) and one pin gesture recovers; external file changes
(e.g. a git pull) show up as changes — pin recovers there too.
## 5. UX Layout
@@ -203,8 +206,9 @@ Parent invariants INV-1..INV-17 carry over unchanged. F6 adds:
by pin.
- **INV-19 (pure view, repo-free)** The diff view never mutates the
document, the sidecar, or attribution state; the baseline snapshot lives
in extension workspace storage, never in the repo — `.threads/`, the
cross-rung contract (INV-14..17), and `SCHEMA_VERSION` are untouched.
in extension **global** storage (`context.globalStorageUri`, v0.2.0 — was
workspace storage), never in the repo — `.threads/`, the cross-rung
contract (INV-14..17), and `SCHEMA_VERSION` are untouched.
### 6.2 High-level architecture
@@ -245,15 +249,22 @@ No sidecar change of any kind (INV-19). The baseline is a private,
per-workspace extension artifact:
```jsonc
// <context.storageUri>/baselines/<repo-relative-docPath>.json
// v0.2.0 (#19): GLOBAL storage, keyed by a hash of the document URI so ANY
// file (in- or out-of-folder) maps to a stable filename:
// <context.globalStorageUri>/baselines/<sha256(documentUri)>.json
{
"docPath": "notes/chapter-1.md",
"uri": "file:///abs/path/notes/chapter-1.md", // the document URI (identity)
"text": "…full document text at the epoch…",
"capturedAt": "ISO-8601",
"reason": "opened" | "machine-landing" | "pinned"
}
```
**Untitled buffers (v0.2.0):** an unsaved buffer (`untitled:` scheme) has no
durable identity to persist against, so its baseline is **in-memory only**
(lost on reload) — the same degrade the storage-unavailable path uses. Saving
it makes it a `file:` doc with a persisted baseline.
- **One baseline per document** — the newest epoch wins; no history of past
baselines (state, not history — the F3/F4 precedent).
- **Owner:** the extension alone; the file lives outside the repo (VS
@@ -314,14 +325,14 @@ per-workspace extension artifact:
baseline written at the last landing/pin is what the reloaded window
diffs against. (The toggle's *tab* does not need to survive reload — the
baseline does.)
- **PUC-5 (graceful edges):** untracked / non-file schemes → warning, no
diff (the F2 `isTracked` rule). Storage unavailable
(`context.storageUri` undefined or write failure) → degrade to in-memory
baselines + one warning (the session still works; reload survival is
lost). Ancient baseline (workspace reopened weeks later) → the diff
title shows `since opened <date>` — self-describing — and pin recovers.
External-to-session file changes appear as changes (INV-18's stated
modulo); pin recovers.
- **PUC-5 (graceful edges):** v0.2.0 (§11) — the diffable predicate is any
`file:` or `untitled:` doc, so only a non-text-editor focus → warning, no
diff. Untitled buffers diff in-memory (no persistence). Storage unavailable
(`context.globalStorageUri` write failure) → degrade to in-memory baselines
+ one warning (the session still works; reload survival is lost). Ancient
baseline (reopened weeks later) → the diff title shows `since opened <date>`
— self-describing — and pin recovers. External-to-session file changes
appear as changes (INV-18's stated modulo); pin recovers.
### 6.6 Non-functional requirements & cross-cutting concerns
@@ -341,7 +352,7 @@ maintain.
| --- | --- | --- |
| **Baseline epoch (the load-bearing fork)** | **Machine-landinganchored auto-baseline**: initialize at first track, advance on every successful seam application, plus a human **pin** gesture. INV-9/10 make the resulting diff operator-authored by construction (INV-18) | **turn start** (under F4 propose-by-default the document doesn't change at turn start — the epoch collapses into the accept; not a distinct moment anymore); **last save** (duplicates native `compareWithSaved` / local history / timeline — #17's complement-not-duplicate rule); **fixed session-start** (machine landings bleed into the "my changes" diff — breaks INV-18); **multiple selectable baselines** (a selector UI for epochs nobody asked for — YAGNI; pin covers the operator-chosen case; selectable is additive later) |
| Pre-accept operator edits absorbed by an advance | Accepted trade-off: the diff reads "my changes since the machine last landed", not "my whole session"; pin gives manual control | **baseline splicing** (apply the accepted replacement *into* the baseline so older operator edits stay visible — needs current→baseline range mapping with murky edge cases when the machine edits operator-authored-since-baseline text; deferred additive refinement, §9) |
| Snapshot home | **Extension workspace storage** (`context.storageUri`) via vscode-free `BaselineStore`; survives reload; zero repo/contract footprint (INV-19) | **sidecar `.threads/`** (full document text per accept dwarfs the artifact and churns git diffs — INV-2; drags INV-14..17 into a local view-state concern no other rung needs); **gitignored repo cache** (litters the operator's working tree; needs `.gitignore` management; risks accidental commit); **in-memory only** (baseline dies at reload — the post-reload diff is empty and lies) |
| Snapshot home | **Extension global storage** (`context.globalStorageUri`, v0.2.0 — was workspace storage) via vscode-free `BaselineStore`, keyed by a URI hash so it works for any file; survives reload; zero repo/contract footprint (INV-19) | **sidecar `.threads/`** (full document text per accept dwarfs the artifact and churns git diffs — INV-2; drags INV-14..17 into a local view-state concern no other rung needs); **gitignored repo cache** (litters the operator's working tree; needs `.gitignore` management; risks accidental commit); **in-memory only** (baseline dies at reload — the post-reload diff is empty and lies) |
| Whose changes the diff shows | Whole-document diff; operator-authorship is INV-18's corollary, **no filtering** | **F3-attribution-filtered diff** (F3 is state-not-history — cannot reconstruct before-text; #17 itself notes this); **per-author diff lenses** (no user story; heavy) |
| View primitive | **Native `vscode.diff`**: readonly `cowriting-baseline:` virtual doc left, the **live document** right (editable in place; decorations/threads keep working) | **decoration-based inline diff** in the normal editor (re-implements diff rendering VS Code already has; F4's fenced-diff primitive is for proposals, not whole documents); **webview diff** (bespoke UI against the F2F5 native-primitives precedent) |
| Machine-landing signal | Additive `onDidApplyAgentEdit` event on the seam owner — INV-9's single ingress is the single signal source | **call-site wiring** (ProposalController + the hidden command both call `advance` — two sites today, N tomorrow, and each new seam driver must remember); **watching attribution records** (indirect, save-coupled, reconstructive) |
@@ -476,8 +487,52 @@ performed once on this machine.
enters the document, INV-9/10). **Pin** — the human gesture resetting the
baseline to now. **Advance** — the automatic baseline re-capture at a
machine landing (INV-18).
- Feature #17 (F6) · Epic #1 (closed) · F5 #14 (PR #15) · F4 #12 (PR #13) ·
F3 #6 (PR #7) · F2 #4 (PR #5) · parent specs `coauthoring-inner-loop.md`,
`coauthoring-attribution.md`, `coauthoring-propose-accept.md`,
`coauthoring-cross-rung-format.md`, `coauthoring-sidecar-contract.md` ·
lineage `ben.stull/rfc-app#48`.
- Feature #17 (F6) · Epic #1 · F5 #14 (PR #15) · F4 #12 (PR #13) ·
F3 #6 (PR #7) · F2 #4 (PR #5) · v0.2.0 follow-up #19 · parent specs
`coauthoring-inner-loop.md`, `coauthoring-attribution.md`,
`coauthoring-propose-accept.md`, `coauthoring-cross-rung-format.md`,
`coauthoring-sidecar-contract.md` · lineage `ben.stull/rfc-app#48`.
---
## 11. Amendment v0.2.0 — diff-view on any file (#19, session 0019)
v0.1.0 scoped F6 to **tracked** documents (a saved file inside the opened
workspace folder), reusing F2's `isTracked`. In manual testing this surfaced as
the toggle refusing untitled buffers and out-of-folder files — friction with no
real cause, since **F6 needs no `.threads/` sidecar**, only a stable document
identity and a storage home. This amendment broadens F6 to **any** text
document; the inner-loop ladder F2F5 is unchanged.
**Deltas (supersede the matching v0.1.0 text above):**
- **Diffable predicate (replaces the `isTracked` gate for F6).** A document is
diffable iff its scheme is `file:` **or** `untitled:`. F6 no longer consults
the workspace root. The two commands are therefore registered
**workspace-independently**`DiffViewController` is constructed before the
no-folder activation branch, so `cowriting.toggleDiffView` /
`cowriting.pinDiffBaseline` are live (never stubbed) even with no folder open.
Only a non-text-editor focus warns.
- **Snapshot home = global storage (replaces workspace storage in §2/§6.3/§6.7
and INV-19).** Baselines live under `context.globalStorageUri`
(machine-wide, always present, independent of any workspace), keyed by
`sha256(documentUri)` — one filesystem-safe key for in-folder, out-of-folder,
and (transiently) untitled docs. Still never the repo; INV-19 holds verbatim
apart from the storage-root word. `Baseline.docPath``Baseline.uri`.
- **Untitled buffers are in-memory only.** An `untitled:` doc has no durable
identity, so its baseline is held in memory and lost on reload (the
storage-unavailable degrade); the moment it is saved it becomes a `file:` doc
with a persisted baseline. The diff title carries an `(unsaved)` marker.
- **Machine-landing advance wiring is unchanged in scope.** The seam fires only
on workspace files, so `onDidApplyAgentEdit → advance` stays in the with-root
branch; F6's baseline capture-on-open is self-wired in the controller for any
diffable doc.
**Unchanged:** the view primitive (native `vscode.diff`, live doc on the
right), the epoch model (`opened`/`machine-landing`/`pinned` + pin), INV-18, the
no-LLM host-E2E tier. **Tests:** host E2E adds an out-of-workspace-file case
(persisted in global storage) and an untitled-buffer case (in-memory, not
persisted), plus a no-folder activation case proving the toggle works folder-
less; `BaselineStore` unit tests re-keyed.
**Shipped:** `#19` (session 0019) — see that session's transcript.