feat: publish coauthoring-sidecar JSON Schema + rung-neutral validator (F5 SLICE-1, PUC-5, #14)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-10 22:54:39 -07:00
parent 0ddeaba5a6
commit 7c4d3ed79f
4 changed files with 227 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
/**
* Schema-conformance helper (F5 SLICE-1, PUC-5): every serialized artifact in
* the unit suite must validate against the published cross-rung schema —
* contract drift fails CI (spec §7.4).
*/
import { readFileSync } from "node:fs";
import Ajv from "ajv/dist/2020";
import { serializeArtifact, type Artifact } from "../../src/model";
const schemaUrl = new URL("../../schemas/coauthoring-sidecar.schema.json", import.meta.url);
const schema = JSON.parse(readFileSync(schemaUrl, "utf8"));
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
export function validateSidecar(data: unknown): { valid: boolean; errors: string[] } {
const valid = validate(data) as boolean;
const errors = (validate.errors ?? []).map((e) => `${e.instancePath || "/"} ${e.message}`);
return { valid, errors };
}
export function expectValidSidecar(artifact: Artifact): void {
const { valid, errors } = validateSidecar(JSON.parse(serializeArtifact(artifact)));
if (!valid) throw new Error(`sidecar failed schema validation:\n${errors.join("\n")}`);
}
+56
View File
@@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import { emptyArtifact, serializeArtifact, type Artifact } from "../src/model";
import { expectValidSidecar, validateSidecar } from "./helpers/validateSidecar";
function fullArtifact(): Artifact {
const a = emptyArtifact("docs/x.md");
a.anchors["a_1"] = { fingerprint: { text: "alpha", before: "", after: " beta", lineHint: 0 } };
a.threads.push({
id: "t_1",
anchorId: "a_1",
status: "open",
messages: [{ id: "m_1", author: { kind: "human", id: "ben" }, body: "hi", createdAt: "2026-06-10T00:00:00.000Z" }],
});
a.attributions.push({
id: "at_1",
anchorId: "a_1",
author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "run-1" } },
createdAt: "2026-06-10T00:00:00.000Z",
updatedAt: "2026-06-10T00:00:00.000Z",
turnId: "turn-1",
});
a.proposals.push({
id: "p_1",
anchorId: "a_1",
replacement: "ALPHA",
author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "run-1" } },
createdAt: "2026-06-10T00:00:00.000Z",
instruction: "shout",
});
return a;
}
describe("coauthoring-sidecar JSON Schema (F5 SLICE-1, PUC-5)", () => {
it("validates the empty artifact and a full artifact", () => {
expectValidSidecar(emptyArtifact("docs/x.md"));
expectValidSidecar(fullArtifact());
});
it("rejects a structurally broken sidecar with error paths", () => {
const broken = JSON.parse(serializeArtifact(fullArtifact()));
broken.threads[0].status = "closed"; // not in the enum
delete broken.schemaVersion;
const { valid, errors } = validateSidecar(broken);
expect(valid).toBe(false);
expect(errors.some((e) => e.includes("schemaVersion"))).toBe(true);
});
it("accepts unknown fields everywhere (forward compat, INV-16)", () => {
const data = JSON.parse(serializeArtifact(fullArtifact()));
data.futureSection = [{ id: "f_1" }];
data.threads[0].futureField = true;
data.threads[0].messages[0].reactions = ["+1"];
const { valid } = validateSidecar(data);
expect(valid).toBe(true);
});
});