--- 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 F2–F4 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/.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_`). | | `anchorId` | string | ✓ | Key into `anchors`. | | `status` | `"open"` \| `"resolved"` | ✓ | Discussion state. | | `messages` | array | ✓ | `Message[]`, chronological. | **Message:** | Key | Type | Req | Semantics | | --- | --- | --- | --- | | `id` | string | ✓ | Unique id (`m_`). | | `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_`). | | `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_`). | | `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.