canopycms 0.12.0 → 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 (177) hide show
  1. package/CHANGELOG.md +326 -48
  2. package/dist/api/admin.d.ts +4 -4
  3. package/dist/api/branch-review.d.ts +20 -2
  4. package/dist/api/branch-review.js +153 -32
  5. package/dist/api/branch-status.d.ts +23 -2
  6. package/dist/api/branch-status.js +16 -2
  7. package/dist/api/branch-update-from-base.d.ts +23 -0
  8. package/dist/api/branch-update-from-base.js +227 -0
  9. package/dist/api/branch.d.ts +10 -0
  10. package/dist/api/branch.js +28 -2
  11. package/dist/api/client.d.ts +15 -2
  12. package/dist/api/client.js +24 -2
  13. package/dist/api/comments.d.ts +2 -2
  14. package/dist/api/content.d.ts +38 -1
  15. package/dist/api/content.js +41 -5
  16. package/dist/api/merge-conflicts.d.ts +235 -0
  17. package/dist/api/merge-conflicts.js +709 -0
  18. package/dist/api/routes.js +2 -0
  19. package/dist/api/types.d.ts +8 -3
  20. package/dist/base-update.d.ts +32 -0
  21. package/dist/base-update.js +31 -0
  22. package/dist/branch-metadata-file.d.ts +66 -0
  23. package/dist/branch-metadata-file.js +16 -1
  24. package/dist/branch-schema-cache.d.ts +12 -0
  25. package/dist/branch-schema-cache.js +27 -2
  26. package/dist/cli/backfill-item-keys.d.ts +77 -0
  27. package/dist/cli/backfill-item-keys.js +320 -0
  28. package/dist/cli/cli.d.ts +1 -1
  29. package/dist/cli/cli.js +9798 -1509
  30. package/dist/cli/generate-ai-content.js +373 -249
  31. package/dist/cli/github-app-manifest.d.ts +4 -1
  32. package/dist/cli/github-app-manifest.js +5 -1
  33. package/dist/cli/init.js +12 -0
  34. package/dist/cli/site-config.d.ts +14 -2
  35. package/dist/cli/site-config.js +20 -4
  36. package/dist/comment-store.d.ts +8 -0
  37. package/dist/comment-store.js +28 -0
  38. package/dist/content-index-generation.d.ts +1 -4
  39. package/dist/content-index-generation.js +1 -4
  40. package/dist/content-store.d.ts +39 -3
  41. package/dist/content-store.js +48 -15
  42. package/dist/editor/BranchManager.d.ts +4 -1
  43. package/dist/editor/BranchManager.js +9 -5
  44. package/dist/editor/Editor.js +462 -62
  45. package/dist/editor/EntryNavigator.d.ts +10 -2
  46. package/dist/editor/EntryNavigator.js +12 -4
  47. package/dist/editor/FormRenderer.d.ts +8 -2
  48. package/dist/editor/FormRenderer.js +5 -6
  49. package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
  50. package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
  51. package/dist/editor/admin/SystemHealthPanel.js +4 -3
  52. package/dist/editor/base-update-wait.d.ts +78 -0
  53. package/dist/editor/base-update-wait.js +98 -0
  54. package/dist/editor/branch-status.js +1 -1
  55. package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
  56. package/dist/editor/components/BaseUpdateNotice.js +24 -0
  57. package/dist/editor/components/BranchPill.d.ts +4 -1
  58. package/dist/editor/components/BranchPill.js +5 -2
  59. package/dist/editor/components/ConflictsGroup.d.ts +112 -0
  60. package/dist/editor/components/ConflictsGroup.js +404 -0
  61. package/dist/editor/components/EditorHeader.d.ts +18 -1
  62. package/dist/editor/components/EditorHeader.js +99 -33
  63. package/dist/editor/components/EntryChangesDrawer.d.ts +16 -2
  64. package/dist/editor/components/EntryChangesDrawer.js +85 -167
  65. package/dist/editor/components/EntryHeader.d.ts +3 -1
  66. package/dist/editor/components/EntryHeader.js +2 -2
  67. package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
  68. package/dist/editor/components/GetLatestChangesButton.js +29 -0
  69. package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
  70. package/dist/editor/components/ReleaseHoldButton.js +36 -0
  71. package/dist/editor/components/RetryToastMessage.js +1 -1
  72. package/dist/editor/components/change-parts.d.ts +51 -0
  73. package/dist/editor/components/change-parts.js +171 -0
  74. package/dist/editor/conflict-cards.d.ts +93 -0
  75. package/dist/editor/conflict-cards.js +284 -0
  76. package/dist/editor/conflict-choices.d.ts +60 -0
  77. package/dist/editor/conflict-choices.js +108 -0
  78. package/dist/editor/conflict-notice.d.ts +25 -0
  79. package/dist/editor/conflict-notice.js +39 -0
  80. package/dist/editor/copy.d.ts +12 -0
  81. package/dist/editor/copy.js +12 -0
  82. package/dist/editor/entry-diff.d.ts +2 -1
  83. package/dist/editor/entry-diff.js +5 -21
  84. package/dist/editor/hooks/useBaseUpdate.d.ts +38 -0
  85. package/dist/editor/hooks/useBaseUpdate.js +279 -0
  86. package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
  87. package/dist/editor/hooks/useBranchChangesData.js +13 -0
  88. package/dist/editor/hooks/useBranchManager.d.ts +8 -1
  89. package/dist/editor/hooks/useBranchManager.js +108 -13
  90. package/dist/editor/hooks/useBranchesData.js +3 -0
  91. package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
  92. package/dist/editor/hooks/useConflictChoices.js +75 -0
  93. package/dist/editor/hooks/useConflictsData.d.ts +26 -0
  94. package/dist/editor/hooks/useConflictsData.js +89 -0
  95. package/dist/editor/hooks/useDraftManager.d.ts +13 -1
  96. package/dist/editor/hooks/useDraftManager.js +40 -9
  97. package/dist/editor/hooks/useEntryManager.d.ts +17 -3
  98. package/dist/editor/hooks/useEntryManager.js +11 -5
  99. package/dist/editor/release-hold.d.ts +12 -0
  100. package/dist/editor/release-hold.js +39 -0
  101. package/dist/editor/server-clock.d.ts +4 -0
  102. package/dist/editor/server-clock.js +15 -0
  103. package/dist/editor/undo-change.d.ts +7 -0
  104. package/dist/editor/undo-change.js +19 -4
  105. package/dist/git-manager.d.ts +6 -0
  106. package/dist/git-manager.js +38 -3
  107. package/dist/item-keys.d.ts +13 -0
  108. package/dist/item-keys.js +36 -14
  109. package/dist/merge/conflict-path.d.ts +40 -0
  110. package/dist/merge/conflict-path.js +128 -0
  111. package/dist/merge/diff3.d.ts +27 -0
  112. package/dist/merge/diff3.js +113 -0
  113. package/dist/merge/entry-merge.d.ts +94 -0
  114. package/dist/merge/entry-merge.js +876 -0
  115. package/dist/merge/list-alignment.d.ts +55 -0
  116. package/dist/merge/list-alignment.js +490 -0
  117. package/dist/merge/rekey.d.ts +11 -0
  118. package/dist/merge/rekey.js +75 -0
  119. package/dist/merge/text-merge.d.ts +29 -0
  120. package/dist/merge/text-merge.js +59 -0
  121. package/dist/merge/value-equality.d.ts +18 -0
  122. package/dist/merge/value-equality.js +50 -0
  123. package/dist/merge-conflicts.d.ts +232 -0
  124. package/dist/merge-conflicts.js +451 -0
  125. package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
  126. package/dist/operating-mode/client-safe-strategy.js +6 -0
  127. package/dist/operating-mode/client-unsafe-strategy.js +6 -0
  128. package/dist/operating-mode/types.d.ts +13 -0
  129. package/dist/services.d.ts +24 -1
  130. package/dist/services.js +138 -22
  131. package/dist/task-queue/cms-task-queue.d.ts +3 -1
  132. package/dist/types.d.ts +63 -5
  133. package/dist/utils/content-serialize.d.ts +18 -1
  134. package/dist/utils/content-serialize.js +12 -0
  135. package/dist/utils/content-write-lock.d.ts +3 -3
  136. package/dist/utils/content-write-lock.js +3 -3
  137. package/dist/utils/git.d.ts +7 -0
  138. package/dist/utils/git.js +35 -0
  139. package/dist/utils/markdown-body-splice.d.ts +1 -1
  140. package/dist/utils/markdown-body-splice.js +1 -19
  141. package/dist/utils/markdown-parse.d.ts +9 -0
  142. package/dist/utils/markdown-parse.js +27 -0
  143. package/dist/version.d.ts +1 -1
  144. package/dist/version.js +1 -1
  145. package/dist/worker/branch-merge.d.ts +80 -40
  146. package/dist/worker/branch-merge.js +244 -58
  147. package/dist/worker/branch-policy.d.ts +18 -2
  148. package/dist/worker/branch-policy.js +36 -0
  149. package/dist/worker/cms-worker.d.ts +37 -3
  150. package/dist/worker/cms-worker.js +88 -16
  151. package/dist/worker/conflicts-status.d.ts +20 -0
  152. package/dist/worker/conflicts-status.js +204 -0
  153. package/dist/worker/field-merge.d.ts +80 -0
  154. package/dist/worker/field-merge.js +729 -0
  155. package/dist/worker/git-sync.d.ts +3 -2
  156. package/dist/worker/git-sync.js +16 -6
  157. package/dist/worker/github-commits.d.ts +42 -10
  158. package/dist/worker/github-commits.js +444 -52
  159. package/dist/worker/github-errors.d.ts +15 -0
  160. package/dist/worker/github-errors.js +47 -0
  161. package/dist/worker/github-gateway-http.js +24 -9
  162. package/dist/worker/github-gateway-wire.d.ts +49 -20
  163. package/dist/worker/github-gateway-wire.js +19 -3
  164. package/dist/worker/github-gateway.d.ts +27 -1
  165. package/dist/worker/github-gateway.js +55 -7
  166. package/dist/worker/github-transfer.d.ts +5 -3
  167. package/dist/worker/github-transfer.js +8 -5
  168. package/dist/worker/history-rewrite.d.ts +40 -8
  169. package/dist/worker/history-rewrite.js +51 -23
  170. package/dist/worker/publish-decision.d.ts +37 -0
  171. package/dist/worker/publish-decision.js +16 -0
  172. package/dist/worker/rebase.d.ts +29 -8
  173. package/dist/worker/rebase.js +492 -211
  174. package/dist/worker/task-runner.d.ts +1 -1
  175. package/dist/worker/task-runner.js +30 -41
  176. package/dist/worker/worker-context.d.ts +24 -2
  177. package/package.json +1 -1
