F2 SLICE-1: artifact schema + CoauthorStore with round-trip tests (#4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Versioned coauthoring artifact schema (spec §6.3) — the shared git-native
|
||||
* envelope under every coauthoring capability. `anchors` and the `Provenance`
|
||||
* author field are SHARED primitives (INV-4): F3 (attributions) and F4
|
||||
* (proposals) add arrays that reference the same shapes without a format break.
|
||||
*
|
||||
* vscode-free and pure, so it is unit-testable in Node (mirrors src/cline.ts).
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export const SCHEMA_VERSION = 1 as const;
|
||||
|
||||
/** Anchored text + bounded context + a line tie-breaker (spec §6.3). */
|
||||
export interface Fingerprint {
|
||||
text: string;
|
||||
/** <= ~3 lines / 120 chars leading context. */
|
||||
before: string;
|
||||
/** <= ~3 lines / 120 chars trailing context. */
|
||||
after: string;
|
||||
/** 0-based line of the anchor start; a tie-breaker, NOT truth (INV-3). */
|
||||
lineHint: number;
|
||||
}
|
||||
|
||||
export interface Anchor {
|
||||
fingerprint: Fingerprint;
|
||||
}
|
||||
|
||||
/** The reusable author field (spec §6.3). `agent` only present when kind=agent. */
|
||||
export type Provenance =
|
||||
| { kind: "human"; id: string }
|
||||
| { kind: "agent"; id: string; agent: { sdk: string; model: string; sessionId: string } };
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
author: Provenance;
|
||||
body: string;
|
||||
/** ISO-8601. */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type ThreadStatus = "open" | "resolved";
|
||||
|
||||
export interface Thread {
|
||||
id: string;
|
||||
anchorId: string;
|
||||
status: ThreadStatus;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface Artifact {
|
||||
schemaVersion: number;
|
||||
/** repo-relative path; the sidecar key. */
|
||||
document: { path: string };
|
||||
/** SHARED primitive (INV-4). */
|
||||
anchors: Record<string, Anchor>;
|
||||
threads: Thread[];
|
||||
/** F3 extension point — reuses anchors[]; not implemented in F2. */
|
||||
attributions: unknown[];
|
||||
/** F4 extension point — reuses anchors[] + provenance; not in F2. */
|
||||
proposals: unknown[];
|
||||
}
|
||||
|
||||
export function emptyArtifact(docPath: string): Artifact {
|
||||
return {
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
document: { path: docPath },
|
||||
anchors: {},
|
||||
threads: [],
|
||||
attributions: [],
|
||||
proposals: [],
|
||||
};
|
||||
}
|
||||
|
||||
/** Stable, generated id (spec §6.3). */
|
||||
export function newId(prefix: string): string {
|
||||
return `${prefix}_${randomUUID()}`;
|
||||
}
|
||||
|
||||
function serializeProvenance(p: Provenance): Record<string, unknown> {
|
||||
return p.kind === "agent"
|
||||
? { kind: p.kind, id: p.id, agent: { sdk: p.agent.sdk, model: p.agent.model, sessionId: p.agent.sessionId } }
|
||||
: { kind: p.kind, id: p.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty-printed JSON with STABLE key ordering (anchors sorted by id), so
|
||||
* sidecar diffs stay minimal and merge-friendly (spec §6.3, INV-2). Always
|
||||
* ends with a trailing newline.
|
||||
*/
|
||||
export function serializeArtifact(a: Artifact): string {
|
||||
const canonical = {
|
||||
schemaVersion: a.schemaVersion,
|
||||
document: { path: a.document.path },
|
||||
anchors: Object.fromEntries(
|
||||
Object.keys(a.anchors)
|
||||
.sort()
|
||||
.map((k) => [
|
||||
k,
|
||||
{
|
||||
fingerprint: {
|
||||
text: a.anchors[k].fingerprint.text,
|
||||
before: a.anchors[k].fingerprint.before,
|
||||
after: a.anchors[k].fingerprint.after,
|
||||
lineHint: a.anchors[k].fingerprint.lineHint,
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
threads: a.threads.map((t) => ({
|
||||
id: t.id,
|
||||
anchorId: t.anchorId,
|
||||
status: t.status,
|
||||
messages: t.messages.map((m) => ({
|
||||
id: m.id,
|
||||
author: serializeProvenance(m.author),
|
||||
body: m.body,
|
||||
createdAt: m.createdAt,
|
||||
})),
|
||||
})),
|
||||
attributions: a.attributions,
|
||||
proposals: a.proposals,
|
||||
};
|
||||
return JSON.stringify(canonical, null, 2) + "\n";
|
||||
}
|
||||
Reference in New Issue
Block a user