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:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 `<dir>/sidecars/<sha256(uri)>.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<string, Artifact>();
|
||||
|
||||
/** @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<string>([
|
||||
...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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user