add specs/coauthoring-sidecar-contract.md (cross-rung contract v1.0, F5 #14)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
---
|
||||
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/<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.
|
||||
Reference in New Issue
Block a user