canopycms 0.12.1 → 0.13.0

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 (93) hide show
  1. package/CHANGELOG.md +211 -49
  2. package/dist/api/branch-review.d.ts +20 -2
  3. package/dist/api/branch-review.js +112 -23
  4. package/dist/api/branch-status.d.ts +11 -2
  5. package/dist/api/branch-status.js +3 -2
  6. package/dist/api/branch-update-from-base.js +57 -6
  7. package/dist/api/branch.d.ts +5 -0
  8. package/dist/api/branch.js +27 -2
  9. package/dist/api/client.d.ts +2 -2
  10. package/dist/api/client.js +2 -2
  11. package/dist/api/content.js +1 -1
  12. package/dist/api/merge-conflicts.d.ts +10 -1
  13. package/dist/api/merge-conflicts.js +42 -7
  14. package/dist/api/types.d.ts +3 -2
  15. package/dist/base-update.d.ts +6 -0
  16. package/dist/branch-metadata-file.d.ts +15 -0
  17. package/dist/branch-metadata-file.js +5 -1
  18. package/dist/cli/cli.js +238 -108
  19. package/dist/cli/generate-ai-content.js +20 -6
  20. package/dist/cli/github-app-manifest.d.ts +4 -1
  21. package/dist/cli/github-app-manifest.js +5 -1
  22. package/dist/cli/init.js +7 -1
  23. package/dist/editor/Editor.js +39 -16
  24. package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
  25. package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
  26. package/dist/editor/admin/SystemHealthPanel.js +4 -3
  27. package/dist/editor/base-update-wait.d.ts +4 -0
  28. package/dist/editor/base-update-wait.js +7 -0
  29. package/dist/editor/components/ConflictsGroup.d.ts +13 -1
  30. package/dist/editor/components/ConflictsGroup.js +52 -12
  31. package/dist/editor/components/EditorHeader.d.ts +3 -1
  32. package/dist/editor/components/EditorHeader.js +11 -7
  33. package/dist/editor/components/EntryChangesDrawer.d.ts +1 -1
  34. package/dist/editor/components/EntryChangesDrawer.js +58 -6
  35. package/dist/editor/conflict-cards.d.ts +4 -1
  36. package/dist/editor/conflict-cards.js +14 -9
  37. package/dist/editor/hooks/useBaseUpdate.d.ts +2 -0
  38. package/dist/editor/hooks/useBaseUpdate.js +12 -2
  39. package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
  40. package/dist/editor/hooks/useBranchChangesData.js +13 -0
  41. package/dist/editor/hooks/useBranchManager.d.ts +2 -0
  42. package/dist/editor/hooks/useBranchManager.js +80 -6
  43. package/dist/editor/hooks/useDraftManager.d.ts +8 -1
  44. package/dist/editor/hooks/useDraftManager.js +16 -1
  45. package/dist/editor/hooks/useEntryManager.d.ts +8 -3
  46. package/dist/editor/hooks/useEntryManager.js +6 -5
  47. package/dist/git-manager.d.ts +1 -0
  48. package/dist/git-manager.js +8 -3
  49. package/dist/merge/entry-merge.js +4 -3
  50. package/dist/merge/list-alignment.d.ts +9 -6
  51. package/dist/merge/list-alignment.js +65 -16
  52. package/dist/merge/rekey.d.ts +11 -0
  53. package/dist/merge/rekey.js +75 -0
  54. package/dist/operating-mode/client-safe-strategy.js +1 -1
  55. package/dist/operating-mode/client-unsafe-strategy.js +6 -0
  56. package/dist/operating-mode/types.d.ts +10 -2
  57. package/dist/services.d.ts +12 -4
  58. package/dist/services.js +56 -0
  59. package/dist/types.d.ts +35 -3
  60. package/dist/utils/git.d.ts +7 -0
  61. package/dist/utils/git.js +35 -0
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/worker/branch-merge.d.ts +27 -3
  65. package/dist/worker/branch-merge.js +106 -5
  66. package/dist/worker/branch-policy.d.ts +18 -2
  67. package/dist/worker/branch-policy.js +36 -0
  68. package/dist/worker/cms-worker.d.ts +21 -0
  69. package/dist/worker/cms-worker.js +29 -4
  70. package/dist/worker/conflicts-status.d.ts +20 -0
  71. package/dist/worker/conflicts-status.js +204 -0
  72. package/dist/worker/field-merge.d.ts +17 -2
  73. package/dist/worker/field-merge.js +95 -2
  74. package/dist/worker/git-sync.d.ts +3 -2
  75. package/dist/worker/git-sync.js +9 -0
  76. package/dist/worker/github-commits.d.ts +3 -3
  77. package/dist/worker/github-commits.js +192 -40
  78. package/dist/worker/github-errors.d.ts +15 -0
  79. package/dist/worker/github-errors.js +47 -0
  80. package/dist/worker/github-gateway-http.js +7 -0
  81. package/dist/worker/github-gateway-wire.d.ts +30 -10
  82. package/dist/worker/github-gateway-wire.js +14 -2
  83. package/dist/worker/github-gateway.d.ts +17 -0
  84. package/dist/worker/github-gateway.js +37 -1
  85. package/dist/worker/history-rewrite.d.ts +40 -8
  86. package/dist/worker/history-rewrite.js +51 -23
  87. package/dist/worker/publish-decision.d.ts +37 -0
  88. package/dist/worker/publish-decision.js +16 -0
  89. package/dist/worker/rebase.d.ts +25 -5
  90. package/dist/worker/rebase.js +225 -81
  91. package/dist/worker/task-runner.js +16 -39
  92. package/dist/worker/worker-context.d.ts +6 -0
  93. package/package.json +1 -1
package/dist/cli/cli.js CHANGED
@@ -1332,22 +1332,22 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
1332
1332
  process.exitCode = 1;
1333
1333
  }
1334
1334
  }
1335
- const { dequeueTask: dequeueTask2, completeTask: completeTask2, listTasks: listTasks2 } = await Promise.resolve().then(() => (init_cms_task_queue(), cms_task_queue_exports));
1335
+ const { dequeueTask: dequeueTask22, completeTask: completeTask22, listTasks: listTasks22 } = await Promise.resolve().then(() => (init_cms_task_queue(), cms_task_queue_exports));
1336
1336
  if (mode === "prod") {
1337
- const pending = await listTasks2(taskDir, "pending");
1337
+ const pending = await listTasks22(taskDir, "pending");
1338
1338
  if (pending.length > 0) {
1339
1339
  throw new Error(`workerRunOnce found ${pending.length} pending task(s) in prod mode but cannot execute them. Use the full worker daemon to process prod task queues.`);
1340
1340
  }
1341
1341
  console.log("No pending tasks");
1342
1342
  } else {
1343
1343
  let taskCount = 0;
1344
- let task = await dequeueTask2(taskDir);
1344
+ let task = await dequeueTask22(taskDir);
1345
1345
  while (task !== null) {
1346
1346
  console.log(`Processing task: ${task.action} (${task.id})`);
1347
1347
  console.warn(` WARNING: Task skipped \u2014 GitHub operations require the full worker daemon`);
1348
- await completeTask2(taskDir, task.id, { skipped: true });
1348
+ await completeTask22(taskDir, task.id, { skipped: true });
1349
1349
  taskCount++;
1350
- task = await dequeueTask2(taskDir);
1350
+ task = await dequeueTask22(taskDir);
1351
1351
  }
1352
1352
  if (taskCount > 0) {
1353
1353
  console.log(`Processed ${taskCount} task(s)`);
@@ -2144,7 +2144,7 @@ var init_init = __esm({
2144
2144
  return false;
2145
2145
  }
2146
2146
  syncsBranchesWithBase() {
2147
- return false;
2147
+ return true;
2148
2148
  }
2149
2149
  getPermissionsFileName() {
2150
2150
  return "permissions.json";
@@ -2204,6 +2204,9 @@ var init_init = __esm({
2204
2204
  autoDetectRemotePath: path8.join(this.getWorkspaceRoot(), "remote.git")
2205
2205
  };
2206
2206
  }
2207
+ baseUpdateRunsInRequest() {
2208
+ return false;
2209
+ }
2207
2210
  requiresExistingRepo() {
2208
2211
  return false;
2209
2212
  }
@@ -2256,6 +2259,9 @@ var init_init = __esm({
2256
2259
  envVarName: "CANOPYCMS_REMOTE_URL"
2257
2260
  };
2258
2261
  }
2262
+ baseUpdateRunsInRequest() {
2263
+ return true;
2264
+ }
2259
2265
  requiresExistingRepo() {
2260
2266
  return false;
2261
2267
  }
@@ -2394,6 +2400,7 @@ var init_github_app_manifest = __esm({
2394
2400
  CANOPY_APP_PERMISSIONS = {
2395
2401
  contents: "write",
2396
2402
  pull_requests: "write",
2403
+ statuses: "write",
2397
2404
  metadata: "read"
2398
2405
  };
2399
2406
  APP_NAME_MAX_LENGTH = 34;
@@ -9230,7 +9237,7 @@ var init_generate_ai_content = __esm({
9230
9237
  return false;
9231
9238
  }
9232
9239
  syncsBranchesWithBase() {
9233
- return false;
9240
+ return true;
9234
9241
  }
9235
9242
  getPermissionsFileName() {
9236
9243
  return "permissions.json";
@@ -9314,6 +9321,9 @@ var init_generate_ai_content = __esm({
9314
9321
  autoDetectRemotePath: path92.join(this.getWorkspaceRoot(), "remote.git")
9315
9322
  };
9316
9323
  }
9324
+ baseUpdateRunsInRequest() {
9325
+ return false;
9326
+ }
9317
9327
  requiresExistingRepo() {
9318
9328
  return false;
9319
9329
  }
@@ -9366,6 +9376,9 @@ var init_generate_ai_content = __esm({
9366
9376
  envVarName: "CANOPYCMS_REMOTE_URL"
9367
9377
  };
9368
9378
  }
9379
+ baseUpdateRunsInRequest() {
9380
+ return true;
9381
+ }
9369
9382
  requiresExistingRepo() {
9370
9383
  return false;
9371
9384
  }
@@ -12629,6 +12642,7 @@ var init_generate_ai_content = __esm({
12629
12642
  archivedAt: z72.string().optional(),
12630
12643
  rebaseFailure: z72.object({ message: z72.string(), firstAt: z72.string(), lastAt: z72.string() }).optional(),
12631
12644
  historyRewrittenFrom: z72.string().optional(),
12645
+ historyRewrittenTo: z72.string().optional(),
12632
12646
  lastGitHubTip: z72.string().optional(),
12633
12647
  keptOverGitHub: z72.array(z72.string()).optional(),
12634
12648
  rewrittenOnGitHub: z72.string().optional(),
@@ -12643,13 +12657,16 @@ var init_generate_ai_content = __esm({
12643
12657
  z72.object({ kind: z72.literal("conflicts") }),
12644
12658
  z72.object({ kind: z72.literal("retargeted"), base: z72.string() })
12645
12659
  ]).optional().catch(void 0),
12646
- // Dropped when malformed: it only decorates the PR body, so it never fails a read.
12660
+ // Dropped when malformed, never failing a read: it decorates the PR body, and without `commit`
12661
+ // the worker judges a move against the clone's head before it (worker/github-commits.ts).
12647
12662
  review: z72.object({
12648
12663
  state: z72.enum(["approved", "changes-requested"]),
12649
12664
  by: z72.object({ userId: z72.string(), name: z72.string().optional() }),
12650
- at: z72.string()
12665
+ at: z72.string(),
12666
+ commit: z72.string().optional()
12651
12667
  }).optional().catch(void 0),
12652
12668
  baseUpdateRequestedAt: z72.string().optional().catch(void 0),
12669
+ latestChangesWaiting: z72.string().optional().catch(void 0),
12653
12670
  // Dropped when malformed: it only ends the editor's wait, which then times out instead.
12654
12671
  lastBaseUpdate: z72.object({
12655
12672
  at: z72.string(),
@@ -14389,10 +14406,14 @@ var init_generate_ai_content = __esm({
14389
14406
  const status = await this.status();
14390
14407
  return status.files.map((f) => f.path);
14391
14408
  }
14409
+ /** The remote's URL as configured; `git remote -v`'s listing would split a path with a space. */
14392
14410
  async getRemoteUrl() {
14393
- const remotes = await this.git.getRemotes(true);
14394
- const remote = remotes.find((r) => r.name === this.remote);
14395
- return remote?.refs.push || remote?.refs.fetch;
14411
+ try {
14412
+ const url = (await this.git.raw(["remote", "get-url", this.remote])).trim();
14413
+ return url === "" ? void 0 : url;
14414
+ } catch {
14415
+ return void 0;
14416
+ }
14396
14417
  }
14397
14418
  /**
14398
14419
  * Excludes `pattern` (e.g. `.canopy-meta/`) from this workspace's git, so it
@@ -17326,7 +17347,7 @@ var init_client_safe_strategy2 = __esm({
17326
17347
  return false;
17327
17348
  }
17328
17349
  syncsBranchesWithBase() {
17329
- return false;
17350
+ return true;
17330
17351
  }
17331
17352
  getPermissionsFileName() {
17332
17353
  return "permissions.json";
@@ -17364,11 +17385,11 @@ var init_branch_name2 = __esm({
17364
17385
  });
17365
17386
 
17366
17387
  // dist/paths/normalize.js
17367
- function normalizeFilesystemPath2(path60) {
17368
- return path60.split(/[\\/]+/).filter(Boolean).join("/");
17388
+ function normalizeFilesystemPath2(path63) {
17389
+ return path63.split(/[\\/]+/).filter(Boolean).join("/");
17369
17390
  }
17370
- function hasTraversalSequence2(path60) {
17371
- const normalized = normalizeFilesystemPath2(path60);
17391
+ function hasTraversalSequence2(path63) {
17392
+ const normalized = normalizeFilesystemPath2(path63);
17372
17393
  return normalized.includes("..");
17373
17394
  }
17374
17395
  function createLogicalPath2(...segments) {
@@ -17381,14 +17402,14 @@ function createLogicalPath2(...segments) {
17381
17402
  function entryLogicalPath2(collectionLogicalPath, slug) {
17382
17403
  return collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug;
17383
17404
  }
17384
- function trimSlashes2(path60) {
17405
+ function trimSlashes2(path63) {
17385
17406
  let start = 0;
17386
- let end = path60.length;
17387
- while (start < end && path60[start] === "/")
17407
+ let end = path63.length;
17408
+ while (start < end && path63[start] === "/")
17388
17409
  start++;
17389
- while (end > start && path60[end - 1] === "/")
17410
+ while (end > start && path63[end - 1] === "/")
17390
17411
  end--;
17391
- return path60.slice(start, end);
17412
+ return path63.slice(start, end);
17392
17413
  }
17393
17414
  var init_normalize2 = __esm({
17394
17415
  "dist/paths/normalize.js"() {
@@ -17400,15 +17421,15 @@ var init_normalize2 = __esm({
17400
17421
  function hasEmbeddedContentId(segment) {
17401
17422
  return PHYSICAL_SEGMENT_PATTERN3.test(segment);
17402
17423
  }
17403
- function looksLikePhysicalPath(path60) {
17404
- const segments = path60.split("/");
17424
+ function looksLikePhysicalPath(path63) {
17425
+ const segments = path63.split("/");
17405
17426
  return segments.some(hasEmbeddedContentId);
17406
17427
  }
17407
- function parseLogicalPath(path60) {
17408
- if (!path60 || typeof path60 !== "string") {
17428
+ function parseLogicalPath(path63) {
17429
+ if (!path63 || typeof path63 !== "string") {
17409
17430
  return { ok: false, error: "Path is required" };
17410
17431
  }
17411
- const normalized = path60.replace(/\\/g, "/");
17432
+ const normalized = path63.replace(/\\/g, "/");
17412
17433
  if (hasTraversalSequence2(normalized)) {
17413
17434
  return { ok: false, error: "Path contains traversal sequence" };
17414
17435
  }
@@ -17945,10 +17966,10 @@ function referenceValueId2(value) {
17945
17966
  return void 0;
17946
17967
  }
17947
17968
  function collectReferenceIds(fields, data) {
17948
- return traverseFields2(fields, data, ({ field, value, path: path60 }) => {
17969
+ return traverseFields2(fields, data, ({ field, value, path: path63 }) => {
17949
17970
  if (field.type !== "reference")
17950
17971
  return [];
17951
- const items = Array.isArray(value) ? value.map((item, index) => ({ item, path: `${path60}[${index}]` })) : [{ item: value, path: path60 }];
17972
+ const items = Array.isArray(value) ? value.map((item, index) => ({ item, path: `${path63}[${index}]` })) : [{ item: value, path: path63 }];
17952
17973
  return items.flatMap(({ item, path: itemPath }) => {
17953
17974
  const id = referenceValueId2(item);
17954
17975
  return id ? [{ id, path: itemPath }] : [];
@@ -18146,6 +18167,9 @@ var init_client_unsafe_strategy2 = __esm({
18146
18167
  autoDetectRemotePath: path31.join(this.getWorkspaceRoot(), "remote.git")
18147
18168
  };
18148
18169
  }
18170
+ baseUpdateRunsInRequest() {
18171
+ return false;
18172
+ }
18149
18173
  requiresExistingRepo() {
18150
18174
  return false;
18151
18175
  }
@@ -18198,6 +18222,9 @@ var init_client_unsafe_strategy2 = __esm({
18198
18222
  envVarName: "CANOPYCMS_REMOTE_URL"
18199
18223
  };
18200
18224
  }
18225
+ baseUpdateRunsInRequest() {
18226
+ return true;
18227
+ }
18201
18228
  requiresExistingRepo() {
18202
18229
  return false;
18203
18230
  }
@@ -18586,6 +18613,7 @@ var init_branch_metadata_file = __esm({
18586
18613
  archivedAt: z16.string().optional(),
18587
18614
  rebaseFailure: z16.object({ message: z16.string(), firstAt: z16.string(), lastAt: z16.string() }).optional(),
18588
18615
  historyRewrittenFrom: z16.string().optional(),
18616
+ historyRewrittenTo: z16.string().optional(),
18589
18617
  lastGitHubTip: z16.string().optional(),
18590
18618
  keptOverGitHub: z16.array(z16.string()).optional(),
18591
18619
  rewrittenOnGitHub: z16.string().optional(),
@@ -18600,13 +18628,16 @@ var init_branch_metadata_file = __esm({
18600
18628
  z16.object({ kind: z16.literal("conflicts") }),
18601
18629
  z16.object({ kind: z16.literal("retargeted"), base: z16.string() })
18602
18630
  ]).optional().catch(void 0),
18603
- // Dropped when malformed: it only decorates the PR body, so it never fails a read.
18631
+ // Dropped when malformed, never failing a read: it decorates the PR body, and without `commit`
18632
+ // the worker judges a move against the clone's head before it (worker/github-commits.ts).
18604
18633
  review: z16.object({
18605
18634
  state: z16.enum(["approved", "changes-requested"]),
18606
18635
  by: z16.object({ userId: z16.string(), name: z16.string().optional() }),
18607
- at: z16.string()
18636
+ at: z16.string(),
18637
+ commit: z16.string().optional()
18608
18638
  }).optional().catch(void 0),
18609
18639
  baseUpdateRequestedAt: z16.string().optional().catch(void 0),
18640
+ latestChangesWaiting: z16.string().optional().catch(void 0),
18610
18641
  // Dropped when malformed: it only ends the editor's wait, which then times out instead.
18611
18642
  lastBaseUpdate: z16.object({
18612
18643
  at: z16.string(),
@@ -19824,7 +19855,7 @@ function extractIdFromFilename2(filename) {
19824
19855
  }
19825
19856
  async function resolveCollectionPath2(root, logicalPath) {
19826
19857
  const fs47 = await import("node:fs/promises");
19827
- const path60 = await import("node:path");
19858
+ const path63 = await import("node:path");
19828
19859
  const segments = logicalPath.split("/").filter(Boolean);
19829
19860
  let currentPath = root;
19830
19861
  for (const segment of segments) {
@@ -19837,7 +19868,7 @@ async function resolveCollectionPath2(root, logicalPath) {
19837
19868
  return logicalName === segment.toLowerCase();
19838
19869
  });
19839
19870
  if (matchingDir) {
19840
- currentPath = path60.join(currentPath, matchingDir.name);
19871
+ currentPath = path63.join(currentPath, matchingDir.name);
19841
19872
  } else {
19842
19873
  return null;
19843
19874
  }
@@ -21226,8 +21257,8 @@ function computeEntryUrl2(collection, slug, contentRoot) {
21226
21257
  if (slug && !isIndexSlug2(slug)) {
21227
21258
  segments.push(slug);
21228
21259
  }
21229
- const path60 = segments.length > 0 ? `/${segments.join("/")}` : "/";
21230
- return path60.toLowerCase();
21260
+ const path63 = segments.length > 0 ? `/${segments.join("/")}` : "/";
21261
+ return path63.toLowerCase();
21231
21262
  }
21232
21263
  var init_entry_url = __esm({
21233
21264
  "dist/utils/entry-url.js"() {
@@ -21730,8 +21761,8 @@ function detectFormatting2(text3) {
21730
21761
  return { insertSpaces: false, tabSize: 1, eol };
21731
21762
  return { insertSpaces: true, tabSize: indent?.length ?? 2, eol };
21732
21763
  }
21733
- function insertMember2(text3, path60, key, value, formattingOptions) {
21734
- const at2 = [...path60, key];
21764
+ function insertMember2(text3, path63, key, value, formattingOptions) {
21765
+ const at2 = [...path63, key];
21735
21766
  const [edit] = modify2(text3, at2, value, {});
21736
21767
  const closing = edit ? /^[ \t]*\r?\n([ \t]*)\}/.exec(text3.slice(edit.offset)) : null;
21737
21768
  if (edit && closing && edit.length === 0 && edit.content.startsWith(",")) {
@@ -21745,35 +21776,35 @@ function insertMember2(text3, path60, key, value, formattingOptions) {
21745
21776
  }
21746
21777
  return applyEdits2(text3, modify2(text3, at2, value, { formattingOptions }));
21747
21778
  }
21748
- function removeMember2(text3, path60) {
21749
- return applyEdits2(text3, modify2(text3, path60, void 0, {}).map((edit) => {
21779
+ function removeMember2(text3, path63) {
21780
+ return applyEdits2(text3, modify2(text3, path63, void 0, {}).map((edit) => {
21750
21781
  const removed = text3.slice(edit.offset, edit.offset + edit.length);
21751
21782
  const lead = /^\s+/.exec(removed)?.[0].length ?? 0;
21752
21783
  const atMember = /\S/.test(text3.charAt(edit.offset + edit.length));
21753
21784
  return edit.content === "" && lead > 0 && atMember ? { ...edit, offset: edit.offset + lead, length: edit.length - lead } : edit;
21754
21785
  }));
21755
21786
  }
21756
- function reconcile2(text3, path60, before, after, formattingOptions) {
21787
+ function reconcile2(text3, path63, before, after, formattingOptions) {
21757
21788
  if (isRecord2(before) && isRecord2(after)) {
21758
21789
  if (Object.keys(after).length === 0 && Object.keys(before).length > 0) {
21759
- return applyEdits2(text3, modify2(text3, path60, {}, { formattingOptions }));
21790
+ return applyEdits2(text3, modify2(text3, path63, {}, { formattingOptions }));
21760
21791
  }
21761
21792
  let next = text3;
21762
21793
  for (const key of Object.keys(before)) {
21763
21794
  if (!Object.prototype.hasOwnProperty.call(after, key))
21764
- next = removeMember2(next, [...path60, key]);
21795
+ next = removeMember2(next, [...path63, key]);
21765
21796
  }
21766
21797
  for (const key of Object.keys(after)) {
21767
- next = Object.prototype.hasOwnProperty.call(before, key) ? reconcile2(next, [...path60, key], before[key], after[key], formattingOptions) : insertMember2(next, path60, key, after[key], formattingOptions);
21798
+ next = Object.prototype.hasOwnProperty.call(before, key) ? reconcile2(next, [...path63, key], before[key], after[key], formattingOptions) : insertMember2(next, path63, key, after[key], formattingOptions);
21768
21799
  }
21769
21800
  return next;
21770
21801
  }
21771
21802
  if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
21772
- return after.reduce((next, item, index) => reconcile2(next, [...path60, index], before[index], item, formattingOptions), text3);
21803
+ return after.reduce((next, item, index) => reconcile2(next, [...path63, index], before[index], item, formattingOptions), text3);
21773
21804
  }
21774
21805
  if (JSON.stringify(before) === JSON.stringify(after))
21775
21806
  return text3;
21776
- return applyEdits2(text3, modify2(text3, path60, after, { formattingOptions }));
21807
+ return applyEdits2(text3, modify2(text3, path63, after, { formattingOptions }));
21777
21808
  }
21778
21809
  function serializeJson2(data, existingRaw) {
21779
21810
  if (existingRaw === void 0)
@@ -23944,11 +23975,11 @@ var init_content_store = __esm({
23944
23975
  getSchemaItems() {
23945
23976
  return this.schemaIndex.values();
23946
23977
  }
23947
- assertSchemaItem(path60) {
23948
- const normalized = normalizeFilesystemPath2(path60);
23978
+ assertSchemaItem(path63) {
23979
+ const normalized = normalizeFilesystemPath2(path63);
23949
23980
  const item = this.schemaIndex.get(normalized);
23950
23981
  if (!item) {
23951
- throw new ContentStoreError2(`Unknown schema item: ${path60}`, "NO_SCHEMA_ITEM");
23982
+ throw new ContentStoreError2(`Unknown schema item: ${path63}`, "NO_SCHEMA_ITEM");
23952
23983
  }
23953
23984
  return item;
23954
23985
  }
@@ -25781,30 +25812,30 @@ function isValidImageCropValue2(value) {
25781
25812
  const rounded = roundCropRect2({ x, y, w, h });
25782
25813
  return isValidCropRect2(rounded.x, rounded.y, rounded.w, rounded.h);
25783
25814
  }
25784
- function validateImageValue2(field, value, path60) {
25815
+ function validateImageValue2(field, value, path63) {
25785
25816
  if (!isPlainRecord6(value)) {
25786
- return [{ fieldPath: path60, message: "Expected an image object with { src, alt }" }];
25817
+ return [{ fieldPath: path63, message: "Expected an image object with { src, alt }" }];
25787
25818
  }
25788
25819
  const errors = [];
25789
25820
  if (typeof value.src !== "string" || value.src.trim() === "") {
25790
- errors.push({ fieldPath: `${path60}.src`, message: "Image src is required" });
25821
+ errors.push({ fieldPath: `${path63}.src`, message: "Image src is required" });
25791
25822
  }
25792
25823
  if (typeof value.alt !== "string") {
25793
- errors.push({ fieldPath: `${path60}.alt`, message: "Image alt text is required" });
25824
+ errors.push({ fieldPath: `${path63}.alt`, message: "Image alt text is required" });
25794
25825
  } else if (value.alt.trim() === "" && field.altOptional !== true) {
25795
- errors.push({ fieldPath: `${path60}.alt`, message: "Image alt text is required" });
25826
+ errors.push({ fieldPath: `${path63}.alt`, message: "Image alt text is required" });
25796
25827
  }
25797
25828
  if (value.width !== void 0 && !isPositiveInt2(value.width)) {
25798
- errors.push({ fieldPath: `${path60}.width`, message: "Image width must be a positive integer" });
25829
+ errors.push({ fieldPath: `${path63}.width`, message: "Image width must be a positive integer" });
25799
25830
  }
25800
25831
  if (value.height !== void 0 && !isPositiveInt2(value.height)) {
25801
25832
  errors.push({
25802
- fieldPath: `${path60}.height`,
25833
+ fieldPath: `${path63}.height`,
25803
25834
  message: "Image height must be a positive integer"
25804
25835
  });
25805
25836
  }
25806
25837
  if (value.crop !== void 0 && !isValidImageCropValue2(value.crop)) {
25807
- errors.push({ fieldPath: `${path60}.crop`, message: "Invalid image crop rect" });
25838
+ errors.push({ fieldPath: `${path63}.crop`, message: "Invalid image crop rect" });
25808
25839
  }
25809
25840
  return errors;
25810
25841
  }
@@ -25813,49 +25844,49 @@ function mergeBodyIntoData(fields, data, body) {
25813
25844
  return data;
25814
25845
  return { ...data, [findBodyFieldName2(fields)]: body };
25815
25846
  }
25816
- function validateScalar2(field, value, path60) {
25847
+ function validateScalar2(field, value, path63) {
25817
25848
  switch (field.type) {
25818
25849
  case "number":
25819
25850
  if (typeof value !== "number" || !Number.isFinite(value)) {
25820
- return [{ fieldPath: path60, message: "Expected a number" }];
25851
+ return [{ fieldPath: path63, message: "Expected a number" }];
25821
25852
  }
25822
25853
  return [];
25823
25854
  case "boolean":
25824
25855
  if (typeof value !== "boolean") {
25825
- return [{ fieldPath: path60, message: "Expected true or false" }];
25856
+ return [{ fieldPath: path63, message: "Expected true or false" }];
25826
25857
  }
25827
25858
  return [];
25828
25859
  case "date":
25829
25860
  if (value !== "" && !isCalendarDay2(value)) {
25830
- return [{ fieldPath: path60, message: "Expected a real date, written YYYY-MM-DD" }];
25861
+ return [{ fieldPath: path63, message: "Expected a real date, written YYYY-MM-DD" }];
25831
25862
  }
25832
25863
  return [];
25833
25864
  case "datetime":
25834
25865
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
25835
- return [{ fieldPath: path60, message: "Expected a valid date/time string" }];
25866
+ return [{ fieldPath: path63, message: "Expected a valid date/time string" }];
25836
25867
  }
25837
25868
  return [];
25838
25869
  case "select": {
25839
25870
  if (typeof value !== "string") {
25840
- return [{ fieldPath: path60, message: "Expected a selection" }];
25871
+ return [{ fieldPath: path63, message: "Expected a selection" }];
25841
25872
  }
25842
25873
  const options = selectOptionValues2(field);
25843
25874
  if (value !== "" && !options.includes(value)) {
25844
- return [{ fieldPath: path60, message: `Must be one of: ${options.join(", ")}` }];
25875
+ return [{ fieldPath: path63, message: `Must be one of: ${options.join(", ")}` }];
25845
25876
  }
25846
25877
  return [];
25847
25878
  }
25848
25879
  case "reference":
25849
25880
  if (referenceValueId2(value) === void 0) {
25850
- return [{ fieldPath: path60, message: "Expected a reference id" }];
25881
+ return [{ fieldPath: path63, message: "Expected a reference id" }];
25851
25882
  }
25852
25883
  return [];
25853
25884
  case "image":
25854
- return validateImageValue2(field, value, path60);
25885
+ return validateImageValue2(field, value, path63);
25855
25886
  default:
25856
25887
  if (STRING_FIELD_TYPES2.has(field.type)) {
25857
25888
  if (typeof value !== "string") {
25858
- return [{ fieldPath: path60, message: "Expected text" }];
25889
+ return [{ fieldPath: path63, message: "Expected text" }];
25859
25890
  }
25860
25891
  }
25861
25892
  return [];
@@ -25878,12 +25909,12 @@ function validateEntryData2(fields, data, pathPrefix = "", options = {}) {
25878
25909
  errors.push(...validateEntryData2(field.fields, data, pathPrefix, options));
25879
25910
  continue;
25880
25911
  }
25881
- const path60 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
25912
+ const path63 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
25882
25913
  const value = data[field.name];
25883
25914
  const isRequired = options.required !== false && "required" in field && field.required === true;
25884
25915
  if (isRequired && isEmptyForRequired2(field, value)) {
25885
25916
  const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
25886
- errors.push({ fieldPath: path60, message });
25917
+ errors.push({ fieldPath: path63, message });
25887
25918
  continue;
25888
25919
  }
25889
25920
  if (value === void 0 || value === null)
@@ -25891,7 +25922,7 @@ function validateEntryData2(fields, data, pathPrefix = "", options = {}) {
25891
25922
  if (Array.isArray(value) && "maxItems" in field && typeof field.maxItems === "number" && value.length > field.maxItems) {
25892
25923
  const cap = field.maxItems;
25893
25924
  errors.push({
25894
- fieldPath: path60,
25925
+ fieldPath: path63,
25895
25926
  message: `At most ${cap} ${cap === 1 ? "item" : "items"}; this list has ${value.length}`,
25896
25927
  maxItems: { count: value.length, cap }
25897
25928
  });
@@ -25899,11 +25930,11 @@ function validateEntryData2(fields, data, pathPrefix = "", options = {}) {
25899
25930
  if (field.type === "block") {
25900
25931
  const blockField = field;
25901
25932
  if (!Array.isArray(value)) {
25902
- errors.push({ fieldPath: path60, message: "Expected a list of blocks" });
25933
+ errors.push({ fieldPath: path63, message: "Expected a list of blocks" });
25903
25934
  continue;
25904
25935
  }
25905
25936
  value.forEach((item, index) => {
25906
- const itemPath = `${path60}[${index}]`;
25937
+ const itemPath = `${path63}[${index}]`;
25907
25938
  if (!isPlainRecord6(item)) {
25908
25939
  errors.push({ fieldPath: itemPath, message: "Expected a block object" });
25909
25940
  return;
@@ -25925,11 +25956,11 @@ function validateEntryData2(fields, data, pathPrefix = "", options = {}) {
25925
25956
  const objectField = field;
25926
25957
  if (objectField.list) {
25927
25958
  if (!Array.isArray(value)) {
25928
- errors.push({ fieldPath: path60, message: "Expected a list of items" });
25959
+ errors.push({ fieldPath: path63, message: "Expected a list of items" });
25929
25960
  continue;
25930
25961
  }
25931
25962
  value.forEach((item, index) => {
25932
- const itemPath = `${path60}[${index}]`;
25963
+ const itemPath = `${path63}[${index}]`;
25933
25964
  if (!isPlainRecord6(item)) {
25934
25965
  errors.push({ fieldPath: itemPath, message: "Expected an object" });
25935
25966
  return;
@@ -25939,36 +25970,36 @@ function validateEntryData2(fields, data, pathPrefix = "", options = {}) {
25939
25970
  }
25940
25971
  });
25941
25972
  } else if (!isPlainRecord6(value)) {
25942
- errors.push({ fieldPath: path60, message: "Expected an object" });
25973
+ errors.push({ fieldPath: path63, message: "Expected an object" });
25943
25974
  } else if (objectField.fields) {
25944
- errors.push(...validateEntryData2(objectField.fields, value, path60, options));
25975
+ errors.push(...validateEntryData2(objectField.fields, value, path63, options));
25945
25976
  }
25946
25977
  continue;
25947
25978
  }
25948
25979
  if ("list" in field && field.list) {
25949
25980
  if (!Array.isArray(value)) {
25950
- errors.push({ fieldPath: path60, message: "Expected a list of values" });
25981
+ errors.push({ fieldPath: path63, message: "Expected a list of values" });
25951
25982
  continue;
25952
25983
  }
25953
25984
  value.forEach((item, index) => {
25954
- errors.push(...validateScalar2(field, item, `${path60}[${index}]`));
25985
+ errors.push(...validateScalar2(field, item, `${path63}[${index}]`));
25955
25986
  });
25956
25987
  continue;
25957
25988
  }
25958
25989
  if (Array.isArray(value) && KNOWN_FIELD_TYPES2.has(field.type)) {
25959
- errors.push({ fieldPath: path60, message: "Expected a single value, not a list" });
25990
+ errors.push({ fieldPath: path63, message: "Expected a single value, not a list" });
25960
25991
  continue;
25961
25992
  }
25962
- errors.push(...validateScalar2(field, value, path60));
25993
+ errors.push(...validateScalar2(field, value, path63));
25963
25994
  }
25964
25995
  return errors;
25965
25996
  }
25966
25997
  function findUnknownKeys(fields, data) {
25967
- return traverseFields2(fields, data, () => [], "", ({ fields: containerFields, data: record, path: path60 }) => {
25998
+ return traverseFields2(fields, data, () => [], "", ({ fields: containerFields, data: record, path: path63 }) => {
25968
25999
  if (containerFields.length === 0)
25969
26000
  return [];
25970
26001
  const known = new Set(flattenGroupFields2(containerFields).map((field) => field.name));
25971
- return Object.keys(record).filter((key) => !known.has(key) && !BLOCK_STRUCTURAL_KEYS2.has(key) && key !== ITEM_KEY2).map((key) => path60 ? `${path60}.${key}` : key);
26002
+ return Object.keys(record).filter((key) => !known.has(key) && !BLOCK_STRUCTURAL_KEYS2.has(key) && key !== ITEM_KEY2).map((key) => path63 ? `${path63}.${key}` : key);
25972
26003
  });
25973
26004
  }
25974
26005
  function normalizeReferenceValues(fields, data) {
@@ -26511,11 +26542,11 @@ function findMarkdownSafetyIssues(fields, format, data, siteAllow) {
26511
26542
  if (policy !== void 0)
26512
26543
  findings.push(...findInValue(bodyName, data[bodyName], policy));
26513
26544
  }
26514
- findings.push(...traverseFields2(fields, data, ({ field, value, path: path60 }) => {
26515
- if (path60 === bodyName || !isMarkdownField(field))
26545
+ findings.push(...traverseFields2(fields, data, ({ field, value, path: path63 }) => {
26546
+ if (path63 === bodyName || !isMarkdownField(field))
26516
26547
  return [];
26517
26548
  const policy = markdownPolicyOf(field, siteAllow);
26518
- return policy === void 0 ? [] : findInValue(path60, value, policy);
26549
+ return policy === void 0 ? [] : findInValue(path63, value, policy);
26519
26550
  }));
26520
26551
  return findings;
26521
26552
  }
@@ -26639,17 +26670,17 @@ var init_markdown_safety = __esm({
26639
26670
  });
26640
26671
 
26641
26672
  // dist/authorization/validation.js
26642
- function parsePermissionPath(path60) {
26643
- if (!path60 || typeof path60 !== "string") {
26673
+ function parsePermissionPath(path63) {
26674
+ if (!path63 || typeof path63 !== "string") {
26644
26675
  return { ok: false, error: "Permission path is required" };
26645
26676
  }
26646
- if (hasTraversalSequence2(path60)) {
26677
+ if (hasTraversalSequence2(path63)) {
26647
26678
  return {
26648
26679
  ok: false,
26649
26680
  error: "Permission path contains traversal sequence (..)"
26650
26681
  };
26651
26682
  }
26652
- const normalized = path60.replace(/\\/g, "/");
26683
+ const normalized = path63.replace(/\\/g, "/");
26653
26684
  if (normalized.startsWith("/") || normalized.endsWith("/")) {
26654
26685
  return {
26655
26686
  ok: false,
@@ -26749,6 +26780,79 @@ var init_validators = __esm({
26749
26780
  }
26750
26781
  });
26751
26782
 
26783
+ // dist/base-update.js
26784
+ var SYNC_BRANCH_ACTION, BASE_UPDATE_GIVE_UP_MS, BASE_UPDATE_EARLY_RETRY_MS;
26785
+ var init_base_update = __esm({
26786
+ "dist/base-update.js"() {
26787
+ "use strict";
26788
+ SYNC_BRANCH_ACTION = "sync-branch";
26789
+ BASE_UPDATE_GIVE_UP_MS = 10 * 6e4;
26790
+ BASE_UPDATE_EARLY_RETRY_MS = 2 * 6e4;
26791
+ }
26792
+ });
26793
+
26794
+ // dist/task-queue/task-queue.js
26795
+ import path57 from "node:path";
26796
+ import crypto3 from "node:crypto";
26797
+ async function enqueueTask2(taskDir, task, logger = nullLogger2) {
26798
+ const id = crypto3.randomUUID();
26799
+ const pendingDir = path57.join(taskDir, "pending");
26800
+ const queuedTask = {
26801
+ id,
26802
+ action: task.action,
26803
+ payload: task.payload,
26804
+ status: "pending",
26805
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
26806
+ retryCount: 0,
26807
+ maxRetries: task.maxRetries ?? DEFAULT_MAX_RETRIES2
26808
+ };
26809
+ const filePath = path57.join(pendingDir, `${id}.json`);
26810
+ await atomicWriteFile3(filePath, JSON.stringify(queuedTask, null, 2));
26811
+ logger.debug("Enqueued task", { id, action: task.action });
26812
+ return id;
26813
+ }
26814
+ var DEFAULT_MAX_RETRIES2, nullLogger2;
26815
+ var init_task_queue2 = __esm({
26816
+ "dist/task-queue/task-queue.js"() {
26817
+ "use strict";
26818
+ init_atomic_write3();
26819
+ DEFAULT_MAX_RETRIES2 = 3;
26820
+ nullLogger2 = { debug: () => {
26821
+ } };
26822
+ }
26823
+ });
26824
+
26825
+ // dist/task-queue/cms-task-queue.js
26826
+ var debugLogger2;
26827
+ var init_cms_task_queue2 = __esm({
26828
+ "dist/task-queue/cms-task-queue.js"() {
26829
+ "use strict";
26830
+ init_debug2();
26831
+ init_task_queue2();
26832
+ debugLogger2 = createDebugLogger3({ prefix: "TaskQueue" });
26833
+ }
26834
+ });
26835
+
26836
+ // dist/task-queue/task-queue-config.js
26837
+ import path58 from "node:path";
26838
+ function getTaskQueueDir3(config) {
26839
+ switch (config.mode) {
26840
+ case "prod": {
26841
+ const workspace = process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3;
26842
+ return path58.join(path58.resolve(workspace), ".tasks");
26843
+ }
26844
+ case "dev": {
26845
+ return path58.join(process.cwd(), ".canopy-dev", ".tasks");
26846
+ }
26847
+ }
26848
+ }
26849
+ var init_task_queue_config3 = __esm({
26850
+ "dist/task-queue/task-queue-config.js"() {
26851
+ "use strict";
26852
+ init_config5();
26853
+ }
26854
+ });
26855
+
26752
26856
  // dist/merge/conflict-path.js
26753
26857
  function describePath(fields, read, at2) {
26754
26858
  let key = "";
@@ -26860,7 +26964,7 @@ var init_conflict_path = __esm({
26860
26964
  // dist/merge-conflicts.js
26861
26965
  import { randomUUID as randomUUID7 } from "node:crypto";
26862
26966
  import fs45 from "node:fs/promises";
26863
- import path57 from "node:path";
26967
+ import path59 from "node:path";
26864
26968
  import { z as z23 } from "zod";
26865
26969
  function withoutResolvedFields(entries, contentId, resolved) {
26866
26970
  const entry = entries[contentId];
@@ -26969,7 +27073,7 @@ var init_merge_conflicts = __esm({
26969
27073
  MergeConflictStore = class {
26970
27074
  constructor(branchRoot, settleMs) {
26971
27075
  this.settleMs = settleMs;
26972
- this.filePath = path57.join(path57.resolve(branchRoot), ".canopy-meta", MERGE_CONFLICTS_FILE);
27076
+ this.filePath = path59.join(path59.resolve(branchRoot), ".canopy-meta", MERGE_CONFLICTS_FILE);
26973
27077
  }
26974
27078
  /** Whether the clone has a record at all. */
26975
27079
  async exists() {
@@ -26994,7 +27098,7 @@ var init_merge_conflicts = __esm({
26994
27098
  const stamp = `${Date.now()}-${randomUUID7().slice(0, 8)}`;
26995
27099
  const aside = `${this.filePath}.malformed-${stamp}`;
26996
27100
  await fs45.copyFile(this.filePath, aside);
26997
- const held = { aside: path57.basename(aside), at: (/* @__PURE__ */ new Date()).toISOString() };
27101
+ const held = { aside: path59.basename(aside), at: (/* @__PURE__ */ new Date()).toISOString() };
26998
27102
  const next = `${this.filePath}.held-${stamp}`;
26999
27103
  await writeOccJsonFile2(next, { schema: RECORD_SCHEMA, entries: {}, held }, { expectedVersion: null, settleMs: this.settleMs });
27000
27104
  await fs45.rename(next, this.filePath);
@@ -27062,11 +27166,20 @@ var init_merge_conflicts = __esm({
27062
27166
  });
27063
27167
 
27064
27168
  // dist/api/merge-conflicts.js
27065
- import path58 from "node:path";
27169
+ import path60 from "node:path";
27066
27170
  import { z as z24 } from "zod";
27067
- async function clearFieldConflicts(branchContext, contentId, resolved) {
27171
+ async function wakeConflictsStatus(config, branch) {
27172
+ if (operatingStrategy3(config.mode).baseUpdateRunsInRequest())
27173
+ return;
27174
+ try {
27175
+ await enqueueTask2(getTaskQueueDir3(config), { action: SYNC_BRANCH_ACTION, payload: { branch } });
27176
+ } catch (err2) {
27177
+ canopyLogWarn3(`CanopyCMS: Could not queue a sync for ${branch} after its last conflict was resolved:`, getErrorMessage(err2));
27178
+ }
27179
+ }
27180
+ async function clearFieldConflicts(config, branchContext, contentId, resolved) {
27068
27181
  let cleared = [];
27069
- await changeRecord(branchContext, (entries) => {
27182
+ await changeRecord(config, branchContext, (entries) => {
27070
27183
  const next = withoutResolvedFields(entries, contentId, resolved);
27071
27184
  cleared = next.cleared;
27072
27185
  return next.cleared.length > 0 ? next.entries : null;
@@ -27077,6 +27190,10 @@ var MAX_SIDE_BYTES, MAX_VERSIONS_BYTES, FORMATS, RECORD_UNREADABLE_ERROR, NO_CON
27077
27190
  var init_merge_conflicts2 = __esm({
27078
27191
  "dist/api/merge-conflicts.js"() {
27079
27192
  "use strict";
27193
+ init_base_update();
27194
+ init_operating_mode2();
27195
+ init_cms_task_queue2();
27196
+ init_task_queue_config3();
27080
27197
  init_content_listing();
27081
27198
  init_content_store();
27082
27199
  init_conflict_path();
@@ -27127,7 +27244,7 @@ var init_merge_conflicts2 = __esm({
27127
27244
  return ref.logicalPath;
27128
27245
  if (ref.kind === "collection-meta")
27129
27246
  return ref.collectionPath;
27130
- const settings = refOf2(`${path58.posix.dirname(at2)}/.collection.json`, flatSchema, contentRoot);
27247
+ const settings = refOf2(`${path60.posix.dirname(at2)}/.collection.json`, flatSchema, contentRoot);
27131
27248
  return settings.kind === "collection-meta" ? settings.collectionPath : void 0;
27132
27249
  };
27133
27250
  locateRecords = async (store, entries, flatSchema, contentRoot) => {
@@ -27161,7 +27278,7 @@ var init_merge_conflicts2 = __esm({
27161
27278
  return admin;
27162
27279
  return [record.address, record.recordedAddress ?? record.address].every((at2) => checkAccess(at2, level).allowed);
27163
27280
  };
27164
- changeRecord = async (branchContext, change) => {
27281
+ changeRecord = async (config, branchContext, change) => {
27165
27282
  const state = await new MergeConflictStore(branchContext.branchRoot).update((current) => {
27166
27283
  if (current === null)
27167
27284
  return null;
@@ -27171,6 +27288,9 @@ var init_merge_conflicts2 = __esm({
27171
27288
  if (state === null)
27172
27289
  return null;
27173
27290
  await getBranchMetadataFileManager(branchContext.branchRoot, branchContext.baseRoot).update((existing) => existing?.branch.name === branchContext.branch.name ? { branch: conflictFilesOf(state.entries) } : null, { keepUpdatedAt: true });
27291
+ if (openConflictCount(state.entries) === 0 && !state.held) {
27292
+ await wakeConflictsStatus(config, branchContext.branch.name);
27293
+ }
27174
27294
  return state.entries;
27175
27295
  };
27176
27296
  entryTypeOf = (flatSchema, ref) => flatSchema.find((item) => item.type === "collection" && item.logicalPath === ref.collectionPath)?.entries?.find((e) => e.name === ref.entryType);
@@ -27398,7 +27518,9 @@ var init_merge_conflicts2 = __esm({
27398
27518
  * latest version has it (`deleted-on-branch`); `use-latest`: replace an entry that can't be merged
27399
27519
  * field by field with the latest version (`unreadable`).
27400
27520
  */
27401
- choice: z24.enum(["keep", "delete", "restore", "use-latest"])
27521
+ choice: z24.enum(["keep", "delete", "restore", "use-latest"]),
27522
+ /** The card promised the entry stays deleted: a `keep` is refused if it is in the tree now. */
27523
+ entryGone: z24.literal(true).optional()
27402
27524
  });
27403
27525
  CHOICE_KINDS = {
27404
27526
  keep: ["fields", "deleted-on-base", "deleted-on-branch", "unreadable"],
@@ -27407,18 +27529,18 @@ var init_merge_conflicts2 = __esm({
27407
27529
  "use-latest": ["unreadable"]
27408
27530
  };
27409
27531
  sameKindAtLatest = async (git, entry, id, placed, flatSchema, contentRoot) => {
27410
- const own = parseTypedFilename2(path58.posix.basename(placed.physicalPath));
27532
+ const own = parseTypedFilename2(path60.posix.basename(placed.physicalPath));
27411
27533
  let files;
27412
27534
  try {
27413
27535
  files = await git.listFilesWithSizeAt(`${entry.mergeSha}^2`, contentRoot);
27414
27536
  } catch {
27415
27537
  return false;
27416
27538
  }
27417
- const latest = files.find((f) => parseTypedFilename2(path58.posix.basename(f.path))?.id === id);
27539
+ const latest = files.find((f) => parseTypedFilename2(path60.posix.basename(f.path))?.id === id);
27418
27540
  if (!latest || !own)
27419
27541
  return false;
27420
27542
  const latestRef = entryRefFromRepoPath(latest.path, flatSchema, contentRoot);
27421
- return latestRef.kind === "entry" && latestRef.collectionPath === placed.collectionPath && latestRef.entryType === own.type && path58.posix.extname(latest.path) === path58.posix.extname(placed.physicalPath);
27543
+ return latestRef.kind === "entry" && latestRef.collectionPath === placed.collectionPath && latestRef.entryType === own.type && path60.posix.extname(latest.path) === path60.posix.extname(placed.physicalPath);
27422
27544
  };
27423
27545
  ConflictChangedError = class extends Error {
27424
27546
  };
@@ -27466,9 +27588,14 @@ var init_merge_conflicts2 = __esm({
27466
27588
  if (current?.kind !== entry.kind || current.mergeSha !== body.mergeSha) {
27467
27589
  throw new ConflictChangedError();
27468
27590
  }
27591
+ if (body.entryGone) {
27592
+ store.invalidateIndex();
27593
+ if ((await store.idIndex()).findById(body.id) !== null)
27594
+ throw new ConflictChangedError();
27595
+ }
27469
27596
  };
27470
27597
  const clear = async () => {
27471
- const written = await changeRecord(branchContext, (now) => {
27598
+ const written = await changeRecord(ctx.services.config, branchContext, (now) => {
27472
27599
  const current = now[body.id];
27473
27600
  if (current?.kind === "fields" && current.mergeSha === body.mergeSha) {
27474
27601
  const { [body.id]: _kept, ...rest } = now;
@@ -27586,7 +27713,7 @@ var init_merge_conflicts2 = __esm({
27586
27713
  guards: ["schema", "writableBranch"],
27587
27714
  handler: resolveHandler
27588
27715
  });
27589
- releaseHoldHandler = async (gc) => {
27716
+ releaseHoldHandler = async (gc, ctx) => {
27590
27717
  const { branchRoot } = gc.branchContext;
27591
27718
  try {
27592
27719
  const written = await withContentWriteLock2(branchRoot, () => new MergeConflictStore(branchRoot).update((current) => {
@@ -27594,6 +27721,9 @@ var init_merge_conflicts2 = __esm({
27594
27721
  return null;
27595
27722
  return { ...current, held: void 0 };
27596
27723
  }));
27724
+ if (written !== null && openConflictCount(written.entries) === 0) {
27725
+ await wakeConflictsStatus(ctx.services.config, gc.branchContext.branch.name);
27726
+ }
27597
27727
  return { ok: true, status: 200, data: { released: written !== null } };
27598
27728
  } catch (err2) {
27599
27729
  if (err2 instanceof ContentWriteLockBusyError2) {
@@ -27976,7 +28106,7 @@ var init_content = __esm({
27976
28106
  if (!id || resolving.length === 0)
27977
28107
  return;
27978
28108
  try {
27979
- resolvedConflicts = await clearFieldConflicts(branchContext, id, resolving);
28109
+ resolvedConflicts = await clearFieldConflicts(ctx.services.config, branchContext, id, resolving);
27980
28110
  } catch (err2) {
27981
28111
  canopyLogWarn3(`CanopyCMS: saved ${logicalPathSegments.join("/")} but couldn't mark its conflicts resolved:`, getErrorMessage(err2));
27982
28112
  resolvedConflicts = [];
@@ -28205,7 +28335,7 @@ __export(backfill_item_keys_exports, {
28205
28335
  backfillItemKeysCLI: () => backfillItemKeysCLI
28206
28336
  });
28207
28337
  import fs46 from "node:fs/promises";
28208
- import path59 from "node:path";
28338
+ import path61 from "node:path";
28209
28339
  import { diffArrays } from "diff";
28210
28340
  import { simpleGit as simpleGit8 } from "simple-git";
28211
28341
  async function backfillItemKeys(options) {
@@ -28217,7 +28347,7 @@ async function backfillItemKeys(options) {
28217
28347
  const entryTypes = collection.entries ?? [];
28218
28348
  let listed;
28219
28349
  try {
28220
- listed = await listCollectionEntries2(root, collection, (file) => result.malformed.push(path59.posix.join(file.collectionPath, file.filename)));
28350
+ listed = await listCollectionEntries2(root, collection, (file) => result.malformed.push(path61.posix.join(file.collectionPath, file.filename)));
28221
28351
  } catch (err2) {
28222
28352
  result.failed.push({ path: `${collection.logicalPath}/`, error: getErrorMessage(err2) });
28223
28353
  continue;
@@ -28246,7 +28376,7 @@ async function backfillItemKeys(options) {
28246
28376
  }
28247
28377
  async function backfillEntry(store, root, item, fields, check) {
28248
28378
  const doc = await store.read(item.collectionPath, item.slug, { resolveReferences: false });
28249
- if (doc.absolutePath !== path59.resolve(root, item.physicalPath)) {
28379
+ if (doc.absolutePath !== path61.resolve(root, item.physicalPath)) {
28250
28380
  throw new Error(`Another file in its collection has the slug "${item.slug}", so neither a save nor the backfill can address this one. Rename or remove one of them.`);
28251
28381
  }
28252
28382
  const before = await fs46.readFile(doc.absolutePath, "utf8");
@@ -28332,7 +28462,7 @@ async function backfillItemKeysCLI(options) {
28332
28462
  }
28333
28463
  let flatSchema;
28334
28464
  try {
28335
- const contentDir = path59.join(projectDir, contentRootName);
28465
+ const contentDir = path61.join(projectDir, contentRootName);
28336
28466
  const resolved = await resolveSchema2(contentDir, registry3, {
28337
28467
  unknownSchema: "degrade"
28338
28468
  });
@@ -28353,7 +28483,7 @@ async function backfillItemKeysCLI(options) {
28353
28483
  return BACKFILL_EXIT_CODES.changesOrRefused;
28354
28484
  }
28355
28485
  }
28356
- const metaDir = path59.join(projectDir, CANOPY_META_DIR2);
28486
+ const metaDir = path61.join(projectDir, CANOPY_META_DIR2);
28357
28487
  const metaDirExisted = await filePathExists(metaDir);
28358
28488
  let result;
28359
28489
  try {