// Server-only disk I/O for the two digest sidecars. The atomic write and the // tolerant read that degrades to null are `sidecar()`'s (lib/sidecar-server.ts), // as for every sidecar; what is this file's own is the append-on-change history // (modelled on availability-server.ts) and a PARTIAL-OWNERSHIP merge that // preserves fields another writer owns. // // The partial-ownership property is the load-bearing one here: writeDigestSection // replaces exactly one section and leaves the other alone, so a metered tags run // never clobbers local chapters (and vice versa). import { sidecar, sidecarField } from "./sidecar-server"; import { DIGEST_FILENAME, DIGEST_OVERRIDES_FILENAME, DIGEST_OVERRIDES_VERSION, DIGEST_SCHEMA_VERSION, isDigestSectionKind, type DigestChapter, type DigestDerivedFrom, type DigestHistoryEntry, type DigestItem, type DigestOverrides, type DigestProvenance, type DigestRecord, type DigestSectionFailure, type DigestSectionKind, type DigestTag, type DigestWarning, } from "./digest"; // Cap the audit trail so a video that is regenerated across many prompt // iterations during Stage B tuning cannot grow an unbounded sidecar. const MAX_HISTORY_ENTRIES = 40; // --------------------------------------------------------------------------- // Reads (tolerant: anything unparseable or structurally wrong reads as absent, // so one corrupt sidecar can never fail a channel-wide sweep) // --------------------------------------------------------------------------- export function coerceDigest(value: unknown): DigestRecord | null { const parsed = value as Partial | null; if (typeof parsed?.digestSchemaVersion !== "number") return null; if (!parsed.sections || typeof parsed.sections !== "object") return null; return { digestSchemaVersion: parsed.digestSchemaVersion, promptVersion: typeof parsed.promptVersion === "number" ? parsed.promptVersion : 0, contextHash: typeof parsed.contextHash === "string" ? parsed.contextHash : "", warnings: Array.isArray(parsed.warnings) ? (parsed.warnings as DigestWarning[]) : [], sections: parsed.sections, // This reader rebuilds the record field by field rather than spreading, // so EVERY new field has to be added here or it round-trips to nothing — // silently, since the write succeeds and the read just omits it. ...(Array.isArray(parsed.failures) ? { failures: parsed.failures as DigestSectionFailure[] } : {}), ...(Array.isArray(parsed.history) ? { history: parsed.history as DigestHistoryEntry[] } : {}), ...(parsed.derivedFrom ? { derivedFrom: parsed.derivedFrom as DigestDerivedFrom } : {}), }; } export function coerceDigestOverrides(value: unknown): DigestOverrides | null { // `null` threw inside the old try (`parsed.chapters` on null) and so read as // absent; the explicit guard keeps that. if (value === null || value === undefined) return null; const parsed = value as Partial; // A hand-authored file may omit `version`; treat it as current rather than // discarding human work over a missing scalar. const chapters = sanitizeChapters(parsed.chapters); const tags = sanitizeTags(parsed.tags); if (chapters.length === 0 && tags.length === 0 && !parsed.note) return null; return { version: typeof parsed.version === "number" ? parsed.version : DIGEST_OVERRIDES_VERSION, ...(chapters.length > 0 ? { chapters } : {}), ...(tags.length > 0 ? { tags } : {}), ...(typeof parsed.note === "string" ? { note: parsed.note } : {}), ...(typeof parsed.updatedAt === "string" ? { updatedAt: parsed.updatedAt } : {}), }; } // Two sidecars, each read field by field above (so the read is also what drops // a field it does not name — see the comment in coerceDigest). export const digestSidecar = sidecar(DIGEST_FILENAME, sidecarField(coerceDigest)); export const digestOverridesSidecar = sidecar( DIGEST_OVERRIDES_FILENAME, sidecarField(coerceDigestOverrides), ); export const { path: digestPath, load: loadDigest, write: writeDigest, } = digestSidecar; export const { path: digestOverridesPath, load: loadDigestOverrides } = digestOverridesSidecar; // Hand-authored overrides are coerced, not trusted: an entry missing an id or a // body is dropped rather than poisoning the merge. function sanitizeChapters(value: unknown): DigestChapter[] { if (!Array.isArray(value)) return []; const out: DigestChapter[] = []; for (const raw of value) { if (!raw || typeof raw !== "object") continue; const r = raw as Record; if (typeof r.id !== "string" || !r.id.trim()) continue; const title = typeof r.title === "string" ? r.title.trim() : ""; const start = typeof r.start === "number" && Number.isFinite(r.start) ? Math.max(0, Math.floor(r.start)) : undefined; // An override may be a patch (id + enabled:false) with no title at all. if (!title && r.enabled !== false) continue; out.push({ id: r.id, ...(start !== undefined ? { start } : {}), ...(typeof r.clock === "string" ? { clock: r.clock } : {}), title, decidedBy: "human", ...(r.enabled === false ? { enabled: false } : {}), } as DigestChapter); } return out; } function sanitizeTags(value: unknown): DigestTag[] { if (!Array.isArray(value)) return []; const out: DigestTag[] = []; for (const raw of value) { if (!raw || typeof raw !== "object") continue; const r = raw as Record; if (typeof r.id !== "string" || !r.id.trim()) continue; const tag = typeof r.tag === "string" ? r.tag.trim() : ""; if (!tag && r.enabled !== false) continue; out.push({ id: r.id, tag, decidedBy: "human", ...(r.enabled === false ? { enabled: false } : {}), }); } return out; } // --------------------------------------------------------------------------- // Writes // --------------------------------------------------------------------------- export type WriteDigestSectionInput = { section: DigestSectionKind; items: DigestItem[]; provenance: DigestProvenance; // Warnings from THIS pass. Warnings belonging to the section being rewritten // are replaced; another section's warnings are preserved. warnings: DigestWarning[]; }; // Read-modify-write ONE section, preserving everything another writer owns. // This is the only path the generator uses, and it never touches // ai-digest.overrides.json — that file is human-owned, full stop. export async function writeDigestSection( videoDir: string, input: WriteDigestSectionInput, ): Promise { const existing = await loadDigest(videoDir); const sections = { ...(existing?.sections ?? {}) }; // The cast is contained here: DigestSectionKind and the item type are paired // by construction at every call site (digestVideo builds both together). (sections as Record)[input.section] = { provenance: input.provenance, items: input.items, }; // Drop the prior pass's warnings for THIS section only. const keptWarnings = (existing?.warnings ?? []).filter( (w) => w.section !== input.section, ); // This section just succeeded, so its recorded total failure is history. // Another section's failure is not ours to clear. const keptFailures = (existing?.failures ?? []).filter( (f) => f.section !== input.section, ); const entry: DigestHistoryEntry = { section: input.section, generatedAt: input.provenance.generatedAt, appId: input.provenance.appId, model: input.provenance.model, promptVersion: input.provenance.promptVersion, contextHash: input.provenance.contextHash, itemCount: input.items.length, warningCount: input.warnings.length, }; const history = [...(existing?.history ?? []), entry].slice( -MAX_HISTORY_ENTRIES, ); const record: DigestRecord = { digestSchemaVersion: DIGEST_SCHEMA_VERSION, promptVersion: input.provenance.promptVersion, contextHash: input.provenance.contextHash, warnings: [...keptWarnings, ...input.warnings], sections, ...(keptFailures.length > 0 ? { failures: keptFailures } : {}), history, }; // A freshly generated section makes the record this video's own again: it is // no longer a copy of a cluster's canonical member. await writeDigest(videoDir, record); return record; } // Record a generation pass that produced nothing usable. // // Writes NO section, on purpose — see DigestSectionFailure. The record it // leaves is what makes the failure reviewable at all: without it the only trace // is a job log that rotates, and the videos the model does worst on are exactly // the ones a review queue most needs to surface. // // At most one failure per section is kept: a sweep retries, and appending would // grow the sidecar without bound on a video that fails every pass. export async function writeDigestFailure( videoDir: string, failure: DigestSectionFailure, ): Promise { const existing = await loadDigest(videoDir); const failures = [ ...(existing?.failures ?? []).filter((f) => f.section !== failure.section), failure, ]; const record: DigestRecord = { digestSchemaVersion: DIGEST_SCHEMA_VERSION, // A failed pass must NOT claim the record's top-level identity — those // mirror the last pass that actually wrote a section, and overwriting them // here would make a corpus survey read a failure as a generation. promptVersion: existing?.promptVersion ?? failure.promptVersion, contextHash: existing?.contextHash ?? "", warnings: existing?.warnings ?? [], sections: existing?.sections ?? {}, failures, ...(existing?.history ? { history: existing.history } : {}), // Preserved: a mirror whose own regeneration failed is still carrying the // canonical member's digest, and dropping this would silently un-attribute // borrowed content the viewer labels as borrowed. ...(existing?.derivedFrom ? { derivedFrom: existing.derivedFrom } : {}), }; await writeDigest(videoDir, record); return record; } // Copy a canonical member's digest onto an aligned duplicate, stamped with the // provenance of where it came from and the measured timing offset that made // sharing safe. The receiving video's own overrides are untouched, so a human // correction on a mirror still wins over the shared machine content. export async function writeSharedDigest( videoDir: string, source: DigestRecord, derivedFrom: DigestDerivedFrom, ): Promise { const record: DigestRecord = { ...source, // The share is an event in the receiving video's history too. history: [ ...(source.history ?? []), ...Object.entries(source.sections) .filter(([kind]) => isDigestSectionKind(kind)) .map(([kind, section]) => ({ section: kind as DigestSectionKind, generatedAt: derivedFrom.sharedAt, appId: section!.provenance.appId, model: section!.provenance.model, promptVersion: section!.provenance.promptVersion, contextHash: section!.provenance.contextHash, itemCount: section!.items.length, warningCount: 0, })), ].slice(-MAX_HISTORY_ENTRIES), derivedFrom, }; await writeDigest(videoDir, record); return record; } // Human-authored write. Separate function, separate file, so nothing in the // generation path can reach it. export async function writeDigestOverrides( videoDir: string, overrides: DigestOverrides, ): Promise { const hasContent = (overrides.chapters?.length ?? 0) > 0 || (overrides.tags?.length ?? 0) > 0 || Boolean(overrides.note); if (!hasContent) { // Emptying the override list means "revert to machine output" — remove the // file rather than leaving an empty shadow behind. await digestOverridesSidecar.remove(videoDir); return; } await digestOverridesSidecar.write(videoDir, { version: DIGEST_OVERRIDES_VERSION, ...(overrides.chapters?.length ? { chapters: overrides.chapters } : {}), ...(overrides.tags?.length ? { tags: overrides.tags } : {}), ...(overrides.note ? { note: overrides.note } : {}), updatedAt: overrides.updatedAt ?? new Date().toISOString(), }); }