feat(f8): SidecarStore interface + GlobalSidecarStore (sha256 key, untitled in-memory)

Spec §6.2/§6.4: out-of-workspace file: → <globalStorage>/sidecars/<sha256(uri)>.json
(INV-19/24); untitled: → in-memory only (F6 degrade). update() carries the INV-16
newer-major throw + anchor prune; consumeSelfWrite is a no-op (outside the .threads
watcher). Mirrors src/baselineStore.ts; vscode-free, unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 13:06:08 -07:00
parent 568139d548
commit 515acb4868
3 changed files with 227 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { createHash } from "node:crypto";
import { GlobalSidecarStore } from "../src/globalSidecarStore";
import { emptyArtifact, SCHEMA_VERSION, type Artifact } from "../src/model";
let dir: string;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), "global-sidecar-"));
});
afterEach(() => {
fs.rmSync(dir, { recursive: true, force: true });
});
const FILE_URI = "file:///abs/notes/chapter-1.md";
const hash = (uri: string) => createHash("sha256").update(uri).digest("hex");
function withThread(uri: string): Artifact {
const a = emptyArtifact(uri);
a.anchors["an_1"] = { fingerprint: { text: "hi", before: "", after: "", lineHint: 0 } };
a.threads.push({ id: "t_1", anchorId: "an_1", status: "open", messages: [] });
return a;
}
describe("GlobalSidecarStore — file: URIs persist to <dir>/sidecars/<sha256(uri)>.json", () => {
it("returns null for a key with no sidecar", () => {
expect(new GlobalSidecarStore(dir).load(FILE_URI)).toBeNull();
});
it("save then load round-trips the artifact at the hashed path", () => {
const store = new GlobalSidecarStore(dir);
const a = withThread(FILE_URI);
store.save(FILE_URI, a);
const onDisk = path.join(dir, "sidecars", `${hash(FILE_URI)}.json`);
expect(fs.existsSync(onDisk)).toBe(true);
expect(store.load(FILE_URI)).toEqual(a);
});
it("sidecarPath returns the hashed disk path for a file: URI", () => {
const store = new GlobalSidecarStore(dir);
expect(store.sidecarPath(FILE_URI)).toBe(path.join(dir, "sidecars", `${hash(FILE_URI)}.json`));
});
it("overwrites in place (newest wins, no history)", () => {
const store = new GlobalSidecarStore(dir);
store.save(FILE_URI, withThread(FILE_URI));
const empty = emptyArtifact(FILE_URI);
store.save(FILE_URI, empty);
expect(store.load(FILE_URI)).toEqual(empty);
});
it("consumeSelfWrite is a no-op returning false (outside the .threads watcher)", () => {
const store = new GlobalSidecarStore(dir);
store.save(FILE_URI, withThread(FILE_URI));
expect(store.consumeSelfWrite(store.sidecarPath(FILE_URI)!)).toBe(false);
});
it("update applies the mutation, prunes unreferenced anchors, and persists", () => {
const store = new GlobalSidecarStore(dir);
const result = store.update(FILE_URI, (a) => {
a.anchors["orphan"] = { fingerprint: { text: "x", before: "", after: "", lineHint: 0 } };
a.anchors["kept"] = { fingerprint: { text: "y", before: "", after: "", lineHint: 0 } };
a.threads.push({ id: "t", anchorId: "kept", status: "open", messages: [] });
});
expect(Object.keys(result.anchors)).toEqual(["kept"]); // "orphan" pruned
expect(store.load(FILE_URI)!.threads.length).toBe(1);
});
it("update throws on a newer-major sidecar (INV-16 backstop)", () => {
const store = new GlobalSidecarStore(dir);
const future = { ...emptyArtifact(FILE_URI), schemaVersion: SCHEMA_VERSION + 1 };
store.save(FILE_URI, future as Artifact);
expect(() => store.update(FILE_URI, () => {})).toThrow(/INV-16/);
});
});
describe("GlobalSidecarStore — untitled: keys are in-memory only (no disk, lost on reload)", () => {
const UNTITLED = "untitled:Untitled-1";
it("save then load round-trips within the same instance (session)", () => {
const store = new GlobalSidecarStore(dir);
const a = emptyArtifact(UNTITLED);
a.threads.push({ id: "t", anchorId: "an", status: "open", messages: [] });
a.anchors["an"] = { fingerprint: { text: "z", before: "", after: "", lineHint: 0 } };
store.save(UNTITLED, a);
expect(store.load(UNTITLED)).toEqual(a);
});
it("writes NO disk file for an untitled key", () => {
const store = new GlobalSidecarStore(dir);
store.save(UNTITLED, emptyArtifact(UNTITLED));
expect(store.sidecarPath(UNTITLED)).toBeUndefined();
expect(fs.existsSync(path.join(dir, "sidecars"))).toBe(false);
});
it("a fresh instance (simulated reload) has no untitled state", () => {
const store = new GlobalSidecarStore(dir);
store.save(UNTITLED, emptyArtifact(UNTITLED));
expect(new GlobalSidecarStore(dir).load(UNTITLED)).toBeNull();
});
});