@indigoai-us/hq-cloud 6.15.19 → 6.15.21

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.
Files changed (48) hide show
  1. package/dist/bin/sync-runner.d.ts +2 -2
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +43 -8
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +77 -4
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/conflict.d.ts +25 -2
  8. package/dist/cli/conflict.d.ts.map +1 -1
  9. package/dist/cli/conflict.js +29 -4
  10. package/dist/cli/conflict.js.map +1 -1
  11. package/dist/cli/share.d.ts.map +1 -1
  12. package/dist/cli/share.js +90 -6
  13. package/dist/cli/share.js.map +1 -1
  14. package/dist/cli/share.test.js +434 -104
  15. package/dist/cli/share.test.js.map +1 -1
  16. package/dist/cli/sync.d.ts.map +1 -1
  17. package/dist/cli/sync.js +9 -1
  18. package/dist/cli/sync.js.map +1 -1
  19. package/dist/cli/sync.test.js +95 -46
  20. package/dist/cli/sync.test.js.map +1 -1
  21. package/dist/journal-delta-writes.test.d.ts +2 -0
  22. package/dist/journal-delta-writes.test.d.ts.map +1 -0
  23. package/dist/journal-delta-writes.test.js +140 -0
  24. package/dist/journal-delta-writes.test.js.map +1 -0
  25. package/dist/journal.d.ts.map +1 -1
  26. package/dist/journal.js +151 -24
  27. package/dist/journal.js.map +1 -1
  28. package/dist/journal.test.js +29 -11
  29. package/dist/journal.test.js.map +1 -1
  30. package/dist/outcome-telemetry.d.ts +7 -1
  31. package/dist/outcome-telemetry.d.ts.map +1 -1
  32. package/dist/outcome-telemetry.js +15 -9
  33. package/dist/outcome-telemetry.js.map +1 -1
  34. package/dist/outcome-telemetry.test.js +15 -0
  35. package/dist/outcome-telemetry.test.js.map +1 -1
  36. package/dist/skill-telemetry.d.ts +6 -1
  37. package/dist/skill-telemetry.d.ts.map +1 -1
  38. package/dist/skill-telemetry.js +14 -8
  39. package/dist/skill-telemetry.js.map +1 -1
  40. package/dist/skill-telemetry.test.js +43 -0
  41. package/dist/skill-telemetry.test.js.map +1 -1
  42. package/dist/telemetry.d.ts +7 -1
  43. package/dist/telemetry.d.ts.map +1 -1
  44. package/dist/telemetry.js +67 -60
  45. package/dist/telemetry.js.map +1 -1
  46. package/dist/telemetry.test.js +12 -0
  47. package/dist/telemetry.test.js.map +1 -1
  48. package/package.json +1 -1
@@ -118,7 +118,23 @@ import * as s3Module from "../s3.js";
118
118
  import { reindex } from "./reindex.js";
119
119
  import { readSyncProgress } from "../sync-progress.js";
120
120
  import { VaultAuthError } from "../vault-client.js";
121
- import { readJournal, getEntry } from "../journal.js";
121
+ import { readJournal, getEntry, listJournals } from "../journal.js";
122
+ /**
123
+ * Read a journal the way production does.
124
+ *
125
+ * These tests used to `JSON.parse` `sync-journal.<slug>.json` directly, back
126
+ * when that file held a full copy of the journal. It is now a small locator —
127
+ * the authority is the v3 state store — so the assertions below go through
128
+ * `readJournal`, which is what the sync engine itself uses. The slug is
129
+ * recovered from the path so each call site keeps naming the journal it means.
130
+ */
131
+ function journalAt(journalPath) {
132
+ const slug = path
133
+ .basename(journalPath)
134
+ .replace(/^sync-journal\./, "")
135
+ .replace(/\.json$/, "");
136
+ return readJournal(slug);
137
+ }
122
138
  const mockConfig = {
123
139
  apiUrl: "https://vault-api.test",
124
140
  authToken: "test-jwt-token",
@@ -516,13 +532,50 @@ describe("sync", () => {
516
532
  hqRoot: tmpDir,
517
533
  });
518
534
  expect(result.conflicts).toBe(1);
519
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
535
+ const journal = journalAt(journalPath);
520
536
  // remoteEtag still recorded so the conflict can't re-fire (#137 invariant)…
521
537
  expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
522
538
  // …but the kept-local copy is flagged divergent so a later currency-gated
523
539
  // delete cannot fire on a journal that lies about local==remote.
524
540
  expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
525
541
  });
