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

14 KiB
Raw Permalink Blame History

status
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.