@valbuild/cli 0.103.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.
@@ -1500,8 +1500,11 @@ async function buildSnapshot(ctx, options = {}) {
1500
1500
  path: p
1501
1501
  });
1502
1502
  }
1503
- entries[`.val/patches/${patch.parentPatchId ?? "head"}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
1503
+ entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
1504
1504
  }
1505
+ // The order lives in the log, not in the records, so it has to be written too
1506
+ // or the unzipped snapshot is a store with no order at all.
1507
+ entries[".val/patches/patches.log"] = ["val-patch-log v1", ...patches.map(patch => `${patch.patchId} ${patch.createdAt} ${patch.path}`), ""].join("\n");
1505
1508
  let includesBinaryFiles = false;
1506
1509
  if (options.includeFiles) {
1507
1510
  includesBinaryFiles = await writeBinaryFiles(ctx, analysis.fileLastUpdatedByPatchId, patches, entries);
@@ -1568,34 +1571,29 @@ async function validateSnapshotSources(ctx, analysis) {
1568
1571
  return validationRes.errors;
1569
1572
  }
1570
1573
  function toSnapshotPatches(orderedPatches) {
1571
- return orderedPatches.map((patch, i) => ({
1574
+ return orderedPatches.map(patch => ({
1572
1575
  patchId: patch.patchId,
1573
1576
  path: patch.path,
1574
1577
  createdAt: patch.createdAt,
1575
1578
  authorId: patch.authorId,
1576
1579
  baseSha: patch.baseSha,
1577
1580
  appliedAt: patch.appliedAt,
1578
- parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
1579
1581
  patch: patch.patch
1580
1582
  }));
1581
1583
  }
1582
1584
 
1583
1585
  /**
1584
1586
  * The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
1585
- * a plain ValOpsFS can read. The directory is named after the PARENT, and
1586
- * createPatchChain walks the linked list from "head".
1587
+ * a plain ValOpsFS can read.
1588
+ *
1589
+ * The directory is named after the patch itself and the record points at
1590
+ * nothing: the order is in `patches.log` beside it. A record that named its
1591
+ * parent is how a snapshot could carry a chain that had already lost a link.
1587
1592
  */
1588
1593
  function toFsPatch(patch) {
1589
1594
  return {
1590
1595
  patch: patch.patch,
1591
1596
  patchId: patch.patchId,
1592
- parentRef: patch.parentPatchId === null ? {
1593
- type: "head",
1594
- headBaseSha: patch.baseSha
1595
- } : {
1596
- type: "patch",
1597
- patchId: patch.parentPatchId
1598
- },
1599
1597
  path: patch.path,
1600
1598
  authorId: patch.authorId,
1601
1599
  sessionId: null,
@@ -1605,14 +1603,13 @@ function toFsPatch(patch) {
1605
1603
  };
1606
1604
  }
1607
1605
  async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries) {
1608
- const parentByPatchId = new Map(patches.map(p => [p.patchId, p.parentPatchId ?? "head"]));
1606
+ const known = new Set(patches.map(p => p.patchId));
1609
1607
  let wrote = false;
1610
1608
  for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
1611
1609
  if (data.isDelete) {
1612
1610
  continue;
1613
1611
  }
1614
- const parentPatchId = parentByPatchId.get(data.patchId);
1615
- if (parentPatchId === undefined) {
1612
+ if (!known.has(data.patchId)) {
1616
1613
  continue;
1617
1614
  }
1618
1615
  const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, data.patchId, data.remote);
@@ -1620,7 +1617,7 @@ async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries)
1620
1617
  continue;
1621
1618
  }
1622
1619
  // Base64 so the snapshot stays a text-only entry map; the replay decodes it.
1623
- entries[`.val/patches/${parentPatchId}/files${filePath}/${path__default["default"].posix.basename(filePath)}.base64`] = buffer.toString("base64");
1620
+ entries[`.val/patches/${data.patchId}/files${filePath}/${path__default["default"].posix.basename(filePath)}.base64`] = buffer.toString("base64");
1624
1621
  wrote = true;
1625
1622
  }
1626
1623
  return wrote;
@@ -1500,8 +1500,11 @@ async function buildSnapshot(ctx, options = {}) {
1500
1500
  path: p
1501
1501
  });
1502
1502
  }
1503
- entries[`.val/patches/${patch.parentPatchId ?? "head"}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
1503
+ entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
1504
1504
  }