542
+ // `publish-local` reaches the PULL leg too, because the strategy is set once
543
+ // for a bidirectional run. It has no meaning here — there is nothing to
544
+ // publish while deciding what to do with an incoming object — so it MUST
545
+ // degrade to `keep`. If it instead fell through to the download branch it
546
+ // would overwrite the very local body the operator elected to publish, which
547
+ // is precisely the data loss the strategy exists to prevent.
548
+ it("conflict publish-local on the pull leg keeps local and never downloads over it", async () => {
549
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
550
+ fs.mkdirSync(companyDocs, { recursive: true });
551
+ fs.writeFileSync(path.join(companyDocs, "handoff.md"), "my elected local version");
552
+ fs.writeFileSync(journalPath, JSON.stringify({
553
+ version: "1",
554
+ lastSync: new Date().toISOString(),
555
+ files: {
556
+ "docs/handoff.md": {
557
+ hash: "old-hash-from-last-sync",
558
+ size: 20,
559
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
560
+ direction: "down",
561
+ },
562
+ },
563
+ }));
564
+ const result = await sync({
565
+ company: "acme",
566
+ onConflict: "publish-local",
567
+ vaultConfig: mockConfig,
568
+ hqRoot: tmpDir,
569
+ });
570
+ expect(result.conflicts).toBe(1);
571
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("my elected local version");
572
+ // Identical bookkeeping to `keep`: the etag is stamped so the conflict
573
+ // cannot re-fire, and the honesty flag is armed so the push leg knows this
574
+ // is the divergent body it may promote under an If-Match fence.
575
+ const journal = journalAt(journalPath);
576
+ expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
577
+ expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
578
+ });
526
579
  it("a genuine download clears a stale localDiverges flag (entry is now truly local==remote)", async () => {
527
580
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
528
581
  fs.mkdirSync(companyDocs, { recursive: true });
@@ -548,7 +601,7 @@ describe("sync", () => {
548
601
  ]);
549
602
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
550
603
  expect(fs.existsSync(path.join(companyDocs, "handoff.md"))).toBe(true);
551
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
604
+ const journal = journalAt(journalPath);
552
605
  expect(journal.files["docs/handoff.md"]?.localDiverges).toBeFalsy();
553
606
  });
