/** * GlobalSidecarStore — the out-of-workspace/untitled authoring sidecar (F8 spec * §6.2/§6.4), mirroring BaselineStore (src/baselineStore.ts): vscode-free (Node * fs/crypto only), one `Artifact` JSON per document in VS Code's per-extension * GLOBAL storage, NEVER the repo (INV-19/24). The key passed in is the DOCUMENT * KEY (the URI string for the docs this store serves): * - `file:` URI → disk at `/sidecars/.json`. * - `untitled:` → an in-memory Map only (no durable identity — the F6 * degrade); lost on reload, and never read by mergeArtifacts (INV-25). * `consumeSelfWrite` is a no-op (these artifacts are outside the `.threads/` * watcher, so there is no self-write storm to suppress). */ import * as fs from "node:fs"; import * as path from "node:path"; import { createHash } from "node:crypto"; import { SCHEMA_VERSION, emptyArtifact, isNewerMajor, serializeArtifact, type Artifact, } from "./model"; import type { SidecarStore } from "./sidecarStore"; export class GlobalSidecarStore implements SidecarStore { /** untitled keys live here only — no durable identity to persist against. */ private readonly memory = new Map(); /** @param storageDir absolute VS Code GLOBAL-storage dir (context.globalStorageUri.fsPath). */ constructor(private readonly storageDir: string) {} private isUntitled(key: string): boolean { return key.startsWith("untitled:"); } /** Filesystem-safe storage key for a persistable (file:) doc: sha256 of its URI. */ private storageKey(key: string): string { return createHash("sha256").update(key).digest("hex"); } /** Disk path for a `file:` key, or undefined for an in-memory untitled key. */ sidecarPath(key: string): string | undefined { if (this.isUntitled(key)) return undefined; return path.join(this.storageDir, "sidecars", `${this.storageKey(key)}.json`); } load(key: string): Artifact | null { if (this.isUntitled(key)) return this.memory.get(key) ?? null; const p = this.sidecarPath(key)!; if (!fs.existsSync(p)) return null; return JSON.parse(fs.readFileSync(p, "utf8")) as Artifact; } save(key: string, artifact: Artifact): void { if (this.isUntitled(key)) { this.memory.set(key, artifact); return; } const p = this.sidecarPath(key)!; fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, serializeArtifact(artifact), "utf8"); } /** See CoauthorStore.update — same INV-16 throw + anchor prune, no self-write mark. */ update(key: string, mutate: (artifact: Artifact) => void): Artifact { const artifact = this.load(key) ?? emptyArtifact(key); if (isNewerMajor(artifact)) { throw new Error( `refusing to write ${key}: sidecar schemaVersion ${artifact.schemaVersion} > supported ${SCHEMA_VERSION} (INV-16)`, ); } mutate(artifact); const referenced = new Set([ ...artifact.threads.map((t) => t.anchorId), ...artifact.attributions.map((a) => a.anchorId), ...artifact.proposals.map((p) => p.anchorId), ]); for (const id of Object.keys(artifact.anchors)) { if (!referenced.has(id)) delete artifact.anchors[id]; } this.save(key, artifact); return artifact; } /** No-op: global artifacts are outside the `.threads/` watcher. */ consumeSelfWrite(_fsPath: string): boolean { return false; } }