add spec ./specs/coauthoring-cross-rung-format.md (status: graduated)
This commit is contained in:
@@ -0,0 +1,491 @@
|
||||
---
|
||||
status: graduated
|
||||
---
|
||||
# Solution Design: Cross-Rung Git-Native Persistence Format + Gitea Round-Trip (F5)
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Author(s)** | Ben Stull (with Claude) |
|
||||
| **Reviewers / approvers** | Ben Stull |
|
||||
| **Status** | `draft` |
|
||||
| **Version** | v0.1.0 |
|
||||
| **Source artifacts** | Epic `benstull/vscode-cowriting-plugin#1` · Feature `#14` (F5) · Parent specs: `coauthoring-inner-loop.md` (§9 OPEN→F5: "the inner-loop shape, not yet the cross-rung contract"), `coauthoring-attribution.md` (§9 OPEN→F5), `coauthoring-propose-accept.md` (§9 OPEN→F5) · Ladder lineage: `ben.stull/rfc-app#48` (the three rungs) and `ben.stull/rfc-app#46` (the Gitea-substrate rung: "continuity of substrate — no migration cliff") · F4 shipped: `#12` (PR #13, session 0012) |
|
||||
|
||||
**Change log**
|
||||
|
||||
| Date | Version | Change | By |
|
||||
| --- | --- | --- | --- |
|
||||
| 2026-06-10 | v0.1.0 | Initial draft — brainstorming session 0014 | Ben Stull + Claude |
|
||||
|
||||
---
|
||||
|
||||
## 1. Business Context
|
||||
|
||||
### 1.1 Executive Summary
|
||||
|
||||
F5 graduates the `.threads/` sidecar from "the editor's private shape" into the
|
||||
**cross-rung contract**: a published, versioned, machine-validatable format any
|
||||
rung of the ladder (editor → Gitea substrate → rfc-app) can read and write
|
||||
without re-homing. It adds the three things a second writer needs — a
|
||||
**contract** (prose + JSON Schema, with compatibility rules including
|
||||
unknown-field preservation), a **cross-rung identity** extension on
|
||||
`Provenance` (git's own email model, plus `onBehalfOf` for agents), and a
|
||||
**deterministic merge** (`mergeArtifacts`) — and **proves the round-trip**: a
|
||||
record written in the editor is modified out-of-editor by a Gitea-rung stand-in
|
||||
and renders correctly on pull. Everything is additive; `schemaVersion` stays 1.
|
||||
|
||||
### 1.2 Background
|
||||
|
||||
Epic #1's outcome statement promises the record is "portable up to the Gitea
|
||||
rung later **without re-homing**", and rfc-app#46 names the strategic prize:
|
||||
**continuity of substrate — no migration cliff** ("git is truth; the surfaces
|
||||
are clients"). Every prior design deferred the contract to exactly this
|
||||
session: F2 called its format "the inner-loop shape, not yet the cross-rung
|
||||
contract"; F3 and F4 each routed "may re-home `attributions[]` /
|
||||
`proposals[]`" here. Meanwhile the shipped format was *built* for this moment
|
||||
— plain pretty JSON, stable key order, trailing newline, state-not-history
|
||||
sections, shared `anchors` — all chosen in F2 for mergeability. Issue #14
|
||||
carried six design forks to this session; all six are resolved here (§6.7,
|
||||
§9). One shipped fact forces real work: `serializeArtifact` rebuilds a
|
||||
canonical object from **known fields only**, so today's writer would silently
|
||||
destroy another rung's data on rewrite — the forward-compatibility gap F5
|
||||
closes (INV-15).
|
||||
|
||||
### 1.3 Business Actors / Roles
|
||||
|
||||
- **Coauthor (human / machine)** — as in F2–F4; unchanged in-editor.
|
||||
- **The second writer** — new: a teammate's editor on another clone, or a
|
||||
Gitea-rung surface appending to the same record per the contract.
|
||||
- **The downstream rungs** — the Gitea substrate (rfc-app#46) and rfc-app:
|
||||
consumers of the contract this Feature publishes.
|
||||
|
||||
### 1.4 Problem Statement
|
||||
|
||||
Portability has never been exercised. There is no published contract, no
|
||||
compatibility/versioning rules for readers that aren't this codebase, no
|
||||
forge-resolvable identity (`Provenance.id` is a local `$USER`), no defined
|
||||
behavior when a second writer touches the record, and a writer that drops
|
||||
fields it doesn't recognize. Until F5, "git is truth; the surfaces are
|
||||
clients" is an assertion, not a property.
|
||||
|
||||
### 1.5 Pain Points
|
||||
|
||||
- Clone the repo on a second machine and the record *probably* works — nothing
|
||||
guarantees it, and any other tool that rewrites the sidecar loses data.
|
||||
- No rung can validate a sidecar without reading this extension's TypeScript.
|
||||
- `kind:"human", id:"benstull"` means nothing to a forge; agents don't record
|
||||
who they acted for.
|
||||
- Two writers = undefined behavior; git will textually conflict pretty JSON
|
||||
and nothing documents the semantic resolution.
|
||||
|
||||
### 1.6 Targeted Business Outcomes
|
||||
|
||||
A contract any rung can implement against; identity a forge can resolve; a
|
||||
merge story a human (or tool) can execute deterministically; and a
|
||||
demonstrated out-of-editor round-trip — converting the epic's portability
|
||||
promise into a tested property and unblocking the rfc-app#46 substrate
|
||||
brainstorming with a concrete input.
|
||||
|
||||
### 1.7 Scope (business)
|
||||
|
||||
**In scope:** the cross-rung contract (prose, normative) + JSON Schema +
|
||||
rung-neutral validator; unknown-field preservation in the writer; the
|
||||
`Provenance` identity extension (`email`, `agent.onBehalfOf`); newer-major
|
||||
fail-safe (read-only) behavior; `mergeArtifacts` (deterministic union-by-id,
|
||||
documented + unit-tested); the Gitea-rung stand-in writer script + round-trip
|
||||
host E2E; byte-fidelity tests.
|
||||
|
||||
**Out of scope (deferred, not forgotten):** the Gitea-side substrate
|
||||
primitives themselves (rfc-app#46's build — discussions UI, LLM participants,
|
||||
native proposal layers); any rfc-app work; a sync daemon/server (git push/pull
|
||||
IS the transport); a git merge *driver* installation (the pure function is the
|
||||
semantics; wiring it into `.gitattributes` is a later convenience); file
|
||||
rename/move tracking; `ratifiedBy` / multi-party provenance (team-rung
|
||||
concern); rejected-proposal history trails.
|
||||
|
||||
### 1.8 Assumptions · Constraints · Dependencies
|
||||
|
||||
- **Parent:** Epic #1 (F5 is its final decomposition item; the three
|
||||
acceptance pillars shipped in F2 #4, F3 #6, F4 #12). **Blocked by:** #12
|
||||
(shipped). **Feeds:** the rfc-app#46 brainstorming and the rfc-app rung.
|
||||
- **Established format facts** (the contract ratifies what shipped):
|
||||
per-document `.threads/<path>.json`; shared `anchors` + `Fingerprint`
|
||||
(text/before/after/lineHint, INV-3); `Provenance` (`human|agent`);
|
||||
`threads[]` / `attributions[]` (state, not history) / pending-only
|
||||
`proposals[]` (INV-13); `SCHEMA_VERSION = 1`; stable key order + trailing
|
||||
newline (INV-2). The extension stops being the assumed sole writer.
|
||||
- All changes are **additive** — existing sidecars remain valid v1 documents
|
||||
unmodified; the serializer's optional-field-omission pattern (F4 precedent)
|
||||
carries the new fields.
|
||||
- No new auth/credential surface; no server.
|
||||
|
||||
### 1.9 Business Use Cases
|
||||
|
||||
- **BUC-1** A coauthor clones the repo on another machine (or a teammate
|
||||
does) and the full record — threads, attribution, pending proposals —
|
||||
arrives intact and keeps working.
|
||||
- **BUC-2** A Gitea-rung surface (today: the stand-in; tomorrow: rfc-app#46's
|
||||
primitives) appends a thread reply per the contract; on pull, the reply
|
||||
appears in the editor's comment thread.
|
||||
- **BUC-3** Two writers diverge; the documented merge yields one record with
|
||||
no silent data loss, and anything genuinely conflicting is surfaced.
|
||||
|
||||
---
|
||||
|
||||
## 2. Solution Proposal
|
||||
|
||||
Publish the **contract** in two halves: a normative prose document
|
||||
(`coauthoring-sidecar-contract.md`, a standing spec in the content repo) and a
|
||||
**JSON Schema** (`schemas/coauthoring-sidecar.schema.json`, code-resident in
|
||||
the plugin repo next to the reference implementation, wired into the unit
|
||||
suite and runnable by any rung via a small validator script). Extend
|
||||
`Provenance` additively with git's own identity model — `email` on humans,
|
||||
`agent.onBehalfOf {id, email?}` on agents — and make the writer
|
||||
**round-trip-safe**: unknown fields are preserved on rewrite (serialized after
|
||||
known keys, sorted), and a sidecar with a newer major `schemaVersion` is
|
||||
read-only (fail-safe). Ship the merge semantics as a pure, vscode-free
|
||||
`mergeArtifacts(ours, theirs)`: sections union by `id`, thread messages union
|
||||
by `id` ordered by `createdAt` (the forge-appends-a-reply case merges cleanly
|
||||
by construction), same-id divergence resolved by `updatedAt` where present
|
||||
else a deterministic tie-break — and every such resolution **surfaced** in the
|
||||
result, never silent. Prove the loop with a Gitea-rung **stand-in writer**
|
||||
(`scripts/crossrung-reply.mjs`: validate → append a reply → stable-serialize)
|
||||
driven by host E2E through the existing external-change machinery.
|
||||
|
||||
---
|
||||
|
||||
## 3. Product Personas
|
||||
|
||||
- **PP-1 Inner-loop coauthor** — unchanged in-editor experience; gains
|
||||
second-machine/teammate safety.
|
||||
- **PP-3 Rung implementer** — new: the developer building the Gitea-substrate
|
||||
or rfc-app surface against the contract (today impersonated by the stand-in
|
||||
script and the validator).
|
||||
|
||||
## 4. Product Use Cases
|
||||
|
||||
- **PUC-1 (fidelity)** A sidecar written by the editor survives
|
||||
serialize → git → reload byte-stable, and survives a *conforming foreign
|
||||
writer's* rewrite with all fields intact (including ones that writer doesn't
|
||||
know).
|
||||
- **PUC-2 (out-of-editor reply)** The stand-in appends a thread reply per the
|
||||
contract → the editor (on external change / pull) renders the new message in
|
||||
the right thread at the right anchor.
|
||||
- **PUC-3 (merge)** Two divergent copies merge via `mergeArtifacts`: disjoint
|
||||
changes union; the reply-append case is clean; same-id divergence resolves
|
||||
deterministically and is reported.
|
||||
- **PUC-4 (fail-safe)** The editor encounters a sidecar with a newer major
|
||||
`schemaVersion`: it surfaces a warning and does not write — never destroys
|
||||
what it can't fully read.
|
||||
- **PUC-5 (validation)** Any rung validates any sidecar against the schema
|
||||
(`scripts/validate-sidecar.mjs <file>` or any standard JSON Schema
|
||||
validator); the plugin's unit suite validates its own outputs.
|
||||
|
||||
## 5. UX Layout
|
||||
|
||||
No new UI surface (Claude Code, not Claude Design — same §5 call as F3/F4).
|
||||
Out-of-editor replies render through the **existing** F2 thread machinery
|
||||
(external-change → re-render). The only new user-visible element is a warning
|
||||
notification + read-only behavior on a newer-major sidecar (PUC-4), reusing
|
||||
the established notification patterns. The contract, schema, validator, and
|
||||
merge function are developer-facing artifacts.
|
||||
|
||||
---
|
||||
|
||||
## 6. Technical Design
|
||||
|
||||
### 6.1 Invariants
|
||||
|
||||
Parent invariants INV-1..INV-13 carry over unchanged. F5 adds:
|
||||
|
||||
- **INV-14 (contract-canonical):** the contract document is the format's
|
||||
source of truth. Format changes land in the contract first, then in code —
|
||||
documentation-leads-automation (handbook §4.1) applied to the format. The
|
||||
JSON Schema and the TypeScript model conform to it; on disagreement the
|
||||
contract wins.
|
||||
- **INV-15 (round-trip preservation):** a conforming writer **preserves
|
||||
fields it does not recognize** when rewriting a sidecar. No rung's writer
|
||||
may destroy another rung's data. (Unknown keys serialize after known keys,
|
||||
sorted lexicographically, so output stays deterministic — INV-2.)
|
||||
- **INV-16 (versioning):** within a major `schemaVersion`, changes are
|
||||
**additive-only** (new optional fields); readers ignore unknown optionals.
|
||||
A reader encountering a **newer major** version goes **fail-safe
|
||||
read-only**: render what it understands, write nothing. Breaking changes
|
||||
bump the major and are recorded in the contract's changelog.
|
||||
- **INV-17 (merge honesty):** merging is deterministic union-by-id per the
|
||||
contract's documented rules; any same-id divergence is resolved by those
|
||||
rules **and surfaced to the caller** — never silently guessed (INV-1's
|
||||
spirit at the artifact level).
|
||||
|
||||
### 6.2 High-level architecture
|
||||
|
||||
Four small units — two artifacts, two vscode-free modules — plus a serializer
|
||||
change and one fail-safe check; controllers and the editor UX are otherwise
|
||||
untouched:
|
||||
|
||||
- **The contract** (`vscode-cowriting-plugin-content`:
|
||||
`specs/coauthoring-sidecar-contract.md`) — normative prose: every section
|
||||
and field, its semantics and invariants, the identity model, the
|
||||
compatibility rules (INV-15/16), and the merge rules (INV-17), with a
|
||||
changelog. Status-stamped and graduated like a spec; versioned with the
|
||||
schema.
|
||||
- **The schema + validator** (plugin repo:
|
||||
`schemas/coauthoring-sidecar.schema.json` + `scripts/validate-sidecar.mjs`,
|
||||
`ajv` as a devDependency) — machine-checkable conformance; wired into the
|
||||
vitest suite (every serialized fixture validates) and runnable standalone
|
||||
by any rung.
|
||||
- **`model.ts` extensions** — `Provenance` gains optional `email` (human and
|
||||
agent alike); the agent payload gains optional
|
||||
`onBehalfOf: { id: string; email?: string }` (the operator the machine
|
||||
acted for — rfc-app#46 §6.5's `On-behalf-of:` made data). The serializer
|
||||
gains **unknown-field preservation** (INV-15) for the artifact root and
|
||||
every record type. `currentAuthor()` starts populating `email` from the
|
||||
workspace git config when available.
|
||||
- **`mergeArtifacts`** (new vscode-free `src/mergeArtifacts.ts`) —
|
||||
`mergeArtifacts(ours, theirs): { merged, conflicts }`: anchors map union;
|
||||
threads/attributions/proposals union by `id`; thread `messages` union by
|
||||
`id` ordered by (`createdAt`, `id`); same-id record divergence → newer
|
||||
`updatedAt` wins where the type has one, else the lexicographically larger
|
||||
canonical serialization (deterministic), and the id is reported in
|
||||
`conflicts`. Refuses (throws) on differing majors (INV-16).
|
||||
- **Fail-safe check** — `CoauthorStore.load` surfaces the artifact's
|
||||
`schemaVersion`; controllers (via one shared helper) treat a newer major as
|
||||
read-only: render best-effort, skip every `store.update` path, warn once
|
||||
(PUC-4).
|
||||
- **Stand-in writer** (`scripts/crossrung-reply.mjs`) — the Gitea-rung
|
||||
impersonator: read sidecar → validate → append a `Message` to a named
|
||||
thread (new `id`, `createdAt`, author with `email`) → stable-serialize →
|
||||
validate again. Doubles as executable documentation of "how a foreign
|
||||
writer behaves."
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph editor rung
|
||||
ext["extension writers\n(Thread/Attribution/Proposal controllers)"] --> ser["serializeArtifact\n+ unknown-field preservation (INV-15)"]
|
||||
end
|
||||
ser --> sc[".threads/<doc>.json\n(canonical carrier)"]
|
||||
sc <--> git["git push / pull\n(the transport)"]
|
||||
git <--> forge["Gitea rung\n(today: crossrung-reply.mjs stand-in)"]
|
||||
contract["contract doc (content repo)\n+ JSON Schema (plugin repo)"] -. governs (INV-14) .-> ser
|
||||
contract -. governs .-> forge
|
||||
sc --> merge["mergeArtifacts\n(union-by-id, surfaced conflicts, INV-17)"]
|
||||
sc --> guard{"schemaVersion major\n> known?"}
|
||||
guard -- yes --> ro["read-only + warn (INV-16)"]
|
||||
guard -- no --> render["existing render/re-anchor paths\n(F2/F3/F4, unchanged)"]
|
||||
```
|
||||
|
||||
### 6.3 Data model & ownership
|
||||
|
||||
Additive only — `schemaVersion` stays **1**; every existing sidecar is already
|
||||
a valid v1 document:
|
||||
|
||||
```jsonc
|
||||
// Provenance — human (new optional identity)
|
||||
{ "kind": "human", "id": "benstull", "email": "ben@wiggleverse.org" }
|
||||
|
||||
// Provenance — agent (new optional on-behalf-of)
|
||||
{ "kind": "agent", "id": "claude",
|
||||
"agent": { "sdk": "@cline/sdk", "model": "sonnet", "sessionId": "run_…",
|
||||
"onBehalfOf": { "id": "benstull", "email": "ben@wiggleverse.org" } } }
|
||||
```
|
||||
|
||||
- **Identity is git's model:** `email` is the cross-rung join key (forges
|
||||
already map email → account); `id` remains the display/short name. No
|
||||
signatures in v1 (provenance, not authentication — the git history is the
|
||||
integrity layer).
|
||||
- **Ownership changes:** the extension is no longer the assumed sole writer.
|
||||
Any conforming writer may append/modify per the contract; the
|
||||
self-write-suppression machinery is untouched (foreign writes *should*
|
||||
arrive as external changes and re-render — that is the F2 path).
|
||||
- **Unknown fields:** preserved on rewrite at every level (artifact root,
|
||||
anchor, thread, message, attribution, proposal), serialized after known
|
||||
keys, sorted (INV-15, INV-2).
|
||||
|
||||
### 6.4 Interfaces & contracts
|
||||
|
||||
- **Contract doc** — normative; field tables per section + invariants +
|
||||
compatibility (INV-15/16) + merge rules (INV-17) + changelog. The F2/F3/F4
|
||||
spec §6.3s are its inputs; it supersedes them as the format reference
|
||||
(INV-14).
|
||||
- **Schema/validator** — `validate-sidecar.mjs <file>` exits 0/1 with
|
||||
per-error paths; vitest helper `expectValidSidecar(artifact)` used by the
|
||||
model/store/merge suites.
|
||||
- **`mergeArtifacts(ours: Artifact, theirs: Artifact): { merged: Artifact;
|
||||
conflicts: string[] }`** — pure, vscode-free, documented as *the* reference
|
||||
implementation of the contract's merge rules. Not yet wired into any UI;
|
||||
rungs and humans invoke it (a `.gitattributes` merge driver is a later
|
||||
convenience, §1.7).
|
||||
- **`CoauthorStore.load`** — unchanged signature; a shared
|
||||
`isNewerMajor(artifact)` helper gates every write path (controllers skip
|
||||
`update`, warn once per doc).
|
||||
- **`scripts/crossrung-reply.mjs <sidecar> <threadId> <body> [--author-id
|
||||
--author-email]`** — the stand-in; validates before and after its write.
|
||||
|
||||
### 6.5 Per–Product-Use-Case design
|
||||
|
||||
- **PUC-1 (fidelity):** unit tests pin byte-stable serialize→parse→serialize;
|
||||
a "foreign writer" test parses a fixture containing unknown fields (root +
|
||||
per-record), rewrites via `serializeArtifact`, and asserts the unknowns
|
||||
survive in sorted-after-known position (INV-15).
|
||||
- **PUC-2 (out-of-editor reply):** host E2E — create a thread in the editor
|
||||
(F2 path) → save → run the stand-in against the sidecar (append reply) →
|
||||
trigger the external-change path (the F2/F3/F4
|
||||
`externalWriteAndReload`/watcher pattern) → assert the thread renders the
|
||||
new message at the re-resolved anchor. No LLM; no real network — the
|
||||
stand-in is a local process, exactly what a forge's write looks like to git.
|
||||
- **PUC-3 (merge):** unit tests of the documented cases: reply-append vs.
|
||||
disjoint editor edit (clean union, no conflicts); both sides append to the
|
||||
same thread (messages interleave by `createdAt`, no conflicts); same
|
||||
attribution id with divergent extents (newer `updatedAt` wins, id
|
||||
reported); divergent same-id proposals (deterministic tie-break, id
|
||||
reported); differing majors → throws.
|
||||
- **PUC-4 (fail-safe):** unit: a v2 fixture loads, `isNewerMajor` true; host
|
||||
E2E: open a doc whose sidecar says `schemaVersion: 2` → warning surfaced,
|
||||
edits/saves do NOT rewrite the sidecar (file bytes unchanged).
|
||||
- **PUC-5 (validation):** the schema validates all shipped fixtures + every
|
||||
serialized artifact in the unit suite; the validator script exits non-zero
|
||||
on a deliberately broken fixture.
|
||||
|
||||
### 6.6 Non-functional requirements & cross-cutting concerns
|
||||
|
||||
All additions are O(artifact) pure functions; no runtime dependencies added to
|
||||
the extension bundle (`ajv` is dev/scripts-only). Unknown-field preservation
|
||||
slightly grows the serializer but keeps determinism (INV-2). Identity adds
|
||||
PII-class data (emails) to a repo-committed file — acceptable: it is exactly
|
||||
what git commits already record; the contract notes it so adopters know.
|
||||
Fail-safe read-only can mean reduced function on version skew — by design
|
||||
(INV-16); the warning names the version. No secrets, no server, no new auth.
|
||||
|
||||
### 6.7 Key decisions & alternatives considered
|
||||
|
||||
| Decision | Chosen | Alternatives rejected |
|
||||
| --- | --- | --- |
|
||||
| Substrate carrier (fork a) | `.threads/` sidecar stays the canonical cross-rung carrier | git notes (no default push/fetch — breaks "travels with ordinary git"; forges don't render notes; bind to commits while the record is document-scoped); commit trailers (only fit commit-shaped data); a forge reads/renders/diffs JSON files natively — the F2 decision gets *stronger* cross-rung |
|
||||
| Contract form + home (fork b) | Prose contract = standing spec in the **content repo**; JSON Schema + validator = **plugin repo** (code-resident by the reference implementation, like the engineering repo's central-schema discipline applied at app scope) | schema in the content repo (test suite would read cross-repo; drifts from the model it validates); engineering `schemas/` now (org-central before a second rung exists — graduate it there when the Gitea rung lands, noted §9); prose-only (not machine-checkable); schema-only (semantics/invariants don't fit JSON Schema) |
|
||||
| Cross-rung identity (fork c) | Optional `email` on `Provenance` + optional `agent.onBehalfOf {id, email?}` — git's own identity model; forges map email→account | forge usernames (couples the record to one forge — the ladder spans surfaces); signed identities (authentication, not provenance; git history is the integrity layer; YAGNI v1); a global actor registry (a server by the back door) |
|
||||
| Merge semantics (fork d) | Pure 2-way `mergeArtifacts`: union-by-id; messages ordered by createdAt; updatedAt-else-deterministic tie-break; conflicts surfaced (INV-17) | 3-way with base (git gives no cheap base for sidecars in conflict resolution; union-by-id with fresh uuids makes 2-way sufficient at this scale); whole-file last-writer (silently loses the other side); mandated git merge driver (installation burden; the pure function IS the semantics — driver is later sugar) |
|
||||
| Round-trip proof shape (fork e) | Local stand-in writer script (validate → append reply → stable-serialize) + the existing external-change E2E path; byte-stability covers transport | real Gitea API round-trip in CI (infra for nothing — git moves bytes verbatim; the forge's *write* behavior is exactly the stand-in); building any Gitea-side primitive now (rfc-app#46's tracker, not this Feature) |
|
||||
| Versioning (fork f) | Everything additive → `schemaVersion` stays 1; INV-16 rules (ignore unknown optionals; preserve unknown fields; newer-major → read-only) | bump to 2 (nothing breaks — a bump would force every reader to handle two majors for no gain); no formal rules (the status quo — exactly the gap F5 closes) |
|
||||
| Writer forward-compat | **Preserve unknown fields** on rewrite (INV-15), sorted after known keys | drop unknowns (today's behavior — a conforming v1.1 writer would destroy a v1.2 rung's data); refuse to write on unknowns (turns every additive change into a flag day) |
|
||||
|
||||
### 6.8 Testing strategy
|
||||
|
||||
- **Unit (vitest, vscode-free):** schema validation of every serialized
|
||||
artifact (PUC-5); byte-stability + unknown-field preservation round-trips
|
||||
(PUC-1); identity serialization (email/onBehalfOf present/absent — F4's
|
||||
optional-omission pattern); `mergeArtifacts` documented cases incl. the
|
||||
conflict reports and the differing-major throw (PUC-3); `isNewerMajor` +
|
||||
v2-fixture load (PUC-4).
|
||||
- **Host E2E (`@vscode/test-electron`, F2–F4 pattern, no LLM):** the
|
||||
round-trip — editor-created thread → save → stand-in appends a reply →
|
||||
external-change → new message renders in the thread at the re-resolved
|
||||
anchor (PUC-2); newer-major sidecar → warning + no rewrite on save (PUC-4).
|
||||
- **Scripted checks:** `validate-sidecar.mjs` against good + broken fixtures;
|
||||
the stand-in's own before/after validation. No manual smoke needed — F5 has
|
||||
no LLM leg.
|
||||
|
||||
### 6.9 Failure modes, rollback & flags
|
||||
|
||||
A non-conforming foreign write → schema validation pinpoints it; the editor's
|
||||
existing resolve-or-orphan ladder already handles semantic damage (anchors
|
||||
that no longer resolve orphan, never guess). Version skew → INV-16 read-only,
|
||||
recoverable by upgrading the older rung. Merge conflicts → surfaced ids +
|
||||
documented rules; worst case the file is plain JSON a human resolves (the F2
|
||||
stance, now with rules). Email absence (no git config) → fields omitted,
|
||||
everything still valid (additive). No feature flag: additive schema + a
|
||||
contract doc; old extensions keep working against new sidecars (they drop
|
||||
unknown fields — which is precisely why INV-15 ships *now*, before any second
|
||||
writer exists).
|
||||
|
||||
---
|
||||
|
||||
## 7. Delivery Plan
|
||||
|
||||
### 7.1 Approach / strategy
|
||||
|
||||
One planning-and-executing session (F5 = #14), plan written just-in-time from
|
||||
this spec, per the F2–F4 precedent.
|
||||
|
||||
### 7.2 Slicing plan
|
||||
|
||||
- **SLICE-1** The contract: `coauthoring-sidecar-contract.md` (content repo,
|
||||
normative, with changelog) + `schemas/coauthoring-sidecar.schema.json` +
|
||||
`scripts/validate-sidecar.mjs` (+ ajv devDependency), schema wired into the
|
||||
unit suite (every serialized fixture validates).
|
||||
- **SLICE-2** Writer conformance: unknown-field preservation in
|
||||
`serializeArtifact` (INV-15) + `Provenance` identity extension
|
||||
(`email`, `agent.onBehalfOf`) + `currentAuthor()` email population +
|
||||
newer-major fail-safe (`isNewerMajor` gating all `store.update` paths,
|
||||
PUC-4 warning) — with compat tests (existing sidecars load/serialize
|
||||
unchanged).
|
||||
- **SLICE-3** `mergeArtifacts` + unit tests of every documented merge case
|
||||
(INV-17).
|
||||
- **SLICE-4** The stand-in (`scripts/crossrung-reply.mjs`) + host E2E
|
||||
round-trip (PUC-2) + newer-major E2E (PUC-4).
|
||||
- **SLICE-5** Docs: README cross-rung section; plugin-repo pointers to the
|
||||
contract; full gate.
|
||||
|
||||
E2E are first-class plan tasks (handbook §9 / §4; this app's required tier is
|
||||
host E2E — a VS Code extension, no browser surface, no deploy stage — the
|
||||
F2–F4 precedent).
|
||||
|
||||
### 7.3 Rollout / launch plan
|
||||
|
||||
Still non-shippable (no marketplace publish). "Done" = issue #14 acceptance
|
||||
met: contract + schema published, unit + host E2E green (round-trip, merge,
|
||||
fail-safe, preservation all exercised). No manual smoke — no LLM leg.
|
||||
|
||||
### 7.4 Risks & mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
| --- | --- |
|
||||
| Contract drifts from code | INV-14 (contract first) + the schema runs in the unit suite — drift fails CI |
|
||||
| Unknown-field preservation breaks byte-stability | deterministic placement rule (after known keys, sorted) + round-trip tests |
|
||||
| Merge rules too naive for real concurrency | scale honesty: union-by-id + fresh uuids makes overlap rare at inner-loop scale; conflicts are surfaced, file stays human-resolvable; revisit with the team rung |
|
||||
| Email-as-identity is wrong for some forge | it is git's own join key and the lowest common denominator; forge-specific ids can be added additively later (INV-16) |
|
||||
| Premature contract (Gitea rung may want changes) | everything additive + a changelog + the rfc-app#46 brainstorming consumes this as INPUT — cheaper to amend a contract than to reverse-engineer an implementation |
|
||||
| Read-only fail-safe surprises a user on version skew | warning names the version and the action; recoverable by upgrading; tested in E2E |
|
||||
|
||||
---
|
||||
|
||||
## 8. Traceability matrix
|
||||
|
||||
| Requirement (issue #14 acceptance) | Use case | Design | Slice |
|
||||
| --- | --- | --- | --- |
|
||||
| Contract document with compatibility rules | PUC-5 | §6.2, INV-14/15/16 | SLICE-1 |
|
||||
| Identity crosses rungs (Provenance) | PUC-2 | §6.3, fork (c) | SLICE-2 |
|
||||
| Round-trip proven, no re-homing | PUC-1/2 | stand-in + INV-15, §6.5 | SLICE-2/4 |
|
||||
| Concurrent-writer story defined and exercised | PUC-3 | `mergeArtifacts`, INV-17 | SLICE-3 |
|
||||
| Validation runnable outside the extension | PUC-5 | schema + validator | SLICE-1 |
|
||||
| Unit + host E2E coverage | — | §6.8 | SLICE-1–4 |
|
||||
|
||||
## 9. Open Questions & Decisions log
|
||||
|
||||
- **RESOLVED (this session — the six forks routed by #14):** (a) carrier =
|
||||
the sidecar, ratified cross-rung; (b) contract = prose (content repo
|
||||
standing spec) + JSON Schema (plugin repo, code-resident); (c) identity =
|
||||
`email` + `agent.onBehalfOf`, git's model; (d) merge = pure 2-way
|
||||
union-by-id with surfaced conflicts (INV-17); (e) proof = local stand-in
|
||||
writer + external-change E2E + byte-stability; (f) versioning = additive,
|
||||
`schemaVersion` stays 1, INV-16 rules (incl. writer unknown-field
|
||||
preservation, INV-15).
|
||||
- **OPEN → Gitea rung (rfc-app#46):** graduate the schema to the engineering
|
||||
central `schemas/` when a second rung actually implements the contract;
|
||||
whether the substrate wants a `.gitattributes` merge driver shipped;
|
||||
forge-specific identity fields beyond email.
|
||||
- **OPEN → later:** `ratifiedBy` / multi-party provenance (team rung); file
|
||||
rename/move tracking (path is still the key); rejected-proposal history
|
||||
trails; signed provenance if integrity-beyond-git is ever required.
|
||||
|
||||
## 10. Glossary & References
|
||||
|
||||
- **Cross-rung contract** — the published format definition all ladder
|
||||
surfaces implement against. **Foreign/conforming writer** — any non-editor
|
||||
process writing a sidecar per the contract. **Stand-in** — the local script
|
||||
impersonating the Gitea rung's write behavior. **Fail-safe read-only** —
|
||||
INV-16's posture on newer-major sidecars: render, never write.
|
||||
- Epic #1 · Feature #14 (F5) · F2 #4 · F3 #6 · F4 #12 (PR #13) · parent specs
|
||||
`coauthoring-inner-loop.md` / `coauthoring-attribution.md` /
|
||||
`coauthoring-propose-accept.md` · rfc-app#48 (ladder) · rfc-app#46
|
||||
(substrate rung) · `wiggleverse/engineering#19` (adjacent metadata
|
||||
discussion).
|
||||
Reference in New Issue
Block a user