import { ModuleFilePath, PatchId } from "@valbuild/core"; /** * The order of the pending patches, held in one flat, append-only text file. * * ## Why a log and not links between the patches * * The store this replaces kept the order in the patches themselves: every record * carried a `parentRef`, and the directory a record lived in was named after that * parent. Reading the chain meant walking those links from `head`, and the walk * stopped dead — silently — at the first id nothing on disk answered for. * * That is not a hypothetical. A store with 410 patches lost exactly one record, * and the 51 patches written after it became unreachable: `/stat` counted the * directories and announced 410, `GET /patches` walked the links and delivered * 359, and the studio waited forever for 51 ids that no longer had a path back to * `head`. One missing file cost every edit made after it. * * So the order lives in one place and the patches reference nothing. A patch * directory is self-contained data; this file says what order the directories go * in. Removing a line cannot orphan the lines below it, because there is nothing * below it to orphan — the entry after a dropped one is simply next. * * ## Why text * * It is read by people during exactly the incidents that make it interesting, and * `cat` should be enough. One entry per line, whitespace-separated, id first: * * ``` * val-patch-log v1 * 659b8cfa-065c-47d1-8d82-fc69a2ac72a9 2026-08-27T11:46:13.730Z /content/authors.val.ts * ``` * * Position in the file IS the order. There is deliberately no sequence number: * a number stored beside the thing it describes is a number that can disagree * with it, and then something has to decide which one lies. * * The timestamp and path are there to make the file readable, not to be believed * — `patch.json` owns those fields. Nothing here is a second copy of state that * anything reads back. */ export declare const PATCH_LOG_FILE_NAME = "patches.log"; export type PatchLogEntry = { patchId: PatchId; createdAt: string; path: ModuleFilePath; }; /** * Something that is wrong with the file but does not stop it being read. * * Reported rather than thrown, because a log that is 99% intact is worth reading * and then repairing. Only a file that cannot be understood at all is fatal, and * that is a `status` on the read result, not a problem in this list. */ export type PatchLogProblem = { type: "missing-header"; /** What stood where the header should have been. */ firstLine: string; } | { type: "unsupported-version"; header: string; } | { type: "unparseable-line"; lineNumber: number; line: string; } | { type: "duplicate-entry"; patchId: PatchId; lineNumber: number; } /** * A final line with no newline after it: a write that did not finish. * * Discarded rather than guessed at, and only ever possible on the LAST line — * appends are serialized by the patch lock, so no other writer can have got in * behind an interrupted one. */ | { type: "torn-final-line"; line: string; }; export type ReadPatchLogResult = { status: "ok"; entries: PatchLogEntry[]; problems: PatchLogProblem[]; } /** No log file at all — an empty store, not a broken one. */ | { status: "absent"; } | { status: "unreadable"; message: string; }; export declare function formatPatchLogLine(entry: PatchLogEntry): string; /** * Split on the first two runs of whitespace only: a module file path may contain * spaces, and it is last precisely so that it can. */ export declare function parsePatchLogLine(line: string): PatchLogEntry | null; export declare function serializePatchLog(entries: readonly PatchLogEntry[]): string; export declare function readPatchLog(logFilePath: string): ReadPatchLogResult; export declare function parsePatchLog(raw: string): ReadPatchLogResult; /** * Add one entry to the end of the log. * * A single `writeSync` on an append-only descriptor, then fsync: the whole line * reaches the file or none of it does, and a reader that catches it mid-flight * discards the partial last line rather than misreading it. * * Callers must hold the patch lock. That is what makes "the torn line can only be * the last one" true, and it is why this does not try to be safe against * concurrent appends on its own. */ export declare function appendPatchLogEntry(logFilePath: string, entry: PatchLogEntry): void; /** * Replace the log wholesale — used by delete and by repair. * * Write a sibling temp file, fsync it, then rename over the original: a rename is * atomic, so a reader either sees the old log or the new one and never a * half-rewritten file. In-place truncation would have a window where the log is * short and the store looks like it lost patches. * * Callers must hold the patch lock. */ export declare function writePatchLogFile(logFilePath: string, entries: readonly PatchLogEntry[]): void;