1505
+ // The order lives in the log, not in the records, so it has to be written too
1506
+ // or the unzipped snapshot is a store with no order at all.
1507
+ entries[".val/patches/patches.log"] = ["val-patch-log v1", ...patches.map(patch => `${patch.patchId} ${patch.createdAt} ${patch.path}`), ""].join("\n");
1505
1508
  let includesBinaryFiles = false;
1506
1509
  if (options.includeFiles) {
1507
1510
  includesBinaryFiles = await writeBinaryFiles(ctx, analysis.fileLastUpdatedByPatchId, patches, entries);
@@ -1568,34 +1571,29 @@ async function validateSnapshotSources(ctx, analysis) {
1568
1571
  return validationRes.errors;
1569
1572
  }
1570
1573
  function toSnapshotPatches(orderedPatches) {
1571
- return orderedPatches.map((patch, i) => ({
1574
+ return orderedPatches.map(patch => ({
1572
1575
  patchId: patch.patchId,
1573
1576
  path: patch.path,
1574
1577
  createdAt: patch.createdAt,
1575
1578
  authorId: patch.authorId,
1576
1579
  baseSha: patch.baseSha,
1577
1580
  appliedAt: patch.appliedAt,
1578
- parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
1579
1581
  patch: patch.patch
1580
1582
  }));
1581
1583
  }
1582
1584
 
1583
1585
  /**
1584
1586
  * The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
1585
- * a plain ValOpsFS can read. The directory is named after the PARENT, and
1586
- * createPatchChain walks the linked list from "head".
1587
+ * a plain ValOpsFS can read.
1588
+ *
1589
+ * The directory is named after the patch itself and the record points at
1590
+ * nothing: the order is in `patches.log` beside it. A record that named its
1591
+ * parent is how a snapshot could carry a chain that had already lost a link.
1587
1592
  */
1588
1593
  function toFsPatch(patch) {
1589
1594
  return {
1590
1595
  patch: patch.patch,
1591
1596
  patchId: patch.patchId,
1592
- parentRef: patch.parentPatchId === null ? {
1593
- type: "head",
1594
- headBaseSha: patch.baseSha
1595
- } : {
1596
- type: "patch",
1597
- patchId: patch.parentPatchId
1598
- },
1599
1597
  path: patch.path,
1600
1598
  authorId: patch.authorId,
1601
1599
  sessionId: null,
@@ -1605,14 +1603,13 @@ function toFsPatch(patch) {
1605
1603
  };
1606
1604
  }
1607
1605
  async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries) {
1608
- const parentByPatchId = new Map(patches.map(p => [p.patchId, p.parentPatchId ?? "head"]));
1606
+ const known = new Set(patches.map(p => p.patchId));
1609
1607
  let wrote = false;
1610
1608
  for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
1611
1609
  if (data.isDelete) {
1612
1610
  continue;
1613
1611
  }
1614
- const parentPatchId = parentByPatchId.get(data.patchId);
1615
- if (parentPatchId === undefined) {
1612
+ if (!known.has(data.patchId)) {
1616
1613
  continue;
1617
1614
  }
1618
1615
  const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, data.patchId, data.remote);
@@ -1620,7 +1617,7 @@ async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries)
1620
1617
  continue;
1621
1618
  }
1622
1619
  // Base64 so the snapshot stays a text-only entry map; the replay decodes it.
1623
- entries[`.val/patches/${parentPatchId}/files${filePath}/${path__default["default"].posix.basename(filePath)}.base64`] = buffer.toString("base64");
1620
+ entries[`.val/patches/${data.patchId}/files${filePath}/${path__default["default"].posix.basename(filePath)}.base64`] = buffer.toString("base64");
1624
1621
  wrote = true;
1625
1622
  }
1626
1623
  return wrote;
@@ -1468,8 +1468,11 @@ async function buildSnapshot(ctx, options = {}) {
1468
1468
  path: p
1469
1469
  });
1470
1470
  }
