Files
vscode-cowriting-plugin-con…/specs/coauthoring-sidecar-contract.md
T
2026-06-10 22:55:56 -07:00

268 lines
14 KiB
Markdown
Raw 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: living
---
# Coauthoring Sidecar — the Cross-Rung Contract (v1.0)
| | |
| --- | --- |
| **Author(s)** | Ben Stull (with Claude) |
| **Status** | `living` — this is a standing contract, not a one-shot Solution Design |
| **Contract version** | v1.0 · `schemaVersion: 1` |
| **Machine-checkable half** | `vscode-cowriting-plugin/schemas/coauthoring-sidecar.schema.json` (+ `scripts/validate-sidecar.mjs`) |
| **Reference implementation** | `vscode-cowriting-plugin/src/model.ts` (serializer), `src/mergeArtifacts.ts` (merge), `scripts/crossrung-reply.mjs` (conforming foreign writer) |
| **Source artifacts** | Solution Design `coauthoring-cross-rung-format.md` (F5, Feature `benstull/vscode-cowriting-plugin#14`) · supersedes the format sections (§6.3) of `coauthoring-inner-loop.md`, `coauthoring-attribution.md`, `coauthoring-propose-accept.md` as the format reference |
**This document is canonical (INV-14).** Format changes land here first, then in
the JSON Schema, then in code — documentation-leads-automation applied to the
format. The Schema and every implementation conform to this prose; on
disagreement, this document wins and the others are fixed.
## Changelog
| Date | Contract | schemaVersion | Change |
| --- | --- | --- | --- |
| 2026-06-10 | v1.0 | 1 | Initial publication. Ratifies the shipped F2F4 format (anchors/threads/attributions/proposals) and adds the additive cross-rung identity fields (`Provenance.email`, `agent.onBehalfOf`) plus the compatibility (INV-15/16) and merge (INV-17) rules. |
---
## 1. Scope
The **coauthoring sidecar** is the git-native record of a document's
human+machine coauthoring state: anchored discussion threads, authorship
attribution, and pending machine proposals. It is the **canonical cross-rung
carrier** — any rung of the ladder (the VS Code editor extension, a Gitea-rung
surface, rfc-app) reads and writes the same file per this contract; git
push/pull is the transport. There is no server, no sync daemon, and no
migration between rungs ("git is truth; the surfaces are clients" —
rfc-app#46).
- **Location:** `.threads/<repo-relative-document-path>.json`, in the same
repository as the document it describes (e.g. `docs/spec.md`
`.threads/docs/spec.md.json`). The document path is the sidecar key; file
rename/move tracking is out of scope for v1.
- **One sidecar per document.** Each sidecar is a single JSON object, the
**Artifact** (§3.1).
- **Multiple writers are expected.** The editor extension is *a* writer, not
*the* writer. Any process conforming to §5 may append to or modify the
record.
## 2. Serialization rules (INV-2)
A conforming writer emits the artifact **deterministically**, so diffs stay
minimal and any two conforming writers produce byte-identical output for the
same logical content:
1. **Pretty JSON**, 2-space indent, UTF-8, **exactly one trailing newline**,
no BOM.
2. **Root key order:** `schemaVersion`, `document`, `anchors`, `threads`,
`attributions`, `proposals` — then unknown root keys (rule 5).
3. **`anchors` keys sorted** lexicographically.
4. **Known keys of every object in the canonical order** given by the field
tables in §3 (the order shown is normative). Optional known fields that are
absent are **omitted entirely** (never `null`).
5. **Unknown keys serialize after known keys, sorted lexicographically**
(INV-15's deterministic placement — see §4).
## 3. Data model
### 3.1 Artifact (root)
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `schemaVersion` | integer ≥ 1 | ✓ | The format **major**. v1.0 of this contract = `1`. See §4. |
| `document` | object | ✓ | `{ "path": string }` — repo-relative path of the described document; the sidecar key. |
| `anchors` | object | ✓ | Map of anchor id → `Anchor` (§3.2). The **shared** anchoring primitive: threads, attributions, and proposals all reference entries here by id. Anchors referenced by no record are pruned by writers that own the rewrite. |
| `threads` | array | ✓ | `Thread[]` (§3.4). |
| `attributions` | array | ✓ | `AttributionRecord[]` (§3.5). **State, not history** — the current authorship map, not an edit log. |
| `proposals` | array | ✓ | `Proposal[]` (§3.6). **Pending-only** — accepted/rejected proposals are removed, never archived here. |
All four sections are required even when empty (`{}` / `[]`).
### 3.2 Anchor & Fingerprint
An **Anchor** is `{ "fingerprint": Fingerprint }`.
A **Fingerprint** locates a text region by content, not position:
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `text` | string | ✓ | The anchored text itself. **The text is the truth**: a region resolves only where this text is found (with its context). For proposals, `text` is also the exact replace target. |
| `before` | string | ✓ | ≤ ~3 lines / 120 chars of leading context. |
| `after` | string | ✓ | ≤ ~3 lines / 120 chars of trailing context. |
| `lineHint` | integer ≥ 0 | ✓ | 0-based line of the anchor start at write time. A **tie-breaker among multiple text matches, never truth**: a reader must not resolve an anchor by line number alone. |
Resolution semantics: resolve by exact `text` match disambiguated by
`before`/`after`, tie-broken by proximity to `lineHint`; if the text cannot be
found, the record is **orphaned** — surfaced to the user, never silently moved
or guessed (the editor's INV-1).
### 3.3 Provenance
The reusable author field, carried by messages, attributions, and proposals.
Identity is **git's own model**: `email` is the cross-rung join key (forges
already map email → account); `id` is the display/short name. There are no
signatures in v1 — this is provenance, not authentication; the git history is
the integrity layer.
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `kind` | `"human"` \| `"agent"` | ✓ | Who kind of actor authored this. |
| `id` | string | ✓ | Short/display name (e.g. a username, `"claude"`). Not forge-resolvable on its own. |
| `email` | string | — | The actor's email — the cross-rung identity join key. Omitted when unknown (e.g. no git config). |
| `agent` | object | ✓ when `kind:"agent"` | Machine details, below. Absent on humans. |
`agent` payload:
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `sdk` | string | ✓ | The driving SDK (e.g. `"@cline/sdk"`). |
| `model` | string | ✓ | Model name/alias used for the turn. |
| `sessionId` | string | ✓ | The SDK session/run id (may be `""` when unavailable). |
| `onBehalfOf` | object | — | `{ "id": string, "email"?: string }` — the **human operator the machine acted for** (rfc-app#46 §6.5's `On-behalf-of:` made data). Omitted when not established. |
**PII note:** emails in a repo-committed file are exactly what git commits
already record; adopters should know the sidecar carries the same class of
data as `git log`.
### 3.4 Thread & Message
**Thread** — an anchored discussion:
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `id` | string | ✓ | Unique id (reference writers use `t_<uuid>`). |
| `anchorId` | string | ✓ | Key into `anchors`. |
| `status` | `"open"` \| `"resolved"` | ✓ | Discussion state. |
| `messages` | array | ✓ | `Message[]`, chronological. |
**Message:**
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `id` | string | ✓ | Unique id (`m_<uuid>`). |
| `author` | Provenance | ✓ | §3.3. |
| `body` | string | ✓ | Markdown message body. |
| `createdAt` | string | ✓ | ISO-8601 UTC timestamp (`YYYY-MM-DDTHH:mm:ss.sssZ`). |
### 3.5 AttributionRecord
The current authorship map: an anchored, author-attributed span. **State, not
history** — records are coalesced/split as the document evolves.
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `id` | string | ✓ | Unique id (`at_<uuid>`). |
| `anchorId` | string | ✓ | Key into `anchors`. |
| `author` | Provenance | ✓ | Who authored the span's text. |
| `createdAt` | string | ✓ | ISO-8601 UTC. |
| `updatedAt` | string | ✓ | ISO-8601 UTC; bumped when the span's extent/fingerprint changes. Drives merge resolution (§6). |
| `turnId` | string | — | Groups all spans applied by one machine turn. |
### 3.6 Proposal
A **pending** machine edit awaiting a human decision. Decisions are
human-only; a proposal never mutates the document until accepted.
| Key | Type | Req | Semantics |
| --- | --- | --- | --- |
| `id` | string | ✓ | Unique id (`p_<uuid>`). |
| `anchorId` | string | ✓ | Key into `anchors`; the anchor's `fingerprint.text` **is** the exact target text and is immutable for the proposal's life. |
| `replacement` | string | ✓ | The full proposed text for the anchored range. |
| `author` | Provenance | ✓ | The proposing machine (with `onBehalfOf` where known). |
| `createdAt` | string | ✓ | ISO-8601 UTC. |
| `turnId` | string | — | Groups N proposals born of one turn. |
| `instruction` | string | — | What the human asked for (review context). |
## 4. Compatibility & versioning (INV-15, INV-16)
`schemaVersion` is the **major**. The rules:
1. **Within a major, changes are additive-only**: new *optional* fields or new
root sections. Existing documents remain valid unmodified; the major does
not bump.
2. **Readers ignore unknown optional fields** they don't implement (but see
rule 3).
3. **Writers preserve unknown fields — INV-15 (round-trip preservation).** A
conforming writer rewriting a sidecar MUST carry every field it does not
recognize, at **every object level** (root, anchor, fingerprint, thread,
message, provenance, attribution, proposal, …), serialized after known
keys, sorted (§2 rule 5). **No rung's writer may destroy another rung's
data.**
4. **A newer major is read-only — fail-safe.** A reader encountering
`schemaVersion` greater than the major it implements MUST NOT write the
sidecar: render what it understands, surface a warning naming the version,
and skip every write path. Recoverable by upgrading the older rung.
5. **Breaking changes bump the major** and are recorded in this changelog.
The JSON Schema deliberately allows unknown fields everywhere; it validates
known-field structure only. Validation of a v-next document against the v1
schema therefore still passes for the v1 subset — by design.
## 5. Conforming-writer checklist
Any process writing a sidecar (a forge surface, a CLI, an editor) MUST:
1. **Validate before writing** (schema + the §3 semantics it touches); refuse
to operate on an invalid record rather than "fixing" it silently.
2. **Refuse newer majors** (§4 rule 4).
3. **Preserve unknown fields** (§4 rule 3).
4. **Mint fresh uuid-based ids** for new records (`m_`/`t_`/`at_`/`p_` +
UUID); never reuse or guess ids.
5. **Stamp ISO-8601 UTC timestamps** (`new Date().toISOString()` shape).
6. **Carry identity**: set `author.email` when the writer knows it; agents set
`onBehalfOf` when the operator is established.
7. **Serialize per §2** (byte-determinism) and **validate again after** the
rewrite.
Executable example: `vscode-cowriting-plugin/scripts/crossrung-reply.mjs` — a
self-contained foreign writer that appends a thread reply per this checklist.
## 6. Merge rules (INV-17)
Two writers on two clones can diverge; git will surface the sidecar as a
textual conflict. The **semantic** resolution is a deterministic 2-way merge,
`merge(ours, theirs)`:
- **Refuse differing majors** (error; a rung never merges what it can't fully
read).
- **`anchors`**: union by key. Same key, divergent value → deterministic
tie-break (below), **key reported**.
- **`threads` / `attributions` / `proposals`**: union by `id`. Records present
on one side only pass through; identical records dedupe. Same-id divergence
resolves:
- **Thread**: the **shell** (all fields except `messages`) and the messages
merge independently. Divergent shells → deterministic tie-break, thread id
reported. (A consequence: divergent `status` resolves to `"resolved"`
over `"open"`, since the tie-break is lexicographic.) `messages` union by
`id`, **ordered by (`createdAt`, `id`)**; divergent same-id messages →
tie-break, message id reported. The common forge case — one side appends a
reply while the other edits elsewhere — merges **cleanly by construction**
(fresh uuids, disjoint sections).
- **Attribution**: **newer `updatedAt` wins**; equal `updatedAt`
tie-break. Id reported either way.
- **Proposal / Message / Anchor** (no `updatedAt`): tie-break. Id reported.
- **Unknown root keys**: union; same key divergent → tie-break, key reported.
- **`document`**: must agree; divergence → tie-break + `"document"` reported
(should not happen — sidecars are per-path).
**The deterministic tie-break:** the **lexicographically larger
stable-stringify** wins, where stable-stringify is JSON with all object keys
sorted and no whitespace. Symmetric: `merge(a,b)` and `merge(b,a)` pick the
same winner.
**Merge honesty (INV-17):** every rule-resolved divergence is **surfaced to
the caller** (the list of resolved ids/keys) — never silently guessed. Humans
remain the final arbiter: the file is plain pretty JSON and stays
human-resolvable; these rules give tools and humans the same answer.
Reference implementation:
`vscode-cowriting-plugin/src/mergeArtifacts.ts` (`mergeArtifacts(ours,
theirs): { merged, conflicts }`). Wiring it as a `.gitattributes` merge driver
is a deployment convenience, not part of this contract.
## 7. Out of scope (v1)
File rename/move tracking · signed provenance / authentication ·
`ratifiedBy` / multi-party provenance · rejected-proposal history trails ·
any transport beyond git itself.