@valbuild/server 0.102.0 → 0.103.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { FileMetadata, FileSource, ImageMetadata, ModuleFilePath, PatchId, RemoteSource, Schema, SelectorSource, SerializedSchema, Source, SourcePath, ValConfig, ValModules, ValidationError } from "@valbuild/core";
1
+ import { MediaSource, FileMetadata, FileSource, ImageMetadata, ModuleFilePath, PatchId, Schema, SelectorSource, SerializedSchema, Source, SourcePath, ValConfig, ValModules, ValidationError } from "@valbuild/core";
2
2
  import { result } from "@valbuild/core/fp";
3
3
  import { JSONValue, ParentRef, Patch, PatchError } from "@valbuild/core/patch";
4
4
  import { ValSyntaxError, ValSyntaxErrorTree } from "./patch/ts/syntax.js";
@@ -84,6 +84,15 @@ export declare abstract class ValOps {
84
84
  schemaSha: SchemaSha;
85
85
  sourcesSha: SourcesSha;
86
86
  patches: PatchId[];
87
+ /**
88
+ * Unpublished changes the store threw away because it could not read
89
+ * them. FS mode only: the content api owns its own patches and does not
90
+ * discard them behind the client's back.
91
+ */
92
+ removed?: {
93
+ patchId: PatchId;
94
+ reason: string;
95
+ }[];
87
96
  /** FS mode only — see the `params` counterpart. */
88
97
  jsonEntriesSha?: string;
89
98
  } | {
@@ -225,9 +234,9 @@ export declare abstract class ValOps {
225
234
  validations: Record<SourcePath, ValidationError[]>;
226
235
  }>;
227
236
  files: Record<SourcePath, FileSource>;
228
- remoteFiles: Record<SourcePath, RemoteSource>;
237
+ remoteFiles: Record<SourcePath, MediaSource>;
229
238
  }>;
230
- validateRemoteFiles(schemas: Schemas, sources: Sources, remoteFiles: Record<SourcePath, RemoteSource>): Promise<Record<SourcePath, ValidationError[]>>;
239
+ validateRemoteFiles(schemas: Schemas, sources: Sources, remoteFiles: Record<SourcePath, MediaSource>): Promise<Record<SourcePath, ValidationError[]>>;
231
240
  validateFiles(schemas: Schemas, sources: Sources, files: Record<SourcePath, FileSource>, fileLastUpdatedByPatchId?: PatchAnalysis["fileLastUpdatedByPatchId"]): Promise<Record<SourcePath, ValidationError[]>>;
232
241
  /**
233
242
  * Applies the pending patches to the source files so they can be committed.
@@ -1,7 +1,6 @@
1
1
  import { PatchId, ModuleFilePath, ValModules } from "@valbuild/core";
2
- import { AuthorId, BaseSha, BinaryFileType, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata, PatchReadError, SourcesSha } from "./ValOps.js";
2
+ import { AuthorId, BaseSha, BinaryFileType, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata, SourcesSha } from "./ValOps.js";
3
3
  import { Patch, ParentRef, ValCommit } from "@valbuild/shared/internal";
4
- import { ParentPatchId } from "@valbuild/core";
5
4
  import { Buffer } from "buffer";
6
5
  export declare class ValOpsFS extends ValOps {
7
6
  private readonly contentUrl;
@@ -14,6 +13,20 @@ export declare class ValOpsFS extends ValOps {
14
13
  * content lives behind a thunk that `JSON.stringify` drops).
15
14
  */
16
15
  private readonly jsonEntryFilesFingerprint;
16
+ /**
17
+ * Unpublished changes repair threw away, waiting to be told to somebody.
18
+ *
19
+ * Held here and drained by the next `getStat`, because stat is the channel
20
+ * that always flows. The case worth reporting is a repair that removed
21
+ * EVERYTHING - which is what an old store looks like - and then there is
22
+ * nothing left for the studio to fetch, so a notice riding on `fetchPatches`
23
+ * would never be collected. Drained rather than kept, because this has to be
24
+ * said once: a permanent flag would put a toast on the screen forever.
25
+ *
26
+ * Held in memory only: a restart loses it, and that is the right trade. The
27
+ * durable record is `patches.repair.log`.
28
+ */
29
+ private readonly removedPatchNotices;
17
30
  onInit(): Promise<void>;
18
31
  getPresignedAuthNonce(project: string, corsOrigin: string, auth: {
19
32
  pat: string;
@@ -50,6 +63,10 @@ export declare class ValOpsFS extends ValOps {
50
63
  schemaSha: SchemaSha;
51
64
  sourcesSha: SourcesSha;
52
65
  patches: PatchId[];
66
+ removed?: {
67
+ patchId: PatchId;
68
+ reason: string;
69
+ }[];
53
70
  jsonEntriesSha?: string;
54
71
  } | {
55
72
  type: "use-websocket";
@@ -67,23 +84,39 @@ export declare class ValOpsFS extends ValOps {
67
84
  unauthorized?: boolean;
68
85
  networkError?: boolean;
69
86
  }>;
70
- private readPatches;
71
- getParentPatchIdFromParentRef(parentRef: ParentRef): ParentPatchId;
87
+ /**
88
+ * The one read every other read comes out of.
89
+ *
90
+ * `getStat` announces from this and `fetchPatches` delivers from this, so the
91
+ * two cannot disagree. They used to: `getStat` counted the directories on disk
92
+ * while `fetchPatches` walked the parent links between them, and when a single
93
+ * record went missing the first said 410 and the second said 359 — with no
94
+ * error anywhere, because a walk that runs out of links just stops.
95
+ *
96
+ * Anything wrong is reported, then repaired, and reset only if repair does not
97
+ * settle it. In that order, and never silently.
98
+ */
99
+ private readStore;
100
+ /**
101
+ * Report, repair, and reset only if repair did not take.
102
+ *
103
+ * Reached only when something is actually wrong, so the healthy path — which
104
+ * is every stat poll — never touches the lock.
105
+ */
106
+ private repairStore;
72
107
  fetchPatches<ExcludePatchOps extends boolean>(filters: {
73
108
  patchIds?: PatchId[];
74
109
  excludePatchOps: ExcludePatchOps;
75
110
  }): Promise<ExcludePatchOps extends true ? OrderedPatchesMetadata : OrderedPatches>;
76
- fetchPatchesFromFS<ExcludePatchOps extends boolean>(excludePatchOps: ExcludePatchOps): Promise<ExcludePatchOps extends true ? FSPatchesMetadata : FSPatches>;
77
- private createPatchChain;
78
111
  private parseJsonFile;
79
- protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, parentRef: ParentRef, authorId: AuthorId | null, sessionId: string | null): Promise<SaveSourceFilePatchResult>;
112
+ protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, _parentRef: ParentRef, authorId: AuthorId | null, sessionId: string | null): Promise<SaveSourceFilePatchResult>;
80
113
  protected getSourceFile(path: string): Promise<WithGenericError<{
81
114
  data: string;
82
115
  }>>;
83
116
  protected saveSourceFile(path: ModuleFilePath, data: string): Promise<WithGenericError<{
84
117
  path: ModuleFilePath;
85
118
  }>>;
86
- saveBase64EncodedBinaryFileFromPatch(filePath: string, parentRef: ParentRef, patchId: PatchId, data: string | null, _type: BinaryFileType, metadata: MetadataOfType<BinaryFileType> | undefined): Promise<WithGenericError<{
119
+ saveBase64EncodedBinaryFileFromPatch(filePath: string, _parentRef: ParentRef, patchId: PatchId, data: string | null, _type: BinaryFileType, metadata: MetadataOfType<BinaryFileType> | undefined): Promise<WithGenericError<{
87
120
  patchId: PatchId;
88
121
  filePath: string;
89
122
  }>>;
@@ -104,7 +137,6 @@ export declare class ValOpsFS extends ValOps {
104
137
  deleteAllPatches(): Promise<{
105
138
  error?: GenericErrorMessage;
106
139
  }>;
107
- private updateOrderedPatches;
108
140
  saveOrUploadFiles(preparedCommit: PreparedCommit, mode: "skip-remote" | "upload-remote", auth?: {
109
141
  apiKey: string;
110
142
  } | {
@@ -118,33 +150,10 @@ export declare class ValOpsFS extends ValOps {
118
150
  }>;
119
151
  getBinaryFile(filePath: string): Promise<Buffer | null>;
120
152
  protected getBinaryFileMetadata<T extends BinaryFileType>(filePath: string, type: T): Promise<OpsMetadata<T>>;
121
- private getParentPatchIdFromPatchId;
122
- private getParentPatchIdFromPatchIdMap;
123
153
  private getPatchesDir;
124
- private getFullPatchDir;
125
- private getBinaryFilePath;
126
- private getBinaryFileMetadataPath;
127
- private getPatchFilePath;
128
- private getPatchBaseFile;
154
+ /**
155
+ * Deliberately outside the patches directory: delete-all and reset rename that
156
+ * whole directory, and a lock that moves away with it is not holding anything.
157
+ */
158
+ private getPatchLockFile;
129
159
  }
130
- type FSPatches = {
131
- patches: Record<PatchId, {
132
- path: ModuleFilePath;
133
- patch: Patch;
134
- parentRef: ParentRef;
135
- createdAt: string;
136
- authorId: AuthorId | null;
137
- baseSha: BaseSha;
138
- appliedAt: null;
139
- }>;
140
- error?: GenericErrorMessage;
141
- errors?: PatchReadError[];
142
- };
143
- type FSPatchesMetadata = {
144
- patches: Record<PatchId, Omit<FSPatches["patches"][PatchId], "patch"> & {
145
- patch?: undefined;
146
- }>;
147
- error?: GenericErrorMessage;
148
- errors?: FSPatches["errors"];
149
- };
150
- export {};
@@ -1,8 +1,19 @@
1
1
  import { FileMetadata, ImageMetadata, SerializedSchema, Source, SourcePath, ValidationError } from "@valbuild/core";
2
- import { Patch } from "@valbuild/core/patch";
2
+ import { JSONValue, Patch } from "@valbuild/core/patch";
3
3
  export type FixPatchRemainingError = ValidationError & {
4
4
  sourcePath?: SourcePath;
5
5
  };
6
+ /**
7
+ * A whole media value, for the fixes that replace one outright.
8
+ *
9
+ * The three remote fixes each wrote this shape out by hand, and one of them was
10
+ * left behind when the shape changed. One name, so the next change cannot miss a
11
+ * site.
12
+ *
13
+ * `path` last is deliberate: it is the field the fix is about, and a stray
14
+ * `path` inside the metadata must not win over it.
15
+ */
16
+ export declare function mediaValue(path: string, metadata: unknown): Record<string, JSONValue>;
6
17
  export declare function createFixPatch(config: {
7
18
  projectRoot: string;
8
19
  remoteHost: string;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The fix handlers behind `val validate --fix`, and the editor quick fixes that
3
+ * must agree with it.
4
+ *
5
+ * ## Why this lives in `@valbuild/server`
6
+ *
7
+ * A fix is two layers. `createFixPatch` is the second: given a validation
8
+ * error it produces a patch. The first is everything that has to happen before a
9
+ * patch can be produced — read the file, check it is actually on disk, extract
10
+ * metadata, pick a remote bucket, upload the bytes, download them back. That
11
+ * layer used to live in `packages/cli/src/runValidation.ts`, which made it
12
+ * reachable only from the CLI: `@valbuild/cli` exports nothing but `./cli`, and
13
+ * `@valbuild/language-server` cannot depend on it anyway without creating a
14
+ * cycle (the CLI depends on the language server).
15
+ *
16
+ * The consequence was that an editor could offer the fixes needing no
17
+ * precondition and had to reimplement or skip the rest. Both happened: the
18
+ * VS Code extension grew its own remote-upload client, and the language server
19
+ * declined to offer remote fixes at all.
20
+ *
21
+ * So the layer sits here, next to `createFixPatch`, and the CLI and the language
22
+ * server are both callers. {@link FixHandlerContext} is deliberately made of
23
+ * things any caller already has — a `Service`, an `IValFSHost`, a project root —
24
+ * rather than of CLI concepts.
25
+ *
26
+ * What stays in the CLI is the driver: the `runValidation` async generator that
27
+ * walks every module, decides what to report, and renders it to a terminal.
28
+ */
29
+ import { ModuleFilePath, SourcePath, ValidationFix } from "@valbuild/core";
30
+ import type { Service } from "./Service.js";
31
+ import type { IValFSHost } from "./ValFSHost.js";
32
+ export type { IValFSHost };
33
+ export type IValRemote = {
34
+ remoteHost: string;
35
+ getSettings(projectName: string, options: {
36
+ pat: string;
37
+ }): Promise<{
38
+ success: true;
39
+ data: {
40
+ publicProjectId: string;
41
+ remoteFileBuckets: {
42
+ bucket: string;
43
+ }[];
44
+ };
45
+ } | {
46
+ success: false;
47
+ message: string;
48
+ }>;
49
+ uploadFile(project: string, bucket: string, fileHash: string, fileExt: string | undefined, fileBuffer: Buffer, options: {
50
+ pat: string;
51
+ }): Promise<{
52
+ success: true;
53
+ } | {
54
+ success: false;
55
+ error: string;
56
+ }>;
57
+ };
58
+ export type ValModule = Awaited<ReturnType<Service["get"]>>;
59
+ export type ValidationError = {
60
+ message: string;
61
+ value?: unknown;
62
+ fixes?: ValidationFix[];
63
+ keyError?: boolean;
64
+ };
65
+ export type FixHandlerContext = {
66
+ sourcePath: SourcePath;
67
+ validationError: ValidationError;
68
+ valModule: ValModule;
69
+ projectRoot: string;
70
+ fix: boolean;
71
+ service: Service;
72
+ valFiles: string[];
73
+ moduleFilePath: ModuleFilePath;
74
+ file: string;
75
+ fs: IValFSHost;
76
+ remoteFiles: Record<SourcePath, {
77
+ ref: string;
78
+ metadata?: Record<string, unknown>;
79
+ }>;
80
+ publicProjectId?: string;
81
+ remoteFileBuckets?: string[];
82
+ remoteFilesCounter: number;
83
+ remote: IValRemote;
84
+ project: string | undefined;
85
+ };
86
+ export type FixHandlerResult = {
87
+ success: boolean;
88
+ errorMessage?: string;
89
+ shouldApplyPatch?: boolean;
90
+ appliedFix?: boolean;
91
+ fixableErrorMessage?: string;
92
+ publicProjectId?: string;
93
+ remoteFileBuckets?: string[];
94
+ remoteFilesCounter?: number;
95
+ events?: ValidationEvent[];
96
+ };
97
+ export type FixHandler = (ctx: FixHandlerContext) => Promise<FixHandlerResult>;
98
+ export type ValidationEvent = {
99
+ type: "file-valid";
100
+ file: string;
101
+ durationMs: number;
102
+ } | {
103
+ type: "file-error-count";
104
+ file: string;
105
+ errorCount: number;
106
+ durationMs: number;
107
+ } | {
108
+ type: "validation-error";
109
+ sourcePath: string;
110
+ message: string;
111
+ keyError?: boolean;
112
+ } | {
113
+ type: "validation-fixable-error";
114
+ sourcePath: string;
115
+ message: string;
116
+ fixable: boolean;
117
+ keyError?: boolean;
118
+ } | {
119
+ type: "unknown-fix";
120
+ sourcePath: string;
121
+ fixes: string[];
122
+ keyError?: boolean;
123
+ } | {
124
+ type: "unregistered-module";
125
+ file: string;
126
+ } | {
127
+ type: "fix-applied";
128
+ file: string;
129
+ sourcePath: string;
130
+ } | {
131
+ type: "fatal-error";
132
+ file: string;
133
+ message: string;
134
+ } | {
135
+ type: "remote-uploading";
136
+ ref: string;
137
+ } | {
138
+ type: "remote-uploaded";
139
+ ref: string;
140
+ } | {
141
+ type: "remote-already-uploaded";
142
+ filePath: string;
143
+ } | {
144
+ type: "remote-downloading";
145
+ sourcePath: string;
146
+ } | {
147
+ type: "summary-errors";
148
+ count: number;
149
+ } | {
150
+ type: "summary-success";
151
+ };
152
+ export declare function handleFileMetadata(ctx: FixHandlerContext): Promise<FixHandlerResult>;
153
+ export declare function handleRemoteFileUpload(ctx: FixHandlerContext): Promise<FixHandlerResult>;
154
+ export declare function handleRemoteGalleryFileUpload(ctx: FixHandlerContext): Promise<FixHandlerResult>;
155
+ export declare function handleRemoteFileDownload(ctx: FixHandlerContext): Promise<FixHandlerResult>;
156
+ export declare function handleRemoteFileCheck(): Promise<FixHandlerResult>;
157
+ export declare function handleUniqueFolderCheck(ctx: FixHandlerContext): Promise<FixHandlerResult>;
158
+ export declare function handleCheckAllFiles(ctx: FixHandlerContext): Promise<FixHandlerResult>;
159
+ export declare function handleJsonValuesExtractEntry(ctx: FixHandlerContext): Promise<FixHandlerResult>;
160
+ export declare const currentFixHandlers: Record<Exclude<ValidationFix, "keyof:check-keys" | "router:check-route">, FixHandler>;
161
+ export declare const fixHandlers: Record<string, FixHandler>;
162
+ export declare function createDefaultValFSHost(): IValFSHost;
@@ -1,2 +1,8 @@
1
1
  import { ValidationError } from "@valbuild/core";
2
+ /**
3
+ * The media path a validation error is about, if it is about media at all.
4
+ *
5
+ * There is no schema in hand here — a `ValidationError` carries only the value
6
+ * it flagged — so the shape is all there is to go on.
7
+ */
2
8
  export declare function getValidationErrorFileRef(validationError: ValidationError): string | null;
@@ -11,6 +11,8 @@ export { formatSyntaxErrorTree } from "./patch/ts/syntax.js";
11
11
  export { analyzeValModule } from "./patch/ts/valModule.js";
12
12
  export type { ValModuleAnalysis } from "./patch/ts/valModule.js";
13
13
  export { createFixPatch } from "./createFixPatch.js";
14
+ export { fixHandlers, currentFixHandlers, createDefaultValFSHost, handleFileMetadata, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleRemoteFileDownload, handleRemoteFileCheck, handleUniqueFolderCheck, handleCheckAllFiles, handleJsonValuesExtractEntry, } from "./fixHandlers.js";
15
+ export type { FixHandler, FixHandlerContext, FixHandlerResult, IValRemote, ValidationEvent, ValidationError, ValModule, } from "./fixHandlers.js";
14
16
  export * from "./jwt.js";
15
17
  export type { ValServer } from "./ValServer.js";
16
18
  export { getSettings } from "./getSettings.js";
@@ -36,3 +38,10 @@ export { formatPatchSourceError } from "./ValOps.js";
36
38
  export { compareWithCapturedReport, readCapturedReport, replaySnapshot, } from "./debug/replaySnapshot.js";
37
39
  export type { ReplayComparison, ReplayResult } from "./debug/replaySnapshot.js";
38
40
  export type { OrderedPatches, PatchAnalysis, PatchSourceError, PreparedCommit, } from "./ValOps.js";
41
+ /**
42
+ * The local-dev patch store, exported so the CLI's debug tooling can read a
43
+ * snapshot back with the same code the server uses rather than a second
44
+ * implementation of the layout.
45
+ */
46
+ export { readPatchStore, describePatchStoreProblems } from "./patchStore.js";
47
+ export type { PatchStoreEntry, PatchStoreProblem, ReadPatchStoreResult, } from "./patchStore.js";
@@ -37,19 +37,5 @@ export declare function deepValidateExpression(value: ts.Expression): result.Res
37
37
  */
38
38
  export declare function evaluateExpression(value: ts.Expression): result.Result<JSONValue, ValSyntaxErrorTree>;
39
39
  export declare function findObjectPropertyAssignment(value: ts.ObjectLiteralExpression, key: string): result.Result<LiteralPropertyAssignment | undefined, ValSyntaxErrorTree>;
40
- export declare function isValFileMethodCall(node: ts.Expression): node is ts.CallExpression;
41
- export declare function isValImageMethodCall(node: ts.Expression): node is ts.CallExpression;
42
- export declare function isValRemoteMethodCall(node: ts.Expression): node is ts.CallExpression;
43
- export declare function findValFileNodeArg(node: ts.CallExpression): result.Result<ts.StringLiteral, ValSyntaxErrorTree>;
44
- export declare function findValRemoteNodeArg(node: ts.CallExpression): result.Result<ts.StringLiteral, ValSyntaxErrorTree>;
45
- export declare function findValImageNodeArg(node: ts.CallExpression): result.Result<ts.StringLiteral, ValSyntaxErrorTree>;
46
- export declare function findValFileMetadataArg(node: ts.CallExpression): result.Result<ts.ObjectLiteralExpression | undefined, ValSyntaxErrorTree>;
47
- export declare function findValImageMetadataArg(node: ts.CallExpression): result.Result<ts.ObjectLiteralExpression | undefined, ValSyntaxErrorTree>;
48
- export declare function findValRemoteMetadataArg(node: ts.CallExpression): result.Result<ts.ObjectLiteralExpression | undefined, ValSyntaxErrorTree>;
49
- /**
50
- * Given a list of expressions, validates that all the expressions are not
51
- * spread elements. In other words, it ensures that the expressions are the
52
- * initializers of the values at their respective indices in the evaluated list.
53
- */
54
40
  export declare function validateInitializers(nodes: ReadonlyArray<ts.Expression>): ValSyntaxErrorTree | undefined;
55
41
  export {};
@@ -0,0 +1,124 @@
1
+ import { ModuleFilePath, PatchId } from "@valbuild/core";
2
+ /**
3
+ * The order of the pending patches, held in one flat, append-only text file.
4
+ *
5
+ * ## Why a log and not links between the patches
6
+ *
7
+ * The store this replaces kept the order in the patches themselves: every record
8
+ * carried a `parentRef`, and the directory a record lived in was named after that
9
+ * parent. Reading the chain meant walking those links from `head`, and the walk
10
+ * stopped dead — silently — at the first id nothing on disk answered for.
11
+ *
12
+ * That is not a hypothetical. A store with 410 patches lost exactly one record,
13
+ * and the 51 patches written after it became unreachable: `/stat` counted the
14
+ * directories and announced 410, `GET /patches` walked the links and delivered
15
+ * 359, and the studio waited forever for 51 ids that no longer had a path back to
16
+ * `head`. One missing file cost every edit made after it.
17
+ *
18
+ * So the order lives in one place and the patches reference nothing. A patch
19
+ * directory is self-contained data; this file says what order the directories go
20
+ * in. Removing a line cannot orphan the lines below it, because there is nothing
21
+ * below it to orphan — the entry after a dropped one is simply next.
22
+ *
23
+ * ## Why text
24
+ *
25
+ * It is read by people during exactly the incidents that make it interesting, and
26
+ * `cat` should be enough. One entry per line, whitespace-separated, id first:
27
+ *
28
+ * ```
29
+ * val-patch-log v1
30
+ * 659b8cfa-065c-47d1-8d82-fc69a2ac72a9 2026-08-27T11:46:13.730Z /content/authors.val.ts
31
+ * ```
32
+ *
33
+ * Position in the file IS the order. There is deliberately no sequence number:
34
+ * a number stored beside the thing it describes is a number that can disagree
35
+ * with it, and then something has to decide which one lies.
36
+ *
37
+ * The timestamp and path are there to make the file readable, not to be believed
38
+ * — `patch.json` owns those fields. Nothing here is a second copy of state that
39
+ * anything reads back.
40
+ */
41
+ export declare const PATCH_LOG_FILE_NAME = "patches.log";
42
+ export type PatchLogEntry = {
43
+ patchId: PatchId;
44
+ createdAt: string;
45
+ path: ModuleFilePath;
46
+ };
47
+ /**
48
+ * Something that is wrong with the file but does not stop it being read.
49
+ *
50
+ * Reported rather than thrown, because a log that is 99% intact is worth reading
51
+ * and then repairing. Only a file that cannot be understood at all is fatal, and
52
+ * that is a `status` on the read result, not a problem in this list.
53
+ */
54
+ export type PatchLogProblem = {
55
+ type: "missing-header";
56
+ /** What stood where the header should have been. */
57
+ firstLine: string;
58
+ } | {
59
+ type: "unsupported-version";
60
+ header: string;
61
+ } | {
62
+ type: "unparseable-line";
63
+ lineNumber: number;
64
+ line: string;
65
+ } | {
66
+ type: "duplicate-entry";
67
+ patchId: PatchId;
68
+ lineNumber: number;
69
+ }
70
+ /**
71
+ * A final line with no newline after it: a write that did not finish.
72
+ *
73
+ * Discarded rather than guessed at, and only ever possible on the LAST line —
74
+ * appends are serialized by the patch lock, so no other writer can have got in
75
+ * behind an interrupted one.
76
+ */
77
+ | {
78
+ type: "torn-final-line";
79
+ line: string;
80
+ };
81
+ export type ReadPatchLogResult = {
82
+ status: "ok";
83
+ entries: PatchLogEntry[];
84
+ problems: PatchLogProblem[];
85
+ }
86
+ /** No log file at all — an empty store, not a broken one. */
87
+ | {
88
+ status: "absent";
89
+ } | {
90
+ status: "unreadable";
91
+ message: string;
92
+ };
93
+ export declare function formatPatchLogLine(entry: PatchLogEntry): string;
94
+ /**
95
+ * Split on the first two runs of whitespace only: a module file path may contain
96
+ * spaces, and it is last precisely so that it can.
97
+ */
98
+ export declare function parsePatchLogLine(line: string): PatchLogEntry | null;
99
+ export declare function serializePatchLog(entries: readonly PatchLogEntry[]): string;
100
+ export declare function readPatchLog(logFilePath: string): ReadPatchLogResult;
101
+ export declare function parsePatchLog(raw: string): ReadPatchLogResult;
102
+ /**
103
+ * Add one entry to the end of the log.
104
+ *
105
+ * A single `writeSync` on an append-only descriptor, then fsync: the whole line
106
+ * reaches the file or none of it does, and a reader that catches it mid-flight
107
+ * discards the partial last line rather than misreading it.
108
+ *
109
+ * Callers must hold the patch lock. That is what makes "the torn line can only be
110
+ * the last one" true, and it is why this does not try to be safe against
111
+ * concurrent appends on its own.
112
+ */
113
+ export declare function appendPatchLogEntry(logFilePath: string, entry: PatchLogEntry): void;
114
+ /**
115
+ * Replace the log wholesale — used by delete and by repair.
116
+ *
117
+ * Write a sibling temp file, fsync it, then rename over the original: a rename is
118
+ * atomic, so a reader either sees the old log or the new one and never a
119
+ * half-rewritten file. In-place truncation would have a window where the log is
120
+ * short and the store looks like it lost patches.
121
+ *
122
+ * Callers must hold the patch lock.
123
+ */
124
+ export declare function writePatchLogFile(logFilePath: string, entries: readonly PatchLogEntry[]): void;