1471
- entries[`.val/patches/${patch.parentPatchId ?? "head"}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
1471
+ entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
1472
1472
  }
1473
+ // The order lives in the log, not in the records, so it has to be written too
1474
+ // or the unzipped snapshot is a store with no order at all.
1475
+ entries[".val/patches/patches.log"] = ["val-patch-log v1", ...patches.map(patch => `${patch.patchId} ${patch.createdAt} ${patch.path}`), ""].join("\n");
1473
1476
  let includesBinaryFiles = false;
1474
1477
  if (options.includeFiles) {
1475
1478
  includesBinaryFiles = await writeBinaryFiles(ctx, analysis.fileLastUpdatedByPatchId, patches, entries);
@@ -1536,34 +1539,29 @@ async function validateSnapshotSources(ctx, analysis) {
1536
1539
  return validationRes.errors;
1537
1540
  }
1538
1541
  function toSnapshotPatches(orderedPatches) {
1539
- return orderedPatches.map((patch, i) => ({
1542
+ return orderedPatches.map(patch => ({
1540
1543
  patchId: patch.patchId,
1541
1544
  path: patch.path,
1542
1545
  createdAt: patch.createdAt,
1543
1546
  authorId: patch.authorId,
1544
1547
  baseSha: patch.baseSha,
1545
1548
  appliedAt: patch.appliedAt,
1546
- parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
1547
1549
  patch: patch.patch
1548
1550
  }));
1549
1551
  }
1550
1552
 
1551
1553
  /**
1552
1554
  * The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
1553
- * a plain ValOpsFS can read. The directory is named after the PARENT, and
1554
- * createPatchChain walks the linked list from "head".
1555
+ * a plain ValOpsFS can read.
1556
+ *
1557
+ * The directory is named after the patch itself and the record points at
1558
+ * nothing: the order is in `patches.log` beside it. A record that named its
1559
+ * parent is how a snapshot could carry a chain that had already lost a link.
1555
1560
  */
1556
1561
  function toFsPatch(patch) {
1557
1562
  return {
1558
1563
  patch: patch.patch,
1559
1564
  patchId: patch.patchId,
1560
- parentRef: patch.parentPatchId === null ? {
1561
- type: "head",
1562
- headBaseSha: patch.baseSha
1563
- } : {
1564
- type: "patch",
1565
- patchId: patch.parentPatchId
1566
- },
1567
1565
  path: patch.path,
1568
1566
  authorId: patch.authorId,
1569
1567
  sessionId: null,
@@ -1573,14 +1571,13 @@ function toFsPatch(patch) {
1573
1571
  };
1574
1572
  }
1575
1573
  async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries) {
1576
- const parentByPatchId = new Map(patches.map(p => [p.patchId, p.parentPatchId ?? "head"]));
1574
+ const known = new Set(patches.map(p => p.patchId));
1577
1575
  let wrote = false;
1578
1576
  for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
1579
1577
  if (data.isDelete) {
1580
1578
  continue;
1581
1579
  }
1582
- const parentPatchId = parentByPatchId.get(data.patchId);
1583
- if (parentPatchId === undefined) {
1580
+ if (!known.has(data.patchId)) {
1584
1581
  continue;
1585
1582
  }
1586
1583
  const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, data.patchId, data.remote);
@@ -1588,7 +1585,7 @@ async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries)
1588
1585
  continue;
1589
1586
  }
1590
1587
  // Base64 so the snapshot stays a text-only entry map; the replay decodes it.
1591
- entries[`.val/patches/${parentPatchId}/files${filePath}/${path.posix.basename(filePath)}.base64`] = buffer.toString("base64");
1588
+ entries[`.val/patches/${data.patchId}/files${filePath}/${path.posix.basename(filePath)}.base64`] = buffer.toString("base64");
1592
1589
  wrote = true;
1593
1590
  }
1594
1591
  return wrote;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valbuild/cli",
3
3
  "private": false,
4
- "version": "0.103.0",
4
+ "version": "0.103.1",
5
5
  "description": "Val CLI tools",
6
6
  "repository": {
7
7
  "type": "git",
@@ -29,8 +29,8 @@
29
29
  "zod": "^4.3.5",
30
30
  "@valbuild/core": "0.103.0",
31
31
  "@valbuild/eslint-plugin": "0.93.0",
32
- "@valbuild/language-server": "0.103.0",
33
- "@valbuild/server": "0.103.0",
32
+ "@valbuild/language-server": "0.103.1",
33
+ "@valbuild/server": "0.103.1",
34
34
  "@valbuild/shared": "0.103.0"
35
35
  },
36
36
  "peerDependencies": {
@@ -1,19 +1,16 @@
1
1
  {
2
2
  "patch": [
3
3
  {
4
- "op": "remove",
5
- "path": ["BBL", "summary", "services", "2"]
4
+ "op": "replace",
5
+ "path": ["BBL", "title"],
6
+ "value": "BBL Housing"
6
7
  }
7
8
  ],
8
- "patchId": "22222222-2222-4222-8222-222222222222",
9
- "parentRef": {
10
- "type": "patch",
11
- "patchId": "11111111-1111-4111-8111-111111111111"
12
- },
9
+ "patchId": "11111111-1111-4111-8111-111111111111",
13
10
  "path": "/content/projects.val.ts",
14
- "authorId": "author-b",
11
+ "authorId": "author-a",
15
12
  "sessionId": null,
16
13
  "baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
17
14
  "coreVersion": null,
18
- "createdAt": "2026-08-11T09:00:00.000Z"
15
+ "createdAt": "2026-08-10T12:00:00.000Z"
19
16
  }
@@ -1,20 +1,15 @@
1
1
  {
2
2
  "patch": [
3
3
  {
4
- "op": "replace",
5
- "path": ["BBL", "summary", "services", "2"],
6
- "value": "Development U"
4
+ "op": "remove",
5
+ "path": ["BBL", "summary", "services", "2"]
7
6
  }
8
7
  ],
9
- "patchId": "33333333-3333-4333-8333-333333333333",
10
- "parentRef": {
11
- "type": "patch",
12
- "patchId": "22222222-2222-4222-8222-222222222222"
13
- },
8
+ "patchId": "22222222-2222-4222-8222-222222222222",
14
9
  "path": "/content/projects.val.ts",
15
- "authorId": "author-a",
10
+ "authorId": "author-b",
16
11
  "sessionId": null,
17
12
  "baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
18
13
  "coreVersion": null,
19
- "createdAt": "2026-08-11T09:00:05.000Z"
14
+ "createdAt": "2026-08-11T09:00:00.000Z"
20
15
  }
@@ -0,0 +1,16 @@
1
+ {
2
+ "patch": [
3
+ {
4
+ "op": "replace",
5
+ "path": ["BBL", "summary", "services", "2"],
6
+ "value": "Development U"
7
+ }
8
+ ],
9
+ "patchId": "33333333-3333-4333-8333-333333333333",
10
+ "path": "/content/projects.val.ts",
11
+ "authorId": "author-a",
12
+ "sessionId": null,
13
+ "baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
14
+ "coreVersion": null,
15
+ "createdAt": "2026-08-11T09:00:05.000Z"
16
+ }
@@ -0,0 +1,4 @@
1
+ val-patch-log v1
2
+ 11111111-1111-4111-8111-111111111111 2026-08-10T12:00:00.000Z /content/projects.val.ts
3
+ 22222222-2222-4222-8222-222222222222 2026-08-11T09:00:00.000Z /content/projects.val.ts
4
+ 33333333-3333-4333-8333-333333333333 2026-08-11T09:00:05.000Z /content/projects.val.ts
@@ -22,8 +22,6 @@ export type SnapshotPatch = {
22
22
  authorId: string | null;
23
23
  baseSha: string;
24
24
  appliedAt: { commitSha: string } | null;
25
- /** Parent in the chain: null means it is the first ("head"). */
26
- parentPatchId: PatchId | null;
27
25
  patch: unknown;
28
26
  };
29
27
 
@@ -199,9 +197,21 @@ export async function buildSnapshot(
199
197
  for (const p of elided.elided) {
200
198
  elidedPatchValues.push({ patchId: patch.patchId, path: p });
201
199
  }
202
- entries[`.val/patches/${patch.parentPatchId ?? "head"}/patch.json`] =
203
- JSON.stringify(toFsPatch(patch), null, 2);
200
+ entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(
201
+ toFsPatch(patch),
202
+ null,
203
+ 2,
204
+ );
204
205
  }
206
+ // The order lives in the log, not in the records, so it has to be written too
207
+ // or the unzipped snapshot is a store with no order at all.
208
+ entries[".val/patches/patches.log"] = [
209
+ "val-patch-log v1",
210
+ ...patches.map(
211
+ (patch) => `${patch.patchId} ${patch.createdAt} ${patch.path}`,
212
+ ),
213
+ "",
214
+ ].join("\n");
205
215
 
206
216
  let includesBinaryFiles = false;
207
217
  if (options.includeFiles) {
@@ -296,31 +306,29 @@ function toSnapshotPatches(
296
306
  appliedAt: { commitSha: string } | null;
297
307
  }[],
298
308
  ): SnapshotPatch[] {
299
- return orderedPatches.map((patch, i) => ({
309
+ return orderedPatches.map((patch) => ({
300
310
  patchId: patch.patchId,
301
311
  path: patch.path,
302
312
  createdAt: patch.createdAt,
303
313
  authorId: patch.authorId,
304
314
  baseSha: patch.baseSha,
305
315
  appliedAt: patch.appliedAt,
306
- parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
307
316
  patch: patch.patch,
308
317
  }));
309
318
  }
310
319
 
311
320
  /**
312
321
  * The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
313
- * a plain ValOpsFS can read. The directory is named after the PARENT, and
314
- * createPatchChain walks the linked list from "head".
322
+ * a plain ValOpsFS can read.
323
+ *
324
+ * The directory is named after the patch itself and the record points at
325
+ * nothing: the order is in `patches.log` beside it. A record that named its
326
+ * parent is how a snapshot could carry a chain that had already lost a link.
315
327
  */
316
328
  function toFsPatch(patch: SnapshotPatch) {
317
329
  return {
318
330
  patch: patch.patch,
319
331
  patchId: patch.patchId,
320
- parentRef:
321
- patch.parentPatchId === null
322
- ? { type: "head", headBaseSha: patch.baseSha }
323
- : { type: "patch", patchId: patch.parentPatchId },
324
332
  path: patch.path,
325
333
  authorId: patch.authorId,
326
334
  sessionId: null,
@@ -339,16 +347,13 @@ async function writeBinaryFiles(
339
347
  patches: SnapshotPatch[],
340
348
  entries: Record<string, string>,
341
349
  ): Promise<boolean> {
342
- const parentByPatchId = new Map(
343
- patches.map((p) => [p.patchId, p.parentPatchId ?? "head"]),
344
- );
350
+ const known = new Set(patches.map((p) => p.patchId));
345
351
  let wrote = false;
346
352
  for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
347
353
  if (data.isDelete) {
348
354
  continue;
349
355
  }
350
- const parentPatchId = parentByPatchId.get(data.patchId);
351
- if (parentPatchId === undefined) {
356
+ if (!known.has(data.patchId)) {
352
357
  continue;
353
358
  }
354
359
  const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(
@@ -361,7 +366,7 @@ async function writeBinaryFiles(
361
366
  }
362
367
  // Base64 so the snapshot stays a text-only entry map; the replay decodes it.
363
368
  entries[
364
- `.val/patches/${parentPatchId}/files${filePath}/${path.posix.basename(filePath)}.base64`
369
+ `.val/patches/${data.patchId}/files${filePath}/${path.posix.basename(filePath)}.base64`
365
370
  ] = buffer.toString("base64");
366
371
  wrote = true;
367
372
  }
package/src/debug.test.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import fs from "fs";
2
+ import os from "os";
1
3
  import path from "path";
2
4
  import { ModuleFilePath } from "@valbuild/core";
5
+ import { readPatchStore } from "@valbuild/server";
3
6
  import { createDebugContext } from "./debug/context";
4
7
  import { buildSnapshot } from "./debug/snapshot";
5
8
 
@@ -71,25 +74,43 @@ describe("val debug snapshot", () => {
71
74
  expect(entries["val.modules.original.ts"]).toContain("unrelated");
72
75
  });
73
76
 
74
- test("writes the patch chain in the layout ValOpsFS reads back", async () => {
77
+ test("writes a patch store ValOpsFS can actually read back", async () => {
75
78
  const { entries } = await build();
76
79
 
77
- // The directory is named after the PARENT patch; the first is "head".
78
- const head = JSON.parse(entries[".val/patches/head/patch.json"]);
79
- expect(head.patchId).toBe("11111111-1111-4111-8111-111111111111");
80
- expect(head.parentRef.type).toBe("head");
81
- // A head ref without headBaseSha does not parse, so the snapshot would be
82
- // unreadable.
83
- expect(typeof head.parentRef.headBaseSha).toBe("string");
84
-
85
- const second = JSON.parse(
86
- entries[".val/patches/11111111-1111-4111-8111-111111111111/patch.json"],
87
- );
88
- expect(second.patchId).toBe("22222222-2222-4222-8222-222222222222");
89
- expect(second.parentRef).toEqual({
90
- type: "patch",
91
- patchId: "11111111-1111-4111-8111-111111111111",
92
- });
80
+ // Materialise it and read it with the real store, rather than asserting on
81
+ // path strings: "the layout ValOpsFS reads back" is the property, and a
82
+ // snapshot that merely looks right is exactly what a debug tool must not
83
+ // produce.
84
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "val-snapshot-"));
85
+ for (const [name, content] of Object.entries(entries)) {
86
+ const file = path.join(dir, name);
87
+ fs.mkdirSync(path.dirname(file), { recursive: true });
88
+ fs.writeFileSync(file, content);
89
+ }
90
+
91
+ const read = readPatchStore(path.join(dir, ".val", "patches"));
92
+ if (read.status !== "ok") {
93
+ throw new Error(`expected ok, got ${read.status}`);
94
+ }
95
+ expect(read.problems).toEqual([]);
96
+ expect(read.entries.map((entry) => entry.patchId)).toEqual([
97
+ "11111111-1111-4111-8111-111111111111",
98
+ "22222222-2222-4222-8222-222222222222",
99
+ "33333333-3333-4333-8333-333333333333",
100
+ ]);
101
+
102
+ // Each record sits under its own id and points at nothing. A record that
103
+ // named its parent is how a snapshot could carry a chain that had already
104
+ // lost a link.
105
+ for (const entry of read.entries) {
106
+ const raw = JSON.parse(
107
+ entries[`.val/patches/${entry.patchId}/patch.json`],
108
+ );
109
+ expect(raw.patchId).toBe(entry.patchId);
110
+ expect(raw).not.toHaveProperty("parentRef");
111
+ }
112
+
113
+ fs.rmSync(dir, { recursive: true, force: true });
93
114
  });
94
115
 
95
116
  test("records why the snapshot may be incomplete rather than failing silently", async () => {
@@ -1,20 +0,0 @@
1
- {
2
- "patch": [
3
- {
4
- "op": "replace",
5
- "path": ["BBL", "title"],
6
- "value": "BBL Housing"
7
- }
8
- ],
9
- "patchId": "11111111-1111-4111-8111-111111111111",
10
- "parentRef": {
11
- "type": "head",
12
- "headBaseSha": "0000000000000000000000000000000000000000000000000000000000000000"
13
- },
14
- "path": "/content/projects.val.ts",
15
- "authorId": "author-a",
16
- "sessionId": null,
17
- "baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
18
- "coreVersion": null,
19
- "createdAt": "2026-08-10T12:00:00.000Z"
20
- }