@valbuild/server 0.103.0 → 0.103.2

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.
@@ -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
  } | {
@@ -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 {};
@@ -38,3 +38,10 @@ export { formatPatchSourceError } from "./ValOps.js";
38
38
  export { compareWithCapturedReport, readCapturedReport, replaySnapshot, } from "./debug/replaySnapshot.js";
39
39
  export type { ReplayComparison, ReplayResult } from "./debug/replaySnapshot.js";
40
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";
@@ -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;
@@ -0,0 +1,216 @@
1
+ import { ModuleFilePath, PatchId } from "@valbuild/core";
2
+ import { z } from "zod";
3
+ import type { AuthorId, BaseSha } from "./ValOps.js";
4
+ import { PatchLogEntry, PatchLogProblem } from "./patchLog.js";
5
+ /**
6
+ * The on-disk shape of the local-dev patch store.
7
+ *
8
+ * ```
9
+ * .val/patches/
10
+ * patches.log the order, and the only place it lives
11
+ * patches.repair.log what repair has done, for the person who has to know
12
+ * <patchId>/patch.json one plain, self-contained directory per patch
13
+ * <patchId>/base.json written when the patch is published
14
+ * <patchId>/files/… binary payloads for this patch's file ops
15
+ * ```
16
+ *
17
+ * A directory is named after the patch it holds, and a record references nothing
18
+ * outside itself. That is the whole design, and it is a direct answer to how the
19
+ * old layout failed: there, a directory was named after a record's PARENT, so
20
+ * reading the store meant following links, and one absent record silently cut off
21
+ * every patch written after it.
22
+ *
23
+ * The invariant this module exists to hold up is narrow and worth stating: **the
24
+ * announced set and the delivered set are the same array.** `getStat` and
25
+ * `fetchPatches` both come out of one `readPatchStore` call, so they cannot report
26
+ * different numbers - which is exactly what they did when this broke, announcing
27
+ * 410 patches and delivering 359 with no error in between.
28
+ */
29
+ export declare const PATCH_REPAIR_LOG_FILE_NAME = "patches.repair.log";
30
+ export declare const FSPatch: z.ZodObject<{
31
+ path: z.ZodString & z.ZodType<ModuleFilePath, string, z.core.$ZodTypeInternals<ModuleFilePath, string>>;
32
+ patch: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
33
+ op: z.ZodLiteral<"add">;
34
+ path: z.ZodArray<z.ZodString>;
35
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
36
+ }, z.core.$strict>, z.ZodObject<{
37
+ op: z.ZodLiteral<"remove">;
38
+ path: z.ZodTuple<[z.ZodString], z.ZodString>;
39
+ }, z.core.$strict>, z.ZodObject<{
40
+ op: z.ZodLiteral<"replace">;
41
+ path: z.ZodArray<z.ZodString>;
42
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
43
+ }, z.core.$strict>, z.ZodObject<{
44
+ op: z.ZodLiteral<"move">;
45
+ from: z.ZodTuple<[z.ZodString], z.ZodString>;
46
+ path: z.ZodArray<z.ZodString>;
47
+ }, z.core.$strict>, z.ZodObject<{
48
+ op: z.ZodLiteral<"copy">;
49
+ from: z.ZodArray<z.ZodString>;
50
+ path: z.ZodArray<z.ZodString>;
51
+ }, z.core.$strict>, z.ZodObject<{
52
+ op: z.ZodLiteral<"test">;
53
+ path: z.ZodArray<z.ZodString>;
54
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
55
+ }, z.core.$strict>, z.ZodObject<{
56
+ op: z.ZodLiteral<"file">;
57
+ path: z.ZodArray<z.ZodString>;
58
+ filePath: z.ZodString;
59
+ value: z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>;
60
+ remote: z.ZodBoolean;
61
+ nestedFilePath: z.ZodOptional<z.ZodArray<z.ZodString>>;
62
+ metadata: z.ZodOptional<z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>>;
63
+ }, z.core.$strict>], "op">>;
64
+ patchId: z.ZodString;
65
+ baseSha: z.ZodString & z.ZodType<BaseSha, string, z.core.$ZodTypeInternals<BaseSha, string>>;
66
+ authorId: z.ZodNullable<z.ZodString & z.ZodType<AuthorId, string, z.core.$ZodTypeInternals<AuthorId, string>>>;
67
+ createdAt: z.ZodString;
68
+ coreVersion: z.ZodNullable<z.ZodString>;
69
+ sessionId: z.ZodNullable<z.ZodString>;
70
+ }, z.core.$strip>;
71
+ export type FSPatchRecord = z.infer<typeof FSPatch>;
72
+ export declare const FSPatchBase: z.ZodObject<{
73
+ baseSha: z.ZodString & z.ZodType<BaseSha, string, z.core.$ZodTypeInternals<BaseSha, string>>;
74
+ timestamp: z.ZodString;
75
+ }, z.core.$strip>;
76
+ export type FSPatchBaseRecord = z.infer<typeof FSPatchBase>;
77
+ export type PatchStoreEntry = {
78
+ patchId: PatchId;
79
+ record: FSPatchRecord;
80
+ /** Present once the patch has been published. */
81
+ base: FSPatchBaseRecord | null;
82
+ };
83
+ /**
84
+ * Something wrong with the store that a reader can see.
85
+ *
86
+ * Every one of these used to be invisible. Reporting them is the point: the
87
+ * failure that motivated this rewrite was not that the store broke, it was that
88
+ * breaking looked exactly like working.
89
+ */
90
+ export type PatchStoreProblem = {
91
+ type: "log";
92
+ problem: PatchLogProblem;
93
+ }
94
+ /**
95
+ * A directory that cannot be used as a patch: no record, a record that does
96
+ * not parse, or one whose `patchId` is not the directory it sits in.
97
+ *
98
+ * That last case is what a store from before this layout looks like - its
99
+ * directories are named after each record's PARENT - and it is deliberately
100
+ * not special-cased. There is nothing to recover: the order lived in links
101
+ * that are exactly what goes wrong, so an old store is read as a pile of
102
+ * unusable directories and removed like any other.
103
+ *
104
+ * This is the problem the person editing is told about, because it is the one
105
+ * where unpublished work disappears.
106
+ */
107
+ | {
108
+ type: "unreadable-patch";
109
+ /** The directory name, which for a usable patch IS the patch id. */
110
+ name: string;
111
+ dir: string;
112
+ message: string;
113
+ }
114
+ /**
115
+ * A perfectly good record the log does not name.
116
+ *
117
+ * The benign half of a crash: a record is written before its log line, so an
118
+ * interrupted append leaves this behind. Nothing ever read it, so nothing is
119
+ * lost by sweeping it up, and the person editing does not need to hear about
120
+ * a patch that never existed as far as they were concerned.
121
+ */
122
+ | {
123
+ type: "orphan-directory";
124
+ name: string;
125
+ dir: string;
126
+ }
127
+ /**
128
+ * The log was gone, and the order was recovered from the records' timestamps.
129
+ *
130
+ * Reported because it is a guess. Patches written inside the same millisecond
131
+ * have no recoverable order - a real store had eight inside 20ms.
132
+ */
133
+ | {
134
+ type: "reconstructed-log";
135
+ entryCount: number;
136
+ };
137
+ export type ReadPatchStoreResult = {
138
+ status: "ok";
139
+ entries: PatchStoreEntry[];
140
+ problems: PatchStoreProblem[];
141
+ } | {
142
+ status: "unreadable";
143
+ message: string;
144
+ };
145
+ export declare function patchesLogFile(patchesDir: string): string;
146
+ export declare function patchRepairLogFile(patchesDir: string): string;
147
+ export declare function patchDir(patchesDir: string, patchId: PatchId): string;
148
+ export declare function patchRecordFile(patchesDir: string, patchId: PatchId): string;
149
+ export declare function patchBaseFile(patchesDir: string, patchId: PatchId): string;
150
+ export declare function patchBinaryFile(patchesDir: string, patchId: PatchId, filePath: string): string;
151
+ export declare function patchBinaryFileMetadata(patchesDir: string, patchId: PatchId, filePath: string): string;
152
+ /**
153
+ * Read the whole store: the order, the records, and everything wrong with it.
154
+ *
155
+ * One call, one answer. Callers that need only the ids and callers that need the
156
+ * ops both use this, which is what stops them disagreeing.
157
+ */
158
+ export declare function readPatchStore(patchesDir: string): ReadPatchStoreResult;
159
+ /** Write a patch record so that a reader sees all of it or none of it. */
160
+ export declare function writePatchRecord(patchesDir: string, patchId: PatchId, record: FSPatchRecord): void;
161
+ /**
162
+ * Add a patch to the store.
163
+ *
164
+ * Record first, then the log line, and the order is the safety property: an
165
+ * interrupted append leaves a directory nothing points at, which repair sweeps
166
+ * up. The reverse order would leave the log naming a patch that is not there -
167
+ * the exact state this whole rewrite exists to make unreachable.
168
+ *
169
+ * Callers must hold the patch lock.
170
+ */
171
+ export declare function appendPatch(patchesDir: string, record: FSPatchRecord): PatchLogEntry;
172
+ export type RepairAction = {
173
+ type: "removed-unreadable-patch";
174
+ name: string;
175
+ because: string;
176
+ } | {
177
+ type: "removed-orphan-directory";
178
+ name: string;
179
+ } | {
180
+ type: "rewrote-log";
181
+ entryCount: number;
182
+ };
183
+ /**
184
+ * Bring the store back to a state where the log and the directories agree.
185
+ *
186
+ * Safe in a way the old layout's repair could never be: the log is a flat list,
187
+ * so dropping an entry does not orphan the entries after it. There is nothing to
188
+ * re-link, which is why this can run unattended where re-parenting a chain could
189
+ * not.
190
+ *
191
+ * A patch that cannot be read is removed rather than kept around to fail again
192
+ * on every load. That does discard unpublished work, which is why
193
+ * {@link RepairAction} carries the reason, why it is written to
194
+ * `patches.repair.log`, and why the caller is expected to tell the person
195
+ * editing.
196
+ *
197
+ * Callers must hold the patch lock.
198
+ */
199
+ export declare function repairPatchStore(patchesDir: string, read: Extract<ReadPatchStoreResult, {
200
+ status: "ok";
201
+ }>): RepairAction[];
202
+ /**
203
+ * Last resort: move the whole store aside and start empty.
204
+ *
205
+ * A rename, never a delete. What is being given up on here is someone's
206
+ * unpublished work, and the least this can do is say where it went.
207
+ *
208
+ * Callers must hold the patch lock.
209
+ */
210
+ export declare function resetPatchStore(patchesDir: string, reason: string): {
211
+ movedTo: string;
212
+ } | {
213
+ error: string;
214
+ };
215
+ /** One line per problem, for a log line or an API error a person has to act on. */
216
+ export declare function describePatchStoreProblems(problems: readonly PatchStoreProblem[]): string[];