@@ -7,6 +7,7 @@
7
7
  import { BRANCH_ROUTES } from './branch.js';
8
8
  import { BRANCH_CHANGES_ROUTES } from './branch-changes.js';
9
9
  import { BRANCH_ENTRY_CHANGE_ROUTES } from './branch-entry-changes.js';
10
+ import { MERGE_CONFLICT_ROUTES } from './merge-conflicts.js';
10
11
  import { WORKFLOW_ROUTES } from './branch-status.js';
11
12
  import { COMMENT_ROUTES } from './comments.js';
12
13
  import { CONTENT_ROUTES } from './content.js';
@@ -28,6 +29,7 @@ export function buildCanopyRoutes() {
28
29
  ...Object.values(BRANCH_ROUTES),
29
30
  ...Object.values(BRANCH_CHANGES_ROUTES),
30
31
  ...Object.values(BRANCH_ENTRY_CHANGE_ROUTES),
32
+ ...Object.values(MERGE_CONFLICT_ROUTES),
31
33
  ...Object.values(WORKFLOW_ROUTES),
32
34
  ...Object.values(COMMENT_ROUTES),
33
35
  ...Object.values(CONTENT_ROUTES),
@@ -58,7 +58,8 @@ export interface ApiResponse<TData = unknown> {
58
58
  * `WRITE_OUTCOME_UNKNOWN`: an entry save ran but lost its branch lock mid-write, so it may have
59
59
  * landed (a 409); the version the caller holds may be stale, so it must re-read before retrying.
60
60
  * `ENTRY_GONE`: an entry update found no file, because the entry was deleted or moved since the
61
- * caller read it (a 409); it was not saved, and only a create can bring the entry back.
61
+ * caller read it (a 409); it was not saved, and only a create can bring the entry back. A
62
+ * resolve-entry answers it too, for an entry no longer where its conflict was shown.
62
63
  * `EDITOR_MODE_MISMATCH`: the editor bundle was built for the other operating mode than the
63
64
  * server runs (a 412, see operating-mode/editor-mode-check.ts); only a rebuild fixes it.
64
65
  * `SETTINGS_CHANGED`: a groups or permissions save named a version the file has moved on from (a
@@ -66,6 +67,10 @@ export interface ApiResponse<TData = unknown> {
66
67
  * The other settings 409, a busy file lock, carries no code. `BRANCH_SYNCING`: an entry save was
67
68
  * refused because the branch is mid-rebase (a 409); nothing was saved, and a retry soon can pass.
68
69
  * `BRANCH_NAME_REFUSED`: a branch create was refused for the name it gave (a 400 or 409), so only
69
- * another name can pass; no other create failure carries it.
70
+ * another name can pass; no other create failure carries it. `OPEN_CONFLICTS`: a submit or approval
71
+ * was refused while a merge's conflicts are open on the branch (a 409, merge-conflicts.ts).
72
+ * `CONFLICT_CHANGED`: a resolve-entry named a conflict the record no longer holds as shown (a 409);
73
+ * re-read the branch's conflicts. `BRANCH_MOVED`: an approval or a request for changes named a commit
74
+ * other than the branch's head, or none (a 409, api/branch-review.ts); reload the branch first.
70
75
  */
71
- export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING' | 'BRANCH_NAME_REFUSED';
76
+ export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING' | 'BRANCH_NAME_REFUSED' | 'OPEN_CONFLICTS' | 'CONFLICT_CHANGED' | 'BRANCH_MOVED';
@@ -0,0 +1,32 @@
1
+ import type { BranchMetadata } from './types.js';
2
+ /**
3
+ * Asking for the latest changes: the API records the request on branch.json and queues this action
4
+ * (api/branch-update-from-base.ts), the worker's sync answers it in `lastBaseUpdate`
5
+ * (worker/rebase.ts), and the editor waits while it stands. Dependency-free, so all three share it.
6
+ */
7
+ export declare const SYNC_BRANCH_ACTION = "sync-branch";
8
+ /**
9
+ * How long a request, or an answer that didn't fail, stands for the next request on the branch,
10
+ * which joins it instead (api/branch-update-from-base.ts).
11
+ */
12
+ export declare const BASE_UPDATE_REQUEST_INTERVAL_MS = 30000;
13
+ /** How long the editor waits on a request, polling for its answer. */
14
+ export declare const BASE_UPDATE_GIVE_UP_MS: number;
15
+ /**
16
+ * How long the worker asks itself for early cycles while a request goes unanswered; its regular
17
+ * cycles answer it after that. Each cycle reads every branch in review from GitHub, so it is short.
18
+ */
19
+ export declare const BASE_UPDATE_EARLY_RETRY_MS: number;
20
+ /**
21
+ * How far in the future a request time may lie and still count: the Lambdas' clocks keep NTP time,
22
+ * so a later one is corrupt and is never waited on.
23
+ */
24
+ export declare const BASE_UPDATE_CLOCK_SKEW_MS = 60000;
25
+ /**
26
+ * How a run in the request (operating-mode `baseUpdateRunsInRequest`) went: it answered the
27
+ * request; a lock held it back; or the branch clones from a remote other than the workspace's
28
+ * `remote.git`, the only one such a run fetches.
29
+ */
30
+ export type BaseUpdateRun = 'answered' | 'busy' | 'other-remote';
31
+ /** Whether the branch's latest request for the latest changes is still unanswered. */
32
+ export declare function isBaseUpdatePending(branch: Pick<BranchMetadata, 'baseUpdateRequestedAt' | 'lastBaseUpdate'>): boolean;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Asking for the latest changes: the API records the request on branch.json and queues this action
3
+ * (api/branch-update-from-base.ts), the worker's sync answers it in `lastBaseUpdate`
4
+ * (worker/rebase.ts), and the editor waits while it stands. Dependency-free, so all three share it.
5
+ */
6
+ export const SYNC_BRANCH_ACTION = 'sync-branch';
7
+ /**
8
+ * How long a request, or an answer that didn't fail, stands for the next request on the branch,
9
+ * which joins it instead (api/branch-update-from-base.ts).
10
+ */
11
+ export const BASE_UPDATE_REQUEST_INTERVAL_MS = 30_000;
12
+ /** How long the editor waits on a request, polling for its answer. */
13
+ export const BASE_UPDATE_GIVE_UP_MS = 10 * 60_000;
14
+ /**
15
+ * How long the worker asks itself for early cycles while a request goes unanswered; its regular
16
+ * cycles answer it after that. Each cycle reads every branch in review from GitHub, so it is short.
17
+ */
18
+ export const BASE_UPDATE_EARLY_RETRY_MS = 2 * 60_000;
19
+ /**
20
+ * How far in the future a request time may lie and still count: the Lambdas' clocks keep NTP time,
21
+ * so a later one is corrupt and is never waited on.
22
+ */
23
+ export const BASE_UPDATE_CLOCK_SKEW_MS = 60_000;
24
+ /** Whether the branch's latest request for the latest changes is still unanswered. */
25
+ export function isBaseUpdatePending(branch) {
26
+ const requestedAt = branch.baseUpdateRequestedAt;
27
+ if (requestedAt === undefined)
28
+ return false;
29
+ const answeredAt = branch.lastBaseUpdate?.at;
30
+ return answeredAt === undefined || Date.parse(answeredAt) < Date.parse(requestedAt);
31
+ }
@@ -83,6 +83,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
83
83
  lastAt: string;
84
84
  }>>;
85
85
  historyRewrittenFrom: z.ZodOptional<z.ZodString>;
86
+ historyRewrittenTo: z.ZodOptional<z.ZodString>;
86
87
  lastGitHubTip: z.ZodOptional<z.ZodString>;
87
88
  keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
88
89
  rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
@@ -121,6 +122,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
121
122
  name?: string | undefined;
122
123
  }>;
123
124
  at: z.ZodString;
125
+ commit: z.ZodOptional<z.ZodString>;
124
126
  }, "strip", z.ZodTypeAny, {
125
127
  at: string;
126
128
  state: "approved" | "changes-requested";
@@ -128,6 +130,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
128
130
  userId: string;
129
131
  name?: string | undefined;
130
132
  };
133
+ commit?: string | undefined;
131
134
  }, {
132
135
  at: string;
133
136
  state: "approved" | "changes-requested";
@@ -135,6 +138,25 @@ export declare const branchMetadataSchema: z.ZodObject<{
135
138
  userId: string;
136
139
  name?: string | undefined;
137
140
  };
141
+ commit?: string | undefined;
142
+ }>>>;
143
+ baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
144
+ latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
145
+ lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
146
+ at: z.ZodString;
147
+ outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
148
+ baseSha: z.ZodOptional<z.ZodString>;
149
+ keptEntries: z.ZodOptional<z.ZodNumber>;
150
+ }, "strip", z.ZodTypeAny, {
151
+ at: string;
152
+ outcome: "merged" | "conflicts" | "up-to-date" | "failed";
153
+ baseSha?: string | undefined;
154
+ keptEntries?: number | undefined;
155
+ }, {
156
+ at: string;
157
+ outcome: "merged" | "conflicts" | "up-to-date" | "failed";
158
+ baseSha?: string | undefined;
159
+ keptEntries?: number | undefined;
138
160
  }>>>;