554
607
  it("leaves a downloaded unreadable link unjournaled without emitting a fatal error", async () => {
@@ -587,7 +640,7 @@ describe("sync", () => {
587
640
  samplePaths: [linkKey],
588
641
  });
589
642
  expect(events.some((event) => event.type === "error")).toBe(false);
590
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
643
+ const journal = journalAt(journalPath);
591
644
  expect(journal.files[linkKey]).toBeUndefined();
592
645
  }
593
646
  finally {
@@ -685,7 +738,7 @@ describe("sync", () => {
685
738
  expect(litter).toEqual([]);
686
739
  // Journal re-stamped off the stale baseline (hash now matches local,
687
740
  // remoteEtag recorded) so the phantom conflict can't re-fire.
688
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
741
+ const journalAfter = journalAt(journalPath);
689
742
  expect(journalAfter.files["docs/handoff.md"].hash).not.toBe("stale-hash");
690
743
  expect(journalAfter.files["docs/handoff.md"].remoteEtag).toBeTruthy();
691
744
  });
@@ -743,7 +796,7 @@ describe("sync", () => {
743
796
  });
744
797
  expect(events.some((event) => event.type === "error")).toBe(false);
745
798
  expect(fs.readFileSync(localPath, "utf-8")).toBe("local version");
746
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
799
+ const journal = journalAt(journalPath);
747
800
  expect(journal.files[linkKey].hash).toBe("stale-hash");
748
801
  }
749
802
  finally {
@@ -1131,7 +1184,7 @@ describe("sync", () => {
1131
1184
  .filter((f) => f.includes(".conflict-"));
1132
1185
  expect(litter).toHaveLength(0);
1133
1186
  // Journal stamped, so the next pass is a plain unchanged-skip.
1134
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1187
+ const journal = journalAt(journalPath);
1135
1188
  expect(journal.files["docs/handoff.md"]).toBeDefined();
1136
1189
  });
1137
1190
  it("RF-F02EXEC: refuses a download whose parent symlink appears after planning", async () => {
@@ -1291,7 +1344,7 @@ describe("sync", () => {
1291
1344
  });
1292
1345
  expect(fs.readFileSync(untrackedPath, "utf-8")).toBe("brand new local work");
1293
1346
  expect(fs.existsSync(trackedPath)).toBe(false);
1294
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1347
+ const journal = journalAt(journalPath);
1295
1348
  expect(journal.files[untrackedKey]).toBeUndefined();
1296
1349
  expect(journal.files[trackedKey]).toBeUndefined();
1297
1350
  });
@@ -1364,7 +1417,7 @@ describe("sync", () => {
1364
1417
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1365
1418
  expect(s3Module.downloadFile).toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1366
1419
  expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1367
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1420
+ const journal = journalAt(journalPath);
1368
1421
  expect(journal.files[key]?.removedAt).toBeUndefined();
1369
1422
  });
1370
1423
  it("does not canonicalize ignored remote-missing journal keys in the tombstone sweep", async () => {
@@ -1412,7 +1465,7 @@ describe("sync", () => {
1412
1465
  // Equivalence: an ignored key is never tombstoned — the local file and its
1413
1466
  // journal entry both survive, exactly as before the fix.
1414
1467
  expect(fs.existsSync(ignoredLocal)).toBe(true);
1415
- const sweptJournal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1468
+ const sweptJournal = journalAt(journalPath);
1416
1469
  expect(sweptJournal.files[ignoredKey]).toBeDefined();
1417
1470
  });
1418
1471
  it("tombstone lookup failure does not resurrect a current intent-backed delete", async () => {
@@ -1446,7 +1499,7 @@ describe("sync", () => {
1446
1499
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1447
1500
  expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1448
1501
  expect(fs.existsSync(localPath)).toBe(false);
1449
- expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]
1502
+ expect(journalAt(journalPath).files[key]
1450
1503
  ?.removedReason).toBe("local-delete");
1451
1504
  });
1452
1505
  it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
@@ -1482,7 +1535,7 @@ describe("sync", () => {
1482
1535
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1483
1536
  // All restored, none tombstoned.
1484
1537
  expect(s3Module.downloadFile).toHaveBeenCalledTimes(12);
1485
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1538
+ const journal = journalAt(journalPath);
1486
1539
  for (const k of keys) {
1487
1540
  expect(journal.files[k]?.removedAt).toBeUndefined();
1488
1541
  }
@@ -1523,7 +1576,7 @@ describe("sync", () => {
1523
1576
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1524
1577
  expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything());
1525
1578
  expect(fs.existsSync(localPath)).toBe(false);
1526
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1579
+ const journal = journalAt(journalPath);
1527
1580
  // Entry PRESERVED as a tombstone, not removed.
1528
1581
  expect(journal.files[key]?.removedAt).toBeTruthy();
1529
1582
  expect(journal.files[key]?.removedReason).toBe("local-delete");
@@ -1555,7 +1608,7 @@ describe("sync", () => {
1555
1608
  it("stamps journal.lastSync on every successful run, even when nothing transferred", async () => {
1556
1609
  // First run downloads both remote files and stamps lastSync.
1557
1610
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1558
- const firstStamp = JSON.parse(fs.readFileSync(journalPath, "utf-8")).lastSync;
1611
+ const firstStamp = journalAt(journalPath).lastSync;
1559
1612
  expect(firstStamp).not.toBe("");
1560
1613
  // Second run: same remote, same local — Stage-2 plan is all-skip, so
1561
1614
  // updateEntry never fires. lastSync must still advance, otherwise the
@@ -1563,7 +1616,7 @@ describe("sync", () => {
1563
1616
  await new Promise((r) => setTimeout(r, 5));
1564
1617
  const result = await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1565
1618
  expect(result.filesDownloaded).toBe(0);
1566
- const secondStamp = JSON.parse(fs.readFileSync(journalPath, "utf-8")).lastSync;
1619
+ const secondStamp = journalAt(journalPath).lastSync;
1567
1620
  expect(new Date(secondStamp).getTime()).toBeGreaterThan(new Date(firstStamp).getTime());
1568
1621
  });
1569
1622
  it("journalSlug: 'personal' routes journal I/O to sync-journal.personal.json", async () => {
@@ -1654,13 +1707,9 @@ describe("sync", () => {
1654
1707
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "bar.md"))).toBe(false);
1655
1708
  // The whole point: it now gets a journal baseline, so the push side stops
1656
1709
  // re-firing a transient conflict every sync (the bug this fix closes).
1657
- const journaledManifest = fs
1658
- .readdirSync(stateDir)
1659
- .filter((f) => f.startsWith("sync-journal."))
1660
- .some((f) => {
1661
- const j = JSON.parse(fs.readFileSync(path.join(stateDir, f), "utf8"));
1662
- return j.files?.["companies/manifest.yaml"] != null;
1663
- });
1710
+ // Enumerate through the API: the per-slug JSON files are locators now, so
1711
+ // reading them directly finds no `files` map at all.
1712
+ const journaledManifest = listJournals().some((j) => j.journal.files?.["companies/manifest.yaml"] != null);
1664
1713
  expect(journaledManifest).toBe(true);
1665
1714
  });
1666
1715
  it("personalMode pull lands the session-continuity pointer + active thread file under <hqRoot>/workspace/threads/ so a handoff resumes on machine B (DEV-1778)", async () => {
@@ -1913,7 +1962,7 @@ describe("sync", () => {
1913
1962
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
1914
1963
  // Journal entry for the deleted path must be gone too — otherwise
1915
1964
  // the next sync would tombstone the same key on every run.
1916
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1965
+ const journal = journalAt(journalPath);
1917
1966
  expect(journal.files["docs/deleted-by-peer.md"]).toBeUndefined();
1918
1967
  expect(journal.files["docs/kept.md"]).toBeDefined();
1919
1968
  });
@@ -1978,7 +2027,7 @@ describe("sync", () => {
1978
2027
  expect(result.filesTombstoned).toBe(0);
1979
2028
  // The journal is healed: the malformed key is gone, the canonical POSIX key
1980
2029
  // points at the live file (so a future pull stays converged, not poisoned).
1981
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2030
+ const journal = journalAt(journalPath);
1982
2031
  expect(journal.files[backslashKey]).toBeUndefined();
1983
2032
  expect(journal.files[posixKey]).toBeDefined();
1984
2033
  expect(Object.keys(journal.files).some((k) => k.includes("\\"))).toBe(false);
@@ -2031,7 +2080,7 @@ describe("sync", () => {
2031
2080
  expect(result.filesTombstoned).toBe(0);
2032
2081
  // Journal entry retained — next sync (push) will re-evaluate and
2033
2082
  // either re-upload the divergent local or surface as a conflict.
2034
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2083
+ const journal = journalAt(journalPath);
2035
2084
  expect(journal.files["docs/edited-locally.md"]).toBeDefined();
2036
2085
  expect(journal.files["docs/edited-locally.md"].hash).toBe(baselineHash);
2037
2086
  });
@@ -2075,7 +2124,7 @@ describe("sync", () => {
2075
2124
  expect(result.filesTombstoned).toBe(0);
2076
2125
  expect(fs.existsSync(racedPath)).toBe(true);
2077
2126
  expect(fs.readFileSync(racedPath, "utf-8")).toBe("concurrent local edit");
2078
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2127
+ const journal = journalAt(journalPath);
2079
2128
  expect(journal.files["docs/racy-delete.md"]).toBeDefined();
2080
2129
  expect(journal.files["docs/racy-delete.md"].hash).toBe(baselineHash);
2081
2130
  });
@@ -2112,7 +2161,7 @@ describe("sync", () => {
2112
2161
  expect(result.filesTombstoned).toBe(0);
2113
2162
  expect(result.conflicts).toBe(1);
2114
2163
  expect(fs.readFileSync(localPath, "utf-8")).toBe(baseline);
2115
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2164
+ const journal = journalAt(journalPath);
2116
2165
  expect(journal.files["docs/created-after-plan.md"]).toBeDefined();
2117
2166
  });
2118
2167
  it("CAS: preserves a concurrent local edit when a staged download is ready to replace it", async () => {
@@ -2196,7 +2245,7 @@ describe("sync", () => {
2196
2245
  propagateDeletePolicy: "all",
2197
2246
  });
2198
2247
  expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
2199
- const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
2248
+ const journal = journalAt(personalJournalPath);
2200
2249
  expect(journal.files["core/core.yaml"]).toBeDefined();
2201
2250
  expect(journal.files[".claude/skills/core:demo/SKILL.md"]).toBeDefined();
2202
2251
  });
@@ -2230,7 +2279,7 @@ describe("sync", () => {
2230
2279
  });
2231
2280
  expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
2232
2281
  expect(result.filesRefusedStale).toBe(2);
2233
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2282
+ const journal = journalAt(journalPath);
2234
2283
  expect(journal.files["docs/a.md"]).toBeDefined();
2235
2284
  expect(journal.files["docs/b.md"]).toBeDefined();
2236
2285
  });
@@ -2322,7 +2371,7 @@ describe("sync", () => {
2322
2371
  expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
2323
2372
  expect(fs.readlinkSync(linkPath)).toBe("new-target.md");
2324
2373
  expect(result.filesTombstoned).toBe(0);
2325
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2374
+ const journal = journalAt(journalPath);
2326
2375
  expect(journal.files["docs/link.md"]).toBeDefined();
2327
2376
  expect(journal.files["docs/link.md"].hash).toBe(baselineHash);
2328
2377
  });
@@ -2389,7 +2438,7 @@ describe("sync", () => {
2389
2438
  // The really-deleted file MUST be tombstoned.
2390
2439
  expect(fs.existsSync(reallyDeletedPath)).toBe(false);
2391
2440
  expect(result.filesTombstoned).toBe(1);
2392
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2441
+ const journal = journalAt(journalPath);
2393
2442
  // Journal entry retained for the invisible key (so next sync at
2394
2443
  // broader scope can re-evaluate).
2395
2444
  expect(journal.files["docs/invisible-to-list.md"]).toBeDefined();
@@ -2463,7 +2512,7 @@ describe("sync", () => {
2463
2512
  expect(result.filesDownloaded).toBe(0);
2464
2513
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
2465
2514
  // Journal entry dropped so it converges (no re-tombstone next run).
2466
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2515
+ const journal = journalAt(journalPath);
2467
2516
  expect(journal.files["docs/shared.md"]).toBeUndefined();
2468
2517
  });
2469
2518
  it("delete-resync: a re-created object NEWER than the tombstone DOES sync (tombstone is not permanent)", async () => {
@@ -2627,7 +2676,7 @@ describe("sync", () => {
2627
2676
  });
2628
2677
  expect(fs.existsSync(deniedPath)).toBe(true);
2629
2678
  expect(result.filesTombstoned).toBe(0);
2630
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2679
+ const journal = journalAt(journalPath);
2631
2680
  expect(journal.files["docs/out-of-scope.md"]).toBeDefined();
2632
2681
  });
2633
2682
  it("retains the journal entry when tombstone unlink fails with EACCES (Codex P1 — Bug #9 follow-up)", async () => {
@@ -2675,7 +2724,7 @@ describe("sync", () => {
2675
2724
  // Tombstone NOT counted — the delete wasn't honored.
2676
2725
  expect(result.filesTombstoned).toBe(0);
2677
2726
  // Journal entry MUST be retained so the next sync retries.
2678
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2727
+ const journal = journalAt(journalPath);
2679
2728
  expect(journal.files["docs/locked-by-peer.md"]).toBeDefined();
2680
2729
  expect(journal.files["docs/locked-by-peer.md"].hash).toBe("locked-hash");
2681
2730
  }
@@ -2902,7 +2951,7 @@ describe("sync", () => {
2902
2951
  expect(s3Module.downloadFile).not.toHaveBeenCalled();
2903
2952
  expect(fs.existsSync(escapedPath)).toBe(false);
2904
2953
  expect(fs.existsSync(path.join(companyRoot, traversalKey))).toBe(false);
2905
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2954
+ const journal = journalAt(journalPath);
2906
2955
  expect(journal.files[traversalKey]).toBeUndefined();
2907
2956
  }
2908
2957
  finally {
@@ -2936,7 +2985,7 @@ describe("sync", () => {
2936
2985
  expect(s3Module.downloadFile).not.toHaveBeenCalled();
2937
2986
  expect(fs.existsSync(escapedPath)).toBe(false);
2938
2987
  expect(fs.existsSync(path.join(companyRoot, remoteKey))).toBe(false);
2939
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2988
+ const journal = journalAt(journalPath);
2940
2989
  expect(journal.files[remoteKey]).toBeUndefined();
2941
2990
  }
2942
2991
  finally {
@@ -3011,7 +3060,7 @@ describe("sync", () => {
3011
3060
  vaultConfig: mockConfig,
3012
3061
  hqRoot: tmpDir,
3013
3062
  });
3014
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3063
+ const journal = journalAt(journalPath);
3015
3064
  expect(journal.files["docs/handoff.md"].remoteEtag).toBe("abc123");
3016
3065
  expect(journal.files["knowledge/readme.md"].remoteEtag).toBe("def456");
3017
3066
  });
@@ -3032,7 +3081,7 @@ describe("sync", () => {
3032
3081
  if (key === "bulk/file-8.md") {
3033
3082
  // This models the next transfer crashing the process. The first
3034
3083
  // eight entries must already be durable before the final writer.
3035
- const checkpoint = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3084
+ const checkpoint = journalAt(journalPath);
3036
3085
  expect(Object.keys(checkpoint.files)).toHaveLength(8);
3037
3086
  throw new Error("simulated crash after checkpoint");
3038
3087
  }
@@ -3046,7 +3095,7 @@ describe("sync", () => {
3046
3095
  onEvent: () => undefined,
3047
3096
  });
3048
3097
  expect(first.filesDownloaded).toBe(8);
3049
- const checkpoint = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3098
+ const checkpoint = journalAt(journalPath);
3050
3099
  expect(Object.keys(checkpoint.files)).toHaveLength(8);
3051
3100
  vi.mocked(s3Module.downloadFile).mockImplementation(defaultDownload);
3052
3101
  vi.mocked(s3Module.downloadFile).mockClear();
@@ -3497,7 +3546,7 @@ describe("sync", () => {
3497
3546
  // skip-unchanged works on the next tick.
3498
3547
  const journalMod = await import("../journal.js");
3499
3548
  const expectedHash = journalMod.hashSymlinkTarget(linkTarget);
3500
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3549
+ const journal = journalAt(journalPath);
3501
3550
  expect(journal.files[linkKey]).toMatchObject({
3502
3551
  hash: expectedHash,
3503
3552
  size: 0,
@@ -4144,7 +4193,7 @@ describe("sync", () => {
4144
4193
  // The journal MUST be updated to reflect the user's keep
4145
4194
  // decision. Pre-fix: catch swallowed the ENOENT, journal stayed
4146
4195
  // at oldHash + old etag, every subsequent sync re-fired.
4147
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4196
+ const journal = journalAt(journalPath);
4148
4197
  const entry = journal.files[linkKey];
4149
4198
  expect(entry.hash).toBe(expectedNewHash);
4150
4199
  expect(entry.size).toBe(0);
@@ -4443,7 +4492,7 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
4443
4492
  // Cleaned, not re-uploaded: local doubled-tree copy gone, journal dropped.
4444
4493
  expect(fs.existsSync(path.join(doubled, "poison.md"))).toBe(false);
4445
4494
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
4446
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4495
+ const journal = journalAt(journalPath);
4447
4496
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
4448
4497
  // No HEAD verify against the invalid key — the server validator would
4449
4498
  // reject it (INVALID_KEY_COMPANIES_SCOPED) and defer the cleanup forever.
@@ -4571,7 +4620,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4571
4620
  expect(events.filter((e) => e.type === "error")).toHaveLength(0);
4572
4621
  expect(result.aborted).toBe(false);
4573
4622
  expect(result.filesSkipped).toBe(1);
4574
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4623
+ const journalAfter = journalAt(journalPath);
4575
4624
  expect(Object.keys(journalAfter.files)).not.toContain(junkKey);
4576
4625
  });
4577
4626
  it("still pulls a junk-spelling key that is ALREADY journaled under that exact spelling", async () => {
@@ -4616,7 +4665,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4616
4665
  // Junk twin still gated; the canonical journal entry tombstones out.
4617
4666
  expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
4618
4667
  expect(result.filesTombstoned).toBe(1);
4619
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4668
+ const journalAfter = journalAt(journalPath);
4620
4669
  expect(Object.keys(journalAfter.files)).not.toContain(canonicalKey);
4621
4670
  });
4622
4671
  it("always downloads a CANONICAL remote key even when the journal holds only its junk twin (post-doctor convergence)", async () => {
@@ -4633,7 +4682,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4633
4682
  expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(1);
4634
4683
  expect(vi.mocked(s3Module.downloadFile).mock.calls[0][1]).toBe(canonicalKey);
4635
4684
  expect(result.filesDownloaded).toBe(1);
4636
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4685
+ const journalAfter = journalAt(journalPath);
4637
4686
  expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
4638
4687
  });
4639
4688
  it("does not tombstone a journaled canonical key when the LIST holds only its junk twin", async () => {
@@ -4649,7 +4698,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4649
4698
  const { events, result } = await runSync();
4650
4699
  // Junk twin skipped, canonical entry NOT tombstoned.
4651
4700
  expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
4652
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4701
+ const journalAfter = journalAt(journalPath);
4653
4702
  expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
4654
4703
  expect(result.filesTombstoned ?? 0).toBe(0);
4655
4704
  expect(events.filter((e) => e.type === "error")).toHaveLength(0);