139
161
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
140
162
  name: z.ZodString;
@@ -199,6 +221,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
199
221
  lastAt: string;
200
222
  }>>;
201
223
  historyRewrittenFrom: z.ZodOptional<z.ZodString>;
224
+ historyRewrittenTo: z.ZodOptional<z.ZodString>;
202
225
  lastGitHubTip: z.ZodOptional<z.ZodString>;
203
226
  keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
204
227
  rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
@@ -237,6 +260,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
237
260
  name?: string | undefined;
238
261
  }>;
239
262
  at: z.ZodString;
263
+ commit: z.ZodOptional<z.ZodString>;
240
264
  }, "strip", z.ZodTypeAny, {
241
265
  at: string;
242
266
  state: "approved" | "changes-requested";
@@ -244,6 +268,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
244
268
  userId: string;
245
269
  name?: string | undefined;
246
270
  };
271
+ commit?: string | undefined;
247
272
  }, {
248
273
  at: string;
249
274
  state: "approved" | "changes-requested";
@@ -251,6 +276,25 @@ export declare const branchMetadataSchema: z.ZodObject<{
251
276
  userId: string;
252
277
  name?: string | undefined;
253
278
  };
279
+ commit?: string | undefined;
280
+ }>>>;
281
+ baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
282
+ latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
283
+ lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
284
+ at: z.ZodString;
285
+ outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
286
+ baseSha: z.ZodOptional<z.ZodString>;
287
+ keptEntries: z.ZodOptional<z.ZodNumber>;
288
+ }, "strip", z.ZodTypeAny, {
289
+ at: string;
290
+ outcome: "merged" | "conflicts" | "up-to-date" | "failed";
291
+ baseSha?: string | undefined;
292
+ keptEntries?: number | undefined;
293
+ }, {
294
+ at: string;
295
+ outcome: "merged" | "conflicts" | "up-to-date" | "failed";
296
+ baseSha?: string | undefined;
297
+ keptEntries?: number | undefined;
254
298
  }>>>;
255
299
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
256
300
  name: z.ZodString;
@@ -315,6 +359,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
315
359
  lastAt: string;
316
360
  }>>;
317
361
  historyRewrittenFrom: z.ZodOptional<z.ZodString>;
362
+ historyRewrittenTo: z.ZodOptional<z.ZodString>;
318
363
  lastGitHubTip: z.ZodOptional<z.ZodString>;
319
364
  keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
320
365
  rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
@@ -353,6 +398,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
353
398
  name?: string | undefined;
354
399
  }>;
355
400
  at: z.ZodString;
401
+ commit: z.ZodOptional<z.ZodString>;
356
402
  }, "strip", z.ZodTypeAny, {
357
403
  at: string;
358
404
  state: "approved" | "changes-requested";
@@ -360,6 +406,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
360
406
  userId: string;
361
407
  name?: string | undefined;
362
408
  };
409
+ commit?: string | undefined;
363
410
  }, {
364
411
  at: string;
365
412
  state: "approved" | "changes-requested";
@@ -367,6 +414,25 @@ export declare const branchMetadataSchema: z.ZodObject<{
367
414
  userId: string;
368
415
  name?: string | undefined;
369
416
  };
417
+ commit?: string | undefined;
418
+ }>>>;
419
+ baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
420
+ latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
421
+ lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
422
+ at: z.ZodString;
423
+ outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
424
+ baseSha: z.ZodOptional<z.ZodString>;
425
+ keptEntries: z.ZodOptional<z.ZodNumber>;
426
+ }, "strip", z.ZodTypeAny, {
427
+ at: string;
428
+ outcome: "merged" | "conflicts" | "up-to-date" | "failed";
429
+ baseSha?: string | undefined;
430
+ keptEntries?: number | undefined;
431
+ }, {
432
+ at: string;
433
+ outcome: "merged" | "conflicts" | "up-to-date" | "failed";
434
+ baseSha?: string | undefined;
435
+ keptEntries?: number | undefined;
370
436
  }>>>;
371
437
  }, z.ZodTypeAny, "passthrough">>;
372
438
  /**
@@ -82,6 +82,7 @@ const branchMetadataShape = {
82
82
  .object({ message: z.string(), firstAt: z.string(), lastAt: z.string() })
83
83
  .optional(),
84
84
  historyRewrittenFrom: z.string().optional(),
85
+ historyRewrittenTo: z.string().optional(),
85
86
  lastGitHubTip: z.string().optional(),
86
87
  keptOverGitHub: z.array(z.string()).optional(),
87
88
  rewrittenOnGitHub: z.string().optional(),
@@ -99,12 +100,26 @@ const branchMetadataShape = {
99
100
  ])
100
101
  .optional()
101
102
  .catch(undefined),
102
- // Dropped when malformed: it only decorates the PR body, so it never fails a read.
103
+ // Dropped when malformed, never failing a read: it decorates the PR body, and without `commit`
104
+ // the worker judges a move against the clone's head before it (worker/github-commits.ts).
103
105
  review: z
104
106
  .object({
105
107
  state: z.enum(['approved', 'changes-requested']),
106
108
  by: z.object({ userId: z.string(), name: z.string().optional() }),
107
109
  at: z.string(),
110
+ commit: z.string().optional(),
111
+ })
112
+ .optional()
113
+ .catch(undefined),
114
+ baseUpdateRequestedAt: z.string().optional().catch(undefined),
115
+ latestChangesWaiting: z.string().optional().catch(undefined),
116
+ // Dropped when malformed: it only ends the editor's wait, which then times out instead.
117
+ lastBaseUpdate: z
118
+ .object({
119
+ at: z.string(),
120
+ outcome: z.enum(['merged', 'up-to-date', 'conflicts', 'failed']),
121
+ baseSha: z.string().optional(),
122
+ keptEntries: z.number().int().nonnegative().optional(),
108
123
  })
109
124
  .optional()
110
125
  .catch(undefined),
@@ -2,6 +2,11 @@ import type { RootCollectionConfig } from './config/index.js';
2
2
  import type { FlatSchemaItem } from './config/types.js';
3
3
  import type { OperatingMode } from './operating-mode/index.js';
4
4
  import type { EntrySchemaRegistry, SchemaIssue, SchemaResolutionResult, UnknownSchemaPolicy } from './schema/types.js';
5
+ /**
6
+ * Bump when BranchSchemaCacheEntry shape changes to auto-invalidate stale caches.
7
+ * @internal Exported for tests.
8
+ */
9
+ export declare const SCHEMA_CACHE_VERSION = 4;
5
10
  /** File name of the schema cache inside {@link schemaCacheDir}. */
6
11
  export declare const SCHEMA_CACHE_FILE = "schema-cache.json";
7
12
  /** resource-generation.ts resource key for the schema cache's marker. */
@@ -32,6 +37,13 @@ export interface ResolvedBranchSchema {
32
37
  flatSchema: FlatSchemaItem[];
33
38
  issues: SchemaIssue[];
34
39
  }
40
+ /**
41
+ * The entry types and collections of a branch's last persisted schema snapshot, for a reader with
42
+ * no entry schema registry to resolve one: the worker's merge, which names conflicts by them.
43
+ * Undefined when there is no snapshot of this version. Its freshness is not checked, so it can lag
44
+ * the tree.
45
+ */
46
+ export declare function readSchemaSnapshot(branchRoot: string): Promise<readonly FlatSchemaItem[] | undefined>;
35
47
  /**
36
48
  * Per-branch schema cache: one file in {@link schemaCacheDir}, the only state
37
49
  * shared across processes. Each process keeps the entries it parsed or wrote,
@@ -10,8 +10,11 @@ import { bumpResourceGeneration, readResourceGeneration, isGenerationCurrent, }
10
10
  import { timeRequestPhase } from './utils/request-timing.js';
11
11
  import { CANOPY_META_DIR } from './utils/git.js';
12
12
  import { canopyLogWarn } from './utils/logger.js';
13
- /** Bump when BranchSchemaCacheEntry shape changes to auto-invalidate stale caches */
14
- const SCHEMA_CACHE_VERSION = 4;
13
+ /**
14
+ * Bump when BranchSchemaCacheEntry shape changes to auto-invalidate stale caches.
15
+ * @internal Exported for tests.
16
+ */
17
+ export const SCHEMA_CACHE_VERSION = 4;
15
18
  /** Minimum interval between mtime staleness checks (ms) */
16
19
  const MTIME_CHECK_DEBOUNCE_MS = 1000;
17
20
  /** Cap on the parsed snapshots one process keeps, counted in bytes of their files. */
@@ -89,6 +92,28 @@ async function schemaCacheDir(branchRoot) {
89
92
  const isClone = await fs.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
90
93
  return isClone ? path.join(gitDir, 'canopycms') : path.join(branchRoot, CANOPY_META_DIR);
91
94
  }
95
+ /**
96
+ * The entry types and collections of a branch's last persisted schema snapshot, for a reader with
97
+ * no entry schema registry to resolve one: the worker's merge, which names conflicts by them.
98
+ * Undefined when there is no snapshot of this version. Its freshness is not checked, so it can lag
99
+ * the tree.
100
+ */
101
+ export async function readSchemaSnapshot(branchRoot) {
102
+ try {
103
+ const cachePath = path.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
104
+ const entry = JSON.parse(await fs.readFile(cachePath, 'utf-8'));
105
+ if (typeof entry !== 'object' || entry === null)
106
+ return undefined;
107
+ if (!('version' in entry) || entry.version !== SCHEMA_CACHE_VERSION)
108
+ return undefined;
109
+ if (!('flatSchema' in entry) || !Array.isArray(entry.flatSchema))
110
+ return undefined;
111
+ return entry.flatSchema;
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ }
92
117
  /**
93
118
  * Per-branch schema cache: one file in {@link schemaCacheDir}, the only state
94
119
  * shared across processes. Each process keeps the entries it parsed or wrote,
@@ -0,0 +1,77 @@
1
+ /**
2
+ * CLI command: npx canopycms backfill-item-keys [--check] [--force] [--app-dir <dir>]
3
+ *
4
+ * Writes the `_key` of every object-list item and block (`item-keys.ts`) into the site's content,
5
+ * so an editor's first save of an entry adds no key lines and a merge sees only real edits.
6
+ *
7
+ * Each entry goes through the editor's own save path, with nothing re-derived:
8
+ * `ContentStore.read` keys the items exactly as the editor's form holds them, and
9
+ * `ContentStore.write` writes that data back with the read's version, through the serialiser,
10
+ * content-write lock and version check a save uses, after the content API's own
11
+ * `entryDataAsSaved`. So a written file is byte for byte what an editor save of the unedited entry
12
+ * leaves, a second run finds every item keyed and writes nothing, and the next editor save adds no
13
+ * lines of its own, a CRLF markdown file aside (safeinsights/canopycms#1234).
14
+ *
15
+ * A file is written only when its items lack keys. The write then also carries whatever else an
16
+ * unedited save normalises (a one-line JSON file comes out pretty-printed, a stray frontmatter key
17
+ * named as the body goes), and those files are listed apart for review. Out of scope, as for the editor: list fields the schema does not
18
+ * describe, `.collection.json`, and entries of a type this code's registry lacks.
19
+ */
20
+ import type { FlatSchemaItem } from '../config/index.js';
21
+ /** One entry file the backfill keys. */
22
+ interface BackfillFileResult {
23
+ /** Path relative to the project root. */
24
+ path: string;
25
+ /** Items and blocks whose file text gives them no usable key, or one an earlier item holds. */
26
+ itemsKeyed: number;
27
+ /** Lines the write changes besides adding `_key` lines: what else an unedited save normalises. */
28
+ otherLines: number;
29
+ }
30
+ interface BackfillResult {
31
+ /** Files written, or with `check`, files a run would write; by path. */
32
+ changed: BackfillFileResult[];
33
+ /** Entries that could not be read or written, and collections that could not be listed. */
34
+ failed: {
35
+ path: string;
36
+ error: string;
37
+ }[];
38
+ /** Entries left alone because the registry has no schema for their entry type. */
39
+ unavailable: string[];
40
+ /** Files in a collection that look like entries but whose names don't parse as one. */
41
+ malformed: string[];
42
+ }
43
+ interface BackfillOptions {
44
+ /** The project root: the directory holding the content root. */
45
+ root: string;
46
+ flatSchema: FlatSchemaItem[];
47
+ contentRootName: string;
48
+ /** Report what a run would write, and write nothing. */
49
+ check: boolean;
50
+ }
51
+ /**
52
+ * Keys every entry's items under `root`, or with `check`, reports which files a run would write.
53
+ * @internal Exported for tests.
54
+ */
55
+ export declare function backfillItemKeys(options: BackfillOptions): Promise<BackfillResult>;
56
+ export interface BackfillItemKeysCLIOptions {
57
+ projectDir: string;
58
+ /** Directory holding `schemas.ts`, relative to the project. Default `app`. */
59
+ appDir?: string;
60
+ check?: boolean;
61
+ /** Write even when the content tree has uncommitted changes. */
62
+ force?: boolean;
63
+ }
64
+ /**
65
+ * Exit codes. `--check`: 0 when no file needs keys, 1 when some would change. A run: 0 when done,
66
+ * 1 when it would not write: uncommitted content changes, or no git to ask. Both: 2 when the
67
+ * site's config or schema could not be loaded, a collection listed, or an entry read or written.
68
+ * @internal Exported for tests.
69
+ */
70
+ export declare const BACKFILL_EXIT_CODES: {
71
+ readonly ok: 0;
72
+ readonly changesOrRefused: 1;
73
+ readonly loadError: 2;
74
+ };
75
+ /** Runs the command and returns its exit code. */
76
+ export declare function backfillItemKeysCLI(options: BackfillItemKeysCLIOptions): Promise<number>;
77
+ export {};