canopycms 0.0.67-int.90 → 0.0.67

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 (539) hide show
  1. package/README.md +30 -3
  2. package/dist/ai/generate.js +1 -27
  3. package/dist/ai/handler.d.ts +0 -4
  4. package/dist/ai/handler.js +0 -11
  5. package/dist/ai/index.d.ts +1 -8
  6. package/dist/ai/index.js +1 -8
  7. package/dist/ai/json-to-markdown.js +15 -58
  8. package/dist/ai/resolve-branch.d.ts +2 -7
  9. package/dist/ai/resolve-branch.js +2 -8
  10. package/dist/ai/strip-mdx.d.ts +5 -13
  11. package/dist/ai/strip-mdx.js +5 -17
  12. package/dist/ai/to-plain-text.d.ts +10 -28
  13. package/dist/ai/to-plain-text.js +10 -28
  14. package/dist/ai/transform-components.d.ts +2 -4
  15. package/dist/ai/transform-components.js +8 -19
  16. package/dist/ai/types.d.ts +5 -10
  17. package/dist/api/admin-branch-health.d.ts +4 -12
  18. package/dist/api/admin-branch-health.js +26 -66
  19. package/dist/api/admin.d.ts +4 -12
  20. package/dist/api/admin.js +10 -26
  21. package/dist/api/assets.d.ts +0 -25
  22. package/dist/api/assets.js +50 -91
  23. package/dist/api/branch-merge.d.ts +1 -1
  24. package/dist/api/branch-merge.js +0 -2
  25. package/dist/api/branch-review.d.ts +0 -8
  26. package/dist/api/branch-review.js +0 -13
  27. package/dist/api/branch-status.d.ts +0 -3
  28. package/dist/api/branch-status.js +8 -33
  29. package/dist/api/branch-withdraw.d.ts +0 -1
  30. package/dist/api/branch-withdraw.js +6 -9
  31. package/dist/api/branch.d.ts +7 -12
  32. package/dist/api/branch.js +7 -44
  33. package/dist/api/client.d.ts +71 -224
  34. package/dist/api/client.js +82 -233
  35. package/dist/api/comments.d.ts +2 -4
  36. package/dist/api/comments.js +0 -17
  37. package/dist/api/content.d.ts +0 -6
  38. package/dist/api/content.js +66 -142
  39. package/dist/api/entries-constants.d.ts +5 -9
  40. package/dist/api/entries-constants.js +5 -9
  41. package/dist/api/entries.d.ts +8 -13
  42. package/dist/api/entries.js +20 -51
  43. package/dist/api/github-sync.d.ts +2 -4
  44. package/dist/api/github-sync.js +4 -8
  45. package/dist/api/groups.d.ts +3 -0
  46. package/dist/api/groups.js +8 -21
  47. package/dist/api/guards.d.ts +4 -16
  48. package/dist/api/guards.js +3 -22
  49. package/dist/api/index.d.ts +4 -4
  50. package/dist/api/permissions.d.ts +2 -0
  51. package/dist/api/permissions.js +18 -40
  52. package/dist/api/reference-options.d.ts +0 -1
  53. package/dist/api/reference-options.js +3 -11
  54. package/dist/api/request-body-hash.d.ts +12 -23
  55. package/dist/api/request-body-hash.js +12 -23
  56. package/dist/api/resolve-references.d.ts +0 -1
  57. package/dist/api/resolve-references.js +13 -28
  58. package/dist/api/route-builder.d.ts +3 -7
  59. package/dist/api/route-builder.js +1 -1
  60. package/dist/api/routes.d.ts +12 -0
  61. package/dist/api/routes.js +47 -0
  62. package/dist/api/schema.d.ts +23 -33
  63. package/dist/api/schema.js +20 -74
  64. package/dist/api/settings-helpers.js +4 -5
  65. package/dist/api/types.d.ts +0 -5
  66. package/dist/api/user.js +0 -5
  67. package/dist/api/validators.d.ts +14 -52
  68. package/dist/api/validators.js +14 -52
  69. package/dist/assets/asset-prefixes.d.ts +4 -8
  70. package/dist/assets/asset-prefixes.js +4 -8
  71. package/dist/assets/asset-src.d.ts +5 -22
  72. package/dist/assets/asset-src.js +5 -21
  73. package/dist/assets/factory.d.ts +1 -2
  74. package/dist/assets/factory.js +1 -2
  75. package/dist/assets/finalize.d.ts +3 -1
  76. package/dist/assets/finalize.js +3 -1
  77. package/dist/assets/index.d.ts +3 -3
  78. package/dist/assets/index.js +1 -1
  79. package/dist/assets/keys.d.ts +5 -3
  80. package/dist/assets/keys.js +6 -3
  81. package/dist/assets/pipeline.d.ts +3 -2
  82. package/dist/assets/pipeline.js +22 -56
  83. package/dist/assets/sharp-loader.d.ts +2 -10
  84. package/dist/assets/sharp-loader.js +2 -10
  85. package/dist/assets/store-local.d.ts +1 -2
  86. package/dist/assets/store-local.js +1 -2
  87. package/dist/assets/store-s3.d.ts +3 -4
  88. package/dist/assets/svg-sanitizer.d.ts +6 -14
  89. package/dist/assets/svg-sanitizer.js +6 -14
  90. package/dist/assets/transform-directives.d.ts +3 -4
  91. package/dist/assets/transform-directives.js +1 -1
  92. package/dist/assets/transform.d.ts +19 -30
  93. package/dist/assets/transform.js +18 -31
  94. package/dist/auth/cache.d.ts +2 -3
  95. package/dist/auth/cache.js +2 -3
  96. package/dist/auth/caching-auth-plugin.d.ts +11 -22
  97. package/dist/auth/caching-auth-plugin.js +12 -14
  98. package/dist/auth/context-helpers.d.ts +6 -20
  99. package/dist/auth/context-helpers.js +5 -17
  100. package/dist/auth/file-based-auth-cache.d.ts +10 -21
  101. package/dist/auth/file-based-auth-cache.js +23 -51
  102. package/dist/auth/plugin.d.ts +29 -61
  103. package/dist/auth/plugin.js +9 -13
  104. package/dist/auth/types.d.ts +11 -20
  105. package/dist/authorization/branch.d.ts +30 -49
  106. package/dist/authorization/branch.js +16 -42
  107. package/dist/authorization/content.d.ts +11 -40
  108. package/dist/authorization/content.js +13 -45
  109. package/dist/authorization/groups/index.d.ts +0 -3
  110. package/dist/authorization/groups/index.js +0 -3
  111. package/dist/authorization/groups/loader.d.ts +14 -28
  112. package/dist/authorization/groups/loader.js +14 -38
  113. package/dist/authorization/groups/schema.d.ts +0 -6
  114. package/dist/authorization/groups/schema.js +4 -7
  115. package/dist/authorization/helpers.d.ts +9 -28
  116. package/dist/authorization/helpers.js +9 -28
  117. package/dist/authorization/index.d.ts +3 -25
  118. package/dist/authorization/index.js +3 -35
  119. package/dist/authorization/path.d.ts +6 -13
  120. package/dist/authorization/path.js +8 -25
  121. package/dist/authorization/permissions/index.d.ts +0 -3
  122. package/dist/authorization/permissions/index.js +0 -3
  123. package/dist/authorization/permissions/loader.d.ts +14 -27
  124. package/dist/authorization/permissions/loader.js +14 -32
  125. package/dist/authorization/permissions/schema.d.ts +1 -6
  126. package/dist/authorization/permissions/schema.js +6 -13
  127. package/dist/authorization/protected-branch.d.ts +36 -65
  128. package/dist/authorization/protected-branch.js +29 -41
  129. package/dist/authorization/settings-file-store.d.ts +38 -81
  130. package/dist/authorization/settings-file-store.js +38 -79
  131. package/dist/authorization/types.d.ts +4 -26
  132. package/dist/authorization/types.js +0 -6
  133. package/dist/authorization/validation.d.ts +1 -23
  134. package/dist/authorization/validation.js +1 -24
  135. package/dist/branch-health.d.ts +39 -67
  136. package/dist/branch-health.js +27 -43
  137. package/dist/branch-metadata-file.d.ts +16 -32
  138. package/dist/branch-metadata-file.js +13 -26
  139. package/dist/branch-metadata.d.ts +49 -80
  140. package/dist/branch-metadata.js +57 -87
  141. package/dist/branch-registry.d.ts +26 -64
  142. package/dist/branch-registry.js +44 -93
  143. package/dist/branch-schema-cache.d.ts +34 -124
  144. package/dist/branch-schema-cache.js +49 -155
  145. package/dist/branch-workspace.d.ts +4 -4
  146. package/dist/branch-workspace.js +15 -19
  147. package/dist/build/generate-ai-content.d.ts +1 -0
  148. package/dist/build/generate-ai-content.js +12 -25
  149. package/dist/build-canopy.d.ts +17 -27
  150. package/dist/build-canopy.js +16 -23
  151. package/dist/build-mode.d.ts +25 -27
  152. package/dist/build-mode.js +25 -29
  153. package/dist/cli/cli.d.ts +15 -26
  154. package/dist/cli/cli.js +601 -978
  155. package/dist/cli/generate-ai-content.js +351 -628
  156. package/dist/cli/github-app-manifest.d.ts +84 -0
  157. package/dist/cli/github-app-manifest.js +161 -0
  158. package/dist/cli/init-github-app.d.ts +110 -444
  159. package/dist/cli/init-github-app.js +236 -773
  160. package/dist/cli/init.js +39 -80
  161. package/dist/cli/migrate.d.ts +13 -10
  162. package/dist/cli/migrate.js +22 -30
  163. package/dist/cli/project-detect.d.ts +17 -31
  164. package/dist/cli/project-detect.js +31 -42
  165. package/dist/cli/project-root.d.ts +4 -11
  166. package/dist/cli/project-root.js +4 -11
  167. package/dist/cli/prompt.d.ts +10 -0
  168. package/dist/cli/prompt.js +67 -0
  169. package/dist/cli/sync.d.ts +0 -8
  170. package/dist/cli/sync.js +8 -28
  171. package/dist/cli/template-files/middleware.ts.template +15 -29
  172. package/dist/cli/templates.d.ts +2 -12
  173. package/dist/cli/templates.js +4 -4
  174. package/dist/client.d.ts +1 -1
  175. package/dist/comment-store.d.ts +32 -53
  176. package/dist/comment-store.js +32 -52
  177. package/dist/config/flatten.d.ts +0 -3
  178. package/dist/config/flatten.js +2 -12
  179. package/dist/config/helpers.js +2 -2
  180. package/dist/config/index.d.ts +2 -2
  181. package/dist/config/index.js +1 -4
  182. package/dist/config/schemas/collection.d.ts +0 -8
  183. package/dist/config/schemas/collection.js +0 -10
  184. package/dist/config/schemas/config.d.ts +6 -78
  185. package/dist/config/schemas/config.js +38 -54
  186. package/dist/config/schemas/field.d.ts +2 -242
  187. package/dist/config/schemas/field.js +9 -20
  188. package/dist/config/schemas/media.js +1 -1
  189. package/dist/config/schemas/permissions.js +0 -2
  190. package/dist/config/schemas/url.d.ts +5 -6
  191. package/dist/config/schemas/url.js +5 -6
  192. package/dist/config/types.d.ts +45 -33
  193. package/dist/config/types.js +0 -1
  194. package/dist/config.d.ts +1 -8
  195. package/dist/config.js +1 -9
  196. package/dist/content-id-index.d.ts +76 -188
  197. package/dist/content-id-index.js +79 -230
  198. package/dist/content-index-generation.d.ts +27 -48
  199. package/dist/content-index-generation.js +35 -84
  200. package/dist/content-index-registry.d.ts +15 -24
  201. package/dist/content-index-registry.js +5 -9
  202. package/dist/content-listing.d.ts +64 -107
  203. package/dist/content-listing.js +68 -118
  204. package/dist/content-reader.d.ts +20 -29
  205. package/dist/content-reader.js +10 -18
  206. package/dist/content-store.d.ts +210 -336
  207. package/dist/content-store.js +413 -657
  208. package/dist/content-tree.d.ts +44 -86
  209. package/dist/content-tree.js +26 -54
  210. package/dist/context.d.ts +67 -94
  211. package/dist/context.js +53 -72
  212. package/dist/dev-content-watcher.d.ts +18 -18
  213. package/dist/dev-content-watcher.js +41 -44
  214. package/dist/editor/BranchManager.d.ts +3 -6
  215. package/dist/editor/BranchManager.js +24 -44
  216. package/dist/editor/CanopyEditor.js +5 -2
  217. package/dist/editor/CommentsPanel.js +0 -2
  218. package/dist/editor/Editor.js +19 -76
  219. package/dist/editor/EditorAuthGate.d.ts +27 -0
  220. package/dist/editor/EditorAuthGate.js +140 -0
  221. package/dist/editor/EntryNavigator.js +0 -20
  222. package/dist/editor/FormRenderer.d.ts +1 -2
  223. package/dist/editor/FormRenderer.js +8 -23
  224. package/dist/editor/admin/SystemHealthPanel.js +20 -37
  225. package/dist/editor/admin/useSystemHealth.d.ts +2 -3
  226. package/dist/editor/admin/useSystemHealth.js +10 -12
  227. package/dist/editor/client-reference-resolver.d.ts +2 -5
  228. package/dist/editor/client-reference-resolver.js +2 -19
  229. package/dist/editor/comments/BranchComments.js +0 -4
  230. package/dist/editor/comments/EntryComments.js +0 -4
  231. package/dist/editor/comments/FieldWrapper.js +0 -4
  232. package/dist/editor/comments/InlineCommentThread.js +0 -2
  233. package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
  234. package/dist/editor/comments/ThreadCarousel.js +0 -12
  235. package/dist/editor/components/EditorFooter.d.ts +0 -8
  236. package/dist/editor/components/EditorFooter.js +0 -5
  237. package/dist/editor/components/EditorHeader.d.ts +0 -31
  238. package/dist/editor/components/EditorHeader.js +21 -71
  239. package/dist/editor/components/EditorSidebar.d.ts +0 -19
  240. package/dist/editor/components/EditorSidebar.js +0 -16
  241. package/dist/editor/components/EntryCreateModal.d.ts +4 -4
  242. package/dist/editor/components/EntryCreateModal.js +7 -22
  243. package/dist/editor/components/RenameEntryModal.js +0 -4
  244. package/dist/editor/components/UserBadge.js +0 -10
  245. package/dist/editor/components/index.js +0 -2
  246. package/dist/editor/context/ApiClientContext.d.ts +8 -7
  247. package/dist/editor/context/ApiClientContext.js +39 -11
  248. package/dist/editor/context/AssetContext.d.ts +7 -17
  249. package/dist/editor/context/AssetContext.js +7 -17
  250. package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
  251. package/dist/editor/context/EditorIdentityContext.js +7 -0
  252. package/dist/editor/context/EditorStateContext.d.ts +10 -41
  253. package/dist/editor/context/EditorStateContext.js +7 -23
  254. package/dist/editor/context/SWRProvider.d.ts +3 -6
  255. package/dist/editor/context/index.d.ts +1 -20
  256. package/dist/editor/context/index.js +1 -20
  257. package/dist/editor/editor-utils.d.ts +8 -47
  258. package/dist/editor/editor-utils.js +19 -72
  259. package/dist/editor/fields/BlockField.d.ts +2 -2
  260. package/dist/editor/fields/BlockField.js +1 -2
  261. package/dist/editor/fields/CodeField.d.ts +0 -1
  262. package/dist/editor/fields/CodeField.js +0 -1
  263. package/dist/editor/fields/DateTimeField.d.ts +15 -22
  264. package/dist/editor/fields/DateTimeField.js +17 -26
  265. package/dist/editor/fields/ImageField.d.ts +2 -2
  266. package/dist/editor/fields/ImageField.js +8 -14
  267. package/dist/editor/fields/InlineGroupField.d.ts +0 -1
  268. package/dist/editor/fields/InlineGroupField.js +0 -1
  269. package/dist/editor/fields/MarkdownField.d.ts +0 -1
  270. package/dist/editor/fields/MarkdownField.js +6 -11
  271. package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
  272. package/dist/editor/fields/MdxImageDialog.js +0 -1
  273. package/dist/editor/fields/NumberField.d.ts +9 -19
  274. package/dist/editor/fields/NumberField.js +11 -22
  275. package/dist/editor/fields/NumberListField.d.ts +7 -10
  276. package/dist/editor/fields/NumberListField.js +7 -10
  277. package/dist/editor/fields/ObjectField.d.ts +5 -8
  278. package/dist/editor/fields/ObjectField.js +0 -1
  279. package/dist/editor/fields/ReferenceField.d.ts +1 -1
  280. package/dist/editor/fields/ReferenceField.js +11 -16
  281. package/dist/editor/fields/SelectField.d.ts +2 -2
  282. package/dist/editor/fields/SelectField.js +0 -1
  283. package/dist/editor/fields/StringListField.d.ts +0 -4
  284. package/dist/editor/fields/StringListField.js +4 -9
  285. package/dist/editor/fields/TextField.d.ts +0 -1
  286. package/dist/editor/fields/TextField.js +0 -1
  287. package/dist/editor/fields/ToggleField.d.ts +0 -1
  288. package/dist/editor/fields/ToggleField.js +0 -1
  289. package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
  290. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
  291. package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
  292. package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
  293. package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
  294. package/dist/editor/group-manager/GroupCard.d.ts +0 -3
  295. package/dist/editor/group-manager/GroupForm.d.ts +0 -3
  296. package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
  297. package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
  298. package/dist/editor/group-manager/MemberList.d.ts +0 -3
  299. package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
  300. package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
  301. package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
  302. package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
  303. package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
  304. package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
  305. package/dist/editor/group-manager/index.d.ts +0 -6
  306. package/dist/editor/group-manager/index.js +0 -10
  307. package/dist/editor/group-manager/types.d.ts +2 -5
  308. package/dist/editor/group-manager/types.js +0 -3
  309. package/dist/editor/hooks/index.d.ts +6 -11
  310. package/dist/editor/hooks/index.js +6 -12
  311. package/dist/editor/hooks/useBranchActions.d.ts +0 -14
  312. package/dist/editor/hooks/useBranchActions.js +0 -22
  313. package/dist/editor/hooks/useBranchManager.d.ts +2 -48
  314. package/dist/editor/hooks/useBranchManager.js +4 -64
  315. package/dist/editor/hooks/useBranchesData.d.ts +7 -6
  316. package/dist/editor/hooks/useBranchesData.js +7 -6
  317. package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
  318. package/dist/editor/hooks/useCommentSystem.js +1 -36
  319. package/dist/editor/hooks/useDraftManager.d.ts +2 -35
  320. package/dist/editor/hooks/useDraftManager.js +23 -64
  321. package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
  322. package/dist/editor/hooks/useEditorLayout.js +0 -23
  323. package/dist/editor/hooks/useEntriesData.d.ts +1 -0
  324. package/dist/editor/hooks/useEntriesData.js +2 -3
  325. package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
  326. package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
  327. package/dist/editor/hooks/useEntryManager.d.ts +2 -27
  328. package/dist/editor/hooks/useEntryManager.js +29 -78
  329. package/dist/editor/hooks/useGroupManager.d.ts +0 -21
  330. package/dist/editor/hooks/useGroupManager.js +0 -21
  331. package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
  332. package/dist/editor/hooks/usePermissionManager.js +0 -20
  333. package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
  334. package/dist/editor/hooks/useReferenceResolution.js +1 -48
  335. package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
  336. package/dist/editor/hooks/useSchemaManager.js +0 -19
  337. package/dist/editor/hooks/useUserContext.d.ts +2 -9
  338. package/dist/editor/hooks/useUserContext.js +10 -10
  339. package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
  340. package/dist/editor/hooks/useUserMetadata.js +0 -3
  341. package/dist/editor/media/AssetCard.d.ts +0 -1
  342. package/dist/editor/media/AssetCard.js +1 -2
  343. package/dist/editor/media/CropStep.d.ts +0 -1
  344. package/dist/editor/media/CropStep.js +0 -1
  345. package/dist/editor/media/MediaLibrary.d.ts +1 -2
  346. package/dist/editor/media/MediaLibrary.js +1 -2
  347. package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
  348. package/dist/editor/media/MediaLibraryBody.js +0 -1
  349. package/dist/editor/media/crop-math.d.ts +1 -1
  350. package/dist/editor/media/crop-math.js +1 -1
  351. package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
  352. package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
  353. package/dist/editor/permission-manager/PermissionEditor.js +0 -1
  354. package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
  355. package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
  356. package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
  357. package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
  358. package/dist/editor/permission-manager/constants.d.ts +0 -3
  359. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
  360. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
  361. package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
  362. package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
  363. package/dist/editor/permission-manager/index.d.ts +0 -7
  364. package/dist/editor/permission-manager/index.js +0 -16
  365. package/dist/editor/permission-manager/types.d.ts +2 -12
  366. package/dist/editor/permission-manager/types.js +0 -3
  367. package/dist/editor/permission-manager/utils.d.ts +0 -28
  368. package/dist/editor/permission-manager/utils.js +9 -23
  369. package/dist/editor/preview-bridge.d.ts +5 -1
  370. package/dist/editor/preview-bridge.js +4 -4
  371. package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
  372. package/dist/editor/schema-editor/CollectionEditor.js +9 -31
  373. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
  374. package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
  375. package/dist/editor/schema-editor/index.d.ts +0 -7
  376. package/dist/editor/schema-editor/index.js +0 -7
  377. package/dist/editor/utils/env.d.ts +3 -18
  378. package/dist/editor/utils/env.js +6 -25
  379. package/dist/entry-link-resolver.d.ts +13 -35
  380. package/dist/entry-link-resolver.js +20 -53
  381. package/dist/entry-schema-registry.d.ts +20 -60
  382. package/dist/entry-schema-registry.js +20 -69
  383. package/dist/entry-schema.d.ts +118 -223
  384. package/dist/entry-schema.js +46 -91
  385. package/dist/git-manager.d.ts +101 -200
  386. package/dist/git-manager.js +211 -384
  387. package/dist/github-service.d.ts +34 -75
  388. package/dist/github-service.js +37 -91
  389. package/dist/http/handler.d.ts +6 -37
  390. package/dist/http/handler.js +65 -120
  391. package/dist/http/router.d.ts +15 -32
  392. package/dist/http/router.js +34 -99
  393. package/dist/http/types.d.ts +15 -40
  394. package/dist/http/types.js +3 -7
  395. package/dist/id.d.ts +4 -10
  396. package/dist/id.js +4 -10
  397. package/dist/index.js +6 -9
  398. package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
  399. package/dist/operating-mode/client-safe-strategy.js +8 -28
  400. package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
  401. package/dist/operating-mode/client-unsafe-strategy.js +9 -34
  402. package/dist/operating-mode/client.d.ts +3 -10
  403. package/dist/operating-mode/client.js +3 -8
  404. package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
  405. package/dist/operating-mode/deployment-name-fixtures.js +14 -18
  406. package/dist/operating-mode/deployment-name.d.ts +19 -29
  407. package/dist/operating-mode/deployment-name.js +29 -47
  408. package/dist/operating-mode/index.d.ts +4 -25
  409. package/dist/operating-mode/index.js +9 -31
  410. package/dist/operating-mode/mode-env.d.ts +32 -54
  411. package/dist/operating-mode/mode-env.js +36 -58
  412. package/dist/operating-mode/types.d.ts +24 -72
  413. package/dist/operating-mode/types.js +1 -7
  414. package/dist/paths/branch-name.d.ts +30 -40
  415. package/dist/paths/branch-name.js +30 -40
  416. package/dist/paths/branch.d.ts +4 -18
  417. package/dist/paths/branch.js +6 -22
  418. package/dist/paths/index.d.ts +2 -13
  419. package/dist/paths/index.js +3 -23
  420. package/dist/paths/normalize-server.d.ts +3 -15
  421. package/dist/paths/normalize-server.js +3 -15
  422. package/dist/paths/normalize.d.ts +13 -33
  423. package/dist/paths/normalize.js +13 -33
  424. package/dist/paths/resolve.d.ts +4 -17
  425. package/dist/paths/resolve.js +4 -22
  426. package/dist/paths/types.d.ts +12 -34
  427. package/dist/paths/types.js +5 -7
  428. package/dist/paths/validation.d.ts +22 -119
  429. package/dist/paths/validation.js +30 -148
  430. package/dist/reference-resolver.d.ts +0 -13
  431. package/dist/reference-resolver.js +0 -18
  432. package/dist/resolve-canopy-user.d.ts +16 -18
  433. package/dist/resolve-canopy-user.js +17 -26
  434. package/dist/resource-generation.d.ts +16 -25
  435. package/dist/resource-generation.js +34 -97
  436. package/dist/schema/meta-loader.d.ts +2 -14
  437. package/dist/schema/meta-loader.js +15 -64
  438. package/dist/schema/resolver.d.ts +1 -4
  439. package/dist/schema/resolver.js +1 -6
  440. package/dist/schema/schema-store-types.d.ts +0 -12
  441. package/dist/schema/schema-store.d.ts +61 -138
  442. package/dist/schema/schema-store.js +85 -282
  443. package/dist/schema/types.d.ts +0 -4
  444. package/dist/server.d.ts +62 -93
  445. package/dist/server.js +63 -94
  446. package/dist/services.d.ts +23 -47
  447. package/dist/services.js +55 -98
  448. package/dist/settings-workspace.d.ts +17 -30
  449. package/dist/settings-workspace.js +51 -81
  450. package/dist/static/index.d.ts +26 -52
  451. package/dist/static/index.js +33 -67
  452. package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
  453. package/dist/task-queue/cms-task-queue.js +13 -0
  454. package/dist/task-queue/task-queue-config.d.ts +3 -0
  455. package/dist/{worker → task-queue}/task-queue-config.js +1 -6
  456. package/dist/task-queue/task-queue.d.ts +16 -22
  457. package/dist/task-queue/task-queue.js +20 -44
  458. package/dist/task-queue/types.d.ts +0 -1
  459. package/dist/task-queue/worker-status.d.ts +35 -0
  460. package/dist/task-queue/worker-status.js +40 -0
  461. package/dist/types.d.ts +40 -51
  462. package/dist/url-collision.d.ts +26 -40
  463. package/dist/url-collision.js +47 -63
  464. package/dist/url-exclusivity-fixtures.d.ts +15 -20
  465. package/dist/url-exclusivity-fixtures.js +14 -19
  466. package/dist/url-path-resolver.d.ts +8 -16
  467. package/dist/url-path-resolver.js +19 -29
  468. package/dist/user.d.ts +8 -21
  469. package/dist/user.js +10 -19
  470. package/dist/utils/async-mutex.js +4 -6
  471. package/dist/utils/atomic-write.d.ts +2 -11
  472. package/dist/utils/atomic-write.js +2 -11
  473. package/dist/utils/body-field.d.ts +8 -15
  474. package/dist/utils/body-field.js +8 -15
  475. package/dist/utils/content-serialize.d.ts +18 -24
  476. package/dist/utils/content-serialize.js +109 -160
  477. package/dist/utils/content-write-lock.d.ts +51 -106
  478. package/dist/utils/content-write-lock.js +66 -111
  479. package/dist/utils/debug.d.ts +7 -23
  480. package/dist/utils/debug.js +7 -23
  481. package/dist/utils/entry-url.d.ts +11 -23
  482. package/dist/utils/entry-url.js +11 -26
  483. package/dist/utils/error.d.ts +25 -90
  484. package/dist/utils/error.js +60 -147
  485. package/dist/utils/flatten-group-fields.d.ts +5 -7
  486. package/dist/utils/flatten-group-fields.js +5 -7
  487. package/dist/utils/format.d.ts +1 -5
  488. package/dist/utils/format.js +1 -5
  489. package/dist/utils/fs.d.ts +3 -4
  490. package/dist/utils/fs.js +3 -4
  491. package/dist/utils/git.d.ts +52 -83
  492. package/dist/utils/git.js +79 -125
  493. package/dist/utils/logger.d.ts +35 -50
  494. package/dist/utils/logger.js +35 -52
  495. package/dist/utils/provisioning-lock.d.ts +23 -35
  496. package/dist/utils/provisioning-lock.js +49 -69
  497. package/dist/utils/sanitize-href.d.ts +5 -10
  498. package/dist/utils/sanitize-href.js +8 -18
  499. package/dist/utils/title-field.d.ts +17 -20
  500. package/dist/utils/title-field.js +18 -27
  501. package/dist/utils/typed-filename.d.ts +20 -48
  502. package/dist/utils/typed-filename.js +25 -61
  503. package/dist/utils/url-prefix.d.ts +3 -3
  504. package/dist/utils/url-prefix.js +3 -3
  505. package/dist/validation/block-structural-keys.d.ts +16 -31
  506. package/dist/validation/block-structural-keys.js +19 -29
  507. package/dist/validation/deletion-checker.d.ts +0 -34
  508. package/dist/validation/deletion-checker.js +1 -43
  509. package/dist/validation/entry-link-validator.d.ts +6 -13
  510. package/dist/validation/entry-link-validator.js +4 -13
  511. package/dist/validation/entry-type-reference-validator.d.ts +13 -18
  512. package/dist/validation/entry-type-reference-validator.js +13 -18
  513. package/dist/validation/entry-validator.d.ts +13 -25
  514. package/dist/validation/entry-validator.js +15 -32
  515. package/dist/validation/field-traversal.d.ts +3 -30
  516. package/dist/validation/field-traversal.js +1 -32
  517. package/dist/validation/reference-validator.d.ts +5 -24
  518. package/dist/validation/reference-validator.js +5 -30
  519. package/dist/worker/cms-worker.d.ts +172 -232
  520. package/dist/worker/cms-worker.js +241 -333
  521. package/dist/worker/git-sync.d.ts +32 -133
  522. package/dist/worker/git-sync.js +129 -191
  523. package/dist/worker/github-auth.d.ts +13 -19
  524. package/dist/worker/github-auth.js +13 -19
  525. package/dist/worker/history-rewrite.d.ts +53 -66
  526. package/dist/worker/history-rewrite.js +54 -60
  527. package/dist/worker/log.d.ts +24 -41
  528. package/dist/worker/log.js +25 -42
  529. package/dist/worker/rebase.d.ts +20 -110
  530. package/dist/worker/rebase.js +279 -354
  531. package/dist/worker/task-runner.d.ts +32 -66
  532. package/dist/worker/task-runner.js +126 -162
  533. package/dist/worker/worker-context.d.ts +41 -81
  534. package/package.json +4 -4
  535. package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
  536. package/dist/worker/task-queue-config.d.ts +0 -8
  537. package/dist/worker/task-queue.js +0 -19
  538. package/dist/worker/worker-status.d.ts +0 -47
  539. package/dist/worker/worker-status.js +0 -52
@@ -12,16 +12,15 @@ import { isRebaseInProgress } from '../utils/git.js';
12
12
  import { enqueueGitHubPush, forcePublishToLocalRemote, markHistoryRewritten, readPublishedSha, reconcilePendingRewrite, } from './history-rewrite.js';
13
13
  import { workerLog, workerLogWarn } from './log.js';
14
14
  /**
15
- * Poll GitHub for a submitted/approved branch's PR resolution.
15
+ * Poll GitHub for a submitted/approved branch's PR resolution. Those branches
16
+ * sit outside the rebase loop and get no other signal that their PR resolved --
17
+ * nothing pushes a merge/close webhook back into the branch workspace.
16
18
  *
17
- * submitted/approved branches sit outside the rebase loop and get no
18
- * other signal that their PR resolved on GitHub -- nothing pushes a
19
- * merge/close webhook back into the branch workspace. merged ->
20
- * auto-archive via buildMergedBranchUpdate (shared with the manual
21
- * markAsMerged API so both paths produce identical archived-branch
22
- * metadata). closed-without-merge -> record pullRequestState only; an
23
- * admin decides the workflow transition from there. Best-effort: any
24
- * failure here is logged and swallowed, retried next sync cycle.
19
+ * merged -> auto-archive via buildMergedBranchUpdate, shared with the manual
20
+ * markAsMerged API so both paths produce identical archived-branch metadata.
21
+ * closed-without-merge -> record pullRequestState only; an admin decides the
22
+ * workflow transition. Best-effort: a failure is logged, swallowed and retried
23
+ * next sync cycle.
25
24
  */
26
25
  export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
27
26
  const prNumber = metaFile?.branch.pullRequestNumber;
@@ -38,8 +37,8 @@ export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
38
37
  });
39
38
  if (data.merged) {
40
39
  const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
41
- // Use GitHub's actual merge time when available; buildMergedBranchUpdate
42
- // falls back to "now" (its default `now` param) when merged_at is absent.
40
+ // GitHub's actual merge time when available; buildMergedBranchUpdate
41
+ // falls back to "now" when merged_at is absent.
43
42
  await meta.save({
44
43
  branch: buildMergedBranchUpdate(branchDir, data.merged_at ? new Date(data.merged_at) : undefined),
45
44
  });
@@ -47,9 +46,8 @@ export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
47
46
  return;
48
47
  }
49
48
  const newState = data.state === 'closed' ? 'closed' : 'open';
50
- // Re-load fresh (not the loop-top `metaFile` snapshot passed in) -- a
51
- // concurrent Lambda write (e.g. an editor re-submitting) may have
52
- // landed since that snapshot was taken.
49
+ // Re-load fresh, not the `metaFile` snapshot passed in: a concurrent Lambda
50
+ // write (an editor re-submitting) may have landed since it was taken.
53
51
  const currentMeta = await BranchMetadataFileManager.loadOnly(branchPath);
54
52
  if (currentMeta?.branch.pullRequestState === newState)
55
53
  return;
@@ -66,29 +64,24 @@ export async function pollMergeState(ctx, branchDir, branchPath, metaFile) {
66
64
  }
67
65
  }
68
66
  /**
69
- * Persist a per-branch rebase failure to branch.json (PR-W2), bounded to
70
- * roughly one save per failing branch per hour: a branch stuck failing
71
- * every cycle must not turn into unbounded save-per-cycle x N-failing-
72
- * branches write amplification -- save() eager-regenerates the branch
73
- * registry (branch-metadata.ts's invalidateRegistry(), O(branch count) EFS
74
- * reads), the same concern the `alreadyClean` no-op guard above exists
75
- * for.
67
+ * Persist a per-branch rebase failure to branch.json, bounded to roughly one
68
+ * save per failing branch per hour: a branch stuck failing every cycle must not
69
+ * become save-per-cycle x N-failing-branches write amplification, since save()
70
+ * eager-regenerates the branch registry (branch-metadata.ts's
71
+ * invalidateRegistry(), O(branch count) EFS reads).
76
72
  *
77
- * Best-effort and non-fatal like every other metadata write in this
78
- * loop's error paths: a corrupt branch.json, a lock-contention error, or
79
- * any other save failure here must never abort the per-branch iteration.
80
- * This matters doubly at the two call sites -- one is inside the outer
81
- * per-branch catch, with no further catch of its own around this call --
82
- * so the whole method is wrapped, not just the load.
73
+ * Best-effort and non-fatal, like every metadata write in this loop's error
74
+ * paths: a corrupt branch.json, lock contention or any other save failure must
75
+ * never abort the per-branch iteration. One of the two call sites is inside the
76
+ * outer per-branch catch with no further catch around it, so the WHOLE method
77
+ * is wrapped, not just the load.
83
78
  */
84
- export async function recordRebaseFailure(ctx, branchPath, branchDir, message) {
79
+ async function recordRebaseFailure(ctx, branchPath, branchDir, message) {
85
80
  const RECORD_REFRESH_MS = 60 * 60 * 1000; // 1 hour
86
- // [REDACT] Defense-in-depth redaction: rebaseFailure.message is
87
- // persisted to branch.json and served to the browser via the
88
- // branch-health admin endpoint. Both call sites in rebaseActiveBranches
89
- // already redact before passing in (the failed.push sites below),
90
- // redactCredentials is idempotent, so redacting again here is free and
91
- // keeps this method safe on its own.
81
+ // [REDACT] Defense in depth: rebaseFailure.message is persisted to
82
+ // branch.json and served to the browser by the branch-health admin endpoint.
83
+ // Both call sites already redact before passing in, but redactCredentials is
84
+ // idempotent, so redacting here is free and keeps this method safe alone.
92
85
  const redactedMessage = redactCredentials(message);
93
86
  try {
94
87
  const existing = await BranchMetadataFileManager.loadOnly(branchPath);
@@ -113,9 +106,8 @@ export async function recordRebaseFailure(ctx, branchPath, branchDir, message) {
113
106
  });
114
107
  }
115
108
  catch (err) {
116
- // Includes BranchMetadataCorruptError from the load above (a save()
117
- // against the same corrupt file would just throw again) as well as
118
- // any other load/save failure -- recording is best-effort
109
+ // Includes BranchMetadataCorruptError from the load above (a save() against
110
+ // the same corrupt file would just throw again): recording is best-effort
119
111
  // observability, never allowed to abort the branch loop.
120
112
  workerLogWarn(` Failed to record rebase failure for ${branchDir}: ${getErrorMessage(err)}`);
121
113
  }
@@ -126,56 +118,43 @@ const MAX_REBASE_ROUNDS = 50;
126
118
  * Drive `git rebase` to a finished state, resolving each conflict in favour of
127
119
  * the BRANCH's version, and report what happened.
128
120
  *
129
- * ABORT OWNERSHIP is split across four sites, none of them redundant. Stating
130
- * it as "the caller owns the abort" would be wrong in a way that invites
131
- * deleting a live one as a duplicate, so here is which exit each covers:
132
- *
133
- * - The **unexpected-error** exit aborts HERE, itself, before breaking. The
134
- * caller's `!completed` abort is then a caught no-op for that path.
135
- * - The **lock-compromised** exit leaves the rebase in progress, and is aborted
136
- * by `rebaseOneBranch`'s OWN `contentLockCompromised && !completed` branch --
121
+ * ABORT OWNERSHIP is split across FOUR sites, none redundant; "the caller owns
122
+ * the abort" would be wrong in a way that invites deleting a live one:
123
+ * - the **unexpected-error** exit aborts HERE, before breaking, so the caller's
124
+ * `!completed` abort is a caught no-op for that path;
125
+ * - the **lock-compromised** exit leaves the rebase in progress and is aborted
126
+ * by `rebaseOneBranch`'s own `contentLockCompromised && !completed` branch,
137
127
  * which returns `skippedLocked` before the `!completed` block below it is
138
- * ever reached. That abort is not the `!completed` one and cannot be folded
139
- * into it.
140
- * - The **conflict-resolution-failure** and **MAX_REBASE_ROUNDS** exits leave
141
- * the rebase in progress and are the only two the caller's `!completed`
142
- * abort actually covers.
143
- *
144
- * (A fifth abort, in `rebaseOneBranch`'s interrupted-rebase recovery, is a
145
- * different concern entirely: it cleans up a PREVIOUS run's abandoned rebase
146
- * before this one starts.)
147
- *
148
- * It can also THROW, from two places, and that is likewise covered rather than
149
- * prevented: `branchGit.status()` is the first statement of the round catch, so
150
- * a vanished `.git` or an EFS error escapes, as can the test hook beside it.
151
- * Such a throw unwinds to `rebaseOneBranch`'s outer catch, and the last-resort
152
- * abort in its `finally` -- the fourth site -- is what stops the clone being
153
- * left mid-rebase. That `finally` is the backstop for exactly this, and is not
154
- * dead code.
128
+ * reached -- a distinct site that cannot be folded into that one;
129
+ * - the **conflict-resolution-failure** and **MAX_REBASE_ROUNDS** exits are the
130
+ * only two the caller's `!completed` abort actually covers;
131
+ * - a THROW (`branchGit.status()` is the first statement of the round catch, so
132
+ * a vanished `.git` or an EFS error escapes, as can the test hook beside it)
133
+ * unwinds to `rebaseOneBranch`'s outer catch, where the last-resort abort in
134
+ * its `finally` is the backstop -- not dead code.
135
+ * (A fifth abort, in the interrupted-rebase recovery, cleans up a PREVIOUS run.)
155
136
  *
156
137
  * What this function DOES guarantee is narrower: no per-file resolution failure
157
- * escapes it. `checkout --theirs` on a MODIFY/DELETE conflict used to throw
158
- * straight out of the round loop, skipping both abort sites and wedging the
159
- * clone forever; those now set `failureReason` and break instead.
138
+ * escapes it. `checkout --theirs` on a MODIFY/DELETE conflict throws straight
139
+ * out of the round loop, skipping both abort sites and wedging the clone
140
+ * forever, so those set `failureReason` and break instead.
160
141
  *
161
142
  * `isLockCompromised` is re-read every round rather than passed as a value:
162
143
  * [SYNC-C1] the content-write lock can be lost BETWEEN rounds, and
163
144
  * `--continue`/`--skip` are as destructive as the initial rebase.
164
145
  */
165
146
  async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLockCompromised) {
166
- // Resolve-and-continue loop: keep branch version for conflicting files, then continue
167
- // Non-conflicting files get main's changes; conflicting files keep branch version.
168
147
  const conflictedFiles = [];
169
148
  let nextAction = 'start';
170
149
  let completed = false;
171
- // PR-W1: captured only on the "unexpected error" exit below, for the
172
- // failed-summary entry pushed at the `if (!completed)` check.
150
+ // Captured only on the "unexpected error" exit below, for the failed-summary
151
+ // entry pushed at the `if (!completed)` check.
173
152
  let failureReason;
174
153
  for (let round = 0; round < MAX_REBASE_ROUNDS && !completed; round++) {
175
- // Re-checked every round: the compromise can land between rounds,
176
- // and `--continue`/`--skip` are as destructive as the initial
177
- // rebase. Handled after the loop so it cannot be mistaken for the
178
- // `!completed` rebase-FAILURE path below.
154
+ // Re-checked every round: the compromise can land between rounds, and
155
+ // `--continue`/`--skip` are as destructive as the initial rebase. Handled
156
+ // after the loop so it cannot be mistaken for the `!completed`
157
+ // rebase-FAILURE path below.
179
158
  if (isLockCompromised())
180
159
  break;
181
160
  try {
@@ -197,30 +176,26 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
197
176
  const st = await branchGit.status();
198
177
  if (st.conflicted.length > 0) {
199
178
  await ctx.afterConflictDetectedForTesting();
200
- // During rebase, --theirs = the branch being replayed (editor's work).
201
- // (git rebase reverses ours/theirs: "ours" is the rebase target, "theirs" is the branch.)
179
+ // During rebase, --theirs = the branch being replayed (the editor's
180
+ // work): git rebase reverses ours/theirs, "ours" being the target.
202
181
  //
203
- // MODIFY/DELETE conflicts have no "their version" to check out
204
- // and must be resolved by staging a delete or an add instead.
205
- // `git checkout --theirs` on one exits non-zero ("path ... does
206
- // not have their version") and simple-git throws -- and because
207
- // this loop body IS the round loop's catch, that throw escapes
208
- // the round loop entirely, skipping BOTH `rebase --abort` sites
209
- // below and leaving the clone wedged mid-rebase forever. The
210
- // index/working-tree code pair identifies which side deleted
211
- // (verified against real git, not inferred):
182
+ // MODIFY/DELETE conflicts have no "their version" to check out and must
183
+ // be resolved by staging a delete or an add instead. `git checkout
184
+ // --theirs` on one exits non-zero and simple-git throws -- and because
185
+ // this loop body IS the round loop's catch, that throw escapes the
186
+ // round loop entirely, skipping BOTH `rebase --abort` sites below and
187
+ // wedging the clone mid-rebase forever. The index/working-tree code
188
+ // pair identifies which side deleted (verified against real git):
212
189
  //
213
- // U/D "deleted by them" -- the BRANCH deleted it, base
214
- // modified it. Git leaves base's version in the tree.
215
- // Keep-branch-version means honouring the delete: git rm.
216
- // D/U "deleted by us" -- base deleted it, the BRANCH
217
- // modified it. Git leaves the branch's version in the
218
- // tree. Keep-branch-version means keeping it: git add.
190
+ // U/D "deleted by them" -- the BRANCH deleted it, base modified it,
191
+ // and git leaves base's version in the tree. Keeping the branch
192
+ // version means honouring the delete: git rm.
193
+ // D/U "deleted by us" -- base deleted it, the BRANCH modified it,
194
+ // and git leaves the branch's version in the tree. Keeping the
195
+ // branch version means keeping it: git add.
219
196
  //
220
- // Any per-file resolution that STILL fails routes into the
221
- // `!completed` path below (which aborts and records) instead of
222
- // escaping -- deliberately NOT a rethrow, since a throw from
223
- // here is exactly the bug being fixed.
197
+ // A per-file resolution that STILL fails routes into the `!completed`
198
+ // path below, which aborts and records, rather than escaping.
224
199
  const conflictKind = new Map(st.files.map((f) => [f.path, `${f.index}${f.working_dir}`]));
225
200
  let resolutionFailure;
226
201
  for (const file of st.conflicted) {
@@ -247,8 +222,8 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
247
222
  }
248
223
  if (resolutionFailure !== undefined) {
249
224
  // Same exit shape as the "unexpected error" branch below: set
250
- // failureReason and break, letting the `!completed` block do
251
- // the single `rebase --abort` and record the failure once.
225
+ // failureReason and break, letting the `!completed` block do the
226
+ // single `rebase --abort` and record the failure once.
252
227
  failureReason = resolutionFailure;
253
228
  break;
254
229
  }
@@ -262,11 +237,11 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
262
237
  nextAction = 'skip';
263
238
  }
264
239
  else {
265
- // Unexpected error — abort and leave branch behind.
266
- // We intentionally don't update conflictStatus/conflictFiles here:
267
- // the rebase didn't complete so we can't determine the true conflict
268
- // state. Previous metadata (possibly stale) is preserved until the
269
- // next successful rebase cycle corrects it.
240
+ // Unexpected error — abort and leave the branch behind.
241
+ // conflictStatus/conflictFiles are deliberately not updated: the
242
+ // rebase did not complete, so the true conflict state is unknown, and
243
+ // the existing (possibly stale) metadata is preserved until the next
244
+ // successful rebase cycle corrects it.
270
245
  workerLogWarn(` Unexpected rebase error in ${branchDir}: ${msg || 'Unknown error'}`);
271
246
  failureReason = msg || 'Unknown error';
272
247
  await branchGit.rebase(['--abort']).catch(() => { });
@@ -279,38 +254,32 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
279
254
  }
280
255
  /**
281
256
  * Map the file paths a rebase resolved --theirs onto the ContentIds the editor
282
- * shows as conflicted, deduplicated.
257
+ * shows as conflicted, deduplicated. IDs rather than paths because they are
258
+ * immutable: a later slug rename must not orphan a conflict marker. Paths that
259
+ * carry no recoverable ID are dropped rather than guessed at.
283
260
  *
284
- * IDs rather than paths because they are immutable: a later slug rename must
285
- * not orphan a conflict marker. Paths that carry no recoverable ID are dropped
286
- * rather than guessed at.
287
- *
288
- * Pure -- the one piece of this loop that can be tested without a git repo.
261
+ * Pure.
289
262
  */
290
- export function conflictFilesToContentIds(conflictedFiles, contentRoot) {
291
- // Convert file paths to ContentIds — immutable, survives slug renames.
292
- // Entry files have IDs in their filename (e.g., "post.slug.a1b2c3d4e5f6.mdx").
293
- // .collection.json files have no ID themselves (extractIdFromFilename returns null
294
- // for dot-prefixed files), so we extract the ID from the parent directory instead.
295
- // The root content directory (e.g., "content/", or "cms/content/" for a
296
- // multi-segment contentRoot) has no embedded ID, so we use ROOT_COLLECTION_ID
297
- // as a sentinel — but only for the configured contentRoot.
263
+ function conflictFilesToContentIds(conflictedFiles, contentRoot) {
264
+ // Entry files carry their ID in the filename. A `.collection.json` has none
265
+ // of its own (extractIdFromFilename returns null for dot-prefixed files), so
266
+ // the ID comes from the parent directory; the root content directory has no
267
+ // embedded ID at all, so ROOT_COLLECTION_ID stands in -- but only for the
268
+ // configured contentRoot.
298
269
  //
299
- // Two different notions of "parent" are needed below, and conflating them
300
- // reproduces the exact bug this comparison guards against (see
301
- // schema-store.ts's contentRootName doc comment for the same shape elsewhere):
302
- // - `parentDir` (a basename) recovers a SUB-collection's own embedded ID
303
- // (e.g. "posts.cNbR5xFm2Kpd" -> "cNbR5xFm2Kpd") — correct as a basename,
304
- // since a collection directory carries its ID in its own name, one path
305
- // segment.
306
- // - `parentPath` (the full relative parent path, normalized) is what must be
307
- // compared against `contentRoot`, because `contentRoot` is documented
308
- // (config/helpers.ts) as allowed to span multiple segments (e.g.
309
- // "cms/content"). Comparing a basename ("content") against that full value
310
- // is always false, which silently drops the root collection's conflict.
311
- // Git reports POSIX-style paths and the configured value may be authored
312
- // with either separator, so both sides go through normalizeFilesystemPath
313
- // before comparing.
270
+ // Two different notions of "parent" are needed, and conflating them is the
271
+ // bug this guards against (see schema-store.ts's contentRootName doc comment
272
+ // for the same shape elsewhere):
273
+ // - `parentDir` (a basename) recovers a SUB-collection's own embedded ID,
274
+ // correct as a basename since a collection directory carries its ID in its
275
+ // own name, one path segment;
276
+ // - `parentPath` (the full normalized relative parent path) is what must be
277
+ // compared against `contentRoot`, which is documented (config/helpers.ts)
278
+ // as allowed to span multiple segments (e.g. "cms/content"). Comparing a
279
+ // basename against that full value is always false, silently dropping the
280
+ // root collection's conflict. Git reports POSIX paths and the configured
281
+ // value may use either separator, so both sides go through
282
+ // normalizeFilesystemPath first.
314
283
  const normalizedContentRoot = normalizeFilesystemPath(contentRoot);
315
284
  const conflictIds = [...new Set(conflictedFiles)]
316
285
  .map((f) => {
@@ -336,44 +305,37 @@ export function conflictFilesToContentIds(conflictedFiles, contentRoot) {
336
305
  * [SYNC-H1] Carry a just-rebased history forward into `remote.git`, and queue
337
306
  * the GitHub hop.
338
307
  *
339
- * Only runs when the branch's pre-rebase history had ALREADY been published
308
+ * Only runs when the pre-rebase history had ALREADY been published
340
309
  * (`publishedSha !== null`); otherwise nothing downstream knows about the
341
- * commits the rebase replaced and there is nothing to reconcile. Without this,
342
- * the editor's next submit is simply rejected non-fast-forward forever.
310
+ * commits the rebase replaced. Without it, the editor's next submit is rejected
311
+ * non-fast-forward forever.
343
312
  *
344
- * `preRebaseHead` and `publishedSha` must both be read BEFORE the rebase --
345
- * the clone's pre-rebase tip is exactly what the rebase destroys.
313
+ * `preRebaseHead` and `publishedSha` must BOTH be read before the rebase -- the
314
+ * clone's pre-rebase tip is exactly what the rebase destroys.
346
315
  */
347
316
  async function carryForwardRewrittenHistory(ctx, args) {
348
317
  const { branchPath, branchDir, branchRef, publishedSha, preRebaseHead } = args;
349
- // [SYNC-H1] The rebase just rewrote this clone's history. If that
350
- // history was already published, nothing else will ever reconcile
351
- // remote.git (and GitHub) with it -- the editor's next submit would
352
- // simply be rejected non-fast-forward. Carry the rewrite forward.
353
318
  if (publishedSha !== null) {
354
319
  if (publishedSha === preRebaseHead) {
355
- // ARMING GUARD. remote.git holds EXACTLY what this clone just
356
- // rebased away and nothing more, so a lease keyed to it can only
357
- // undo our own rewrite.
320
+ // ARMING GUARD: remote.git holds EXACTLY what this clone just rebased
321
+ // away and nothing more, so a lease keyed to it can only undo our own
322
+ // rewrite.
358
323
  //
359
- // The inequality case below is not defensive padding: branch
360
- // clones never fetch their own branch (GitManager's clone is
361
- // --single-branch and checkoutBranch only checks out an existing
362
- // local branch), while reconcileTrackedBranches fast-forwards
363
- // remote.git to GitHub's tip. So after a reviewer pushes a fixup
364
- // straight to the PR branch, remote.git legitimately holds a
365
- // commit this clone has never seen. Leasing on "whatever
366
- // remote.git currently holds" would be SATISFIED there and would
367
- // delete that fixup from remote.git and then from GitHub,
368
- // silently. Keying the lease to the pre-rebase tip turns that
369
- // case into the visible divergence it should be.
324
+ // The inequality case below is not defensive padding. Branch clones never
325
+ // fetch their own branch (GitManager's clone is --single-branch and
326
+ // checkoutBranch only checks out an existing local branch), while
327
+ // reconcileTrackedBranches fast-forwards remote.git to GitHub's tip, so
328
+ // after a direct push of a fixup to the PR branch remote.git legitimately
329
+ // holds a commit this clone has never seen. Leasing on "whatever
330
+ // remote.git currently holds" would be SATISFIED there and would silently
331
+ // delete that fixup from remote.git and then GitHub. Keying the lease to
332
+ // the pre-rebase tip turns that case into the visible divergence it is.
370
333
  //
371
- // Order matters: mark, then push, then queue. A crash after any
372
- // step leaves the marker set with the work unfinished, which
373
- // reconcilePendingRewrite() completes on a later cycle. Pushing
374
- // first would leave remote.git rewritten, GitHub stale and
375
- // nothing recorded -- unrecoverable, and landing on exactly the
376
- // false "another deployment" diagnosis this change removes.
334
+ // Order matters: mark, then push, then queue. A crash after any step
335
+ // leaves the marker set with the work unfinished, which
336
+ // reconcilePendingRewrite() completes on a later cycle. Pushing first
337
+ // would leave remote.git rewritten, GitHub stale and nothing recorded --
338
+ // unrecoverable, and diagnosed as a false "another deployment" collision.
377
339
  await markHistoryRewritten(ctx, branchPath, branchDir, publishedSha);
378
340
  if (await forcePublishToLocalRemote(ctx, branchPath, branchRef, publishedSha)) {
379
341
  await enqueueGitHubPush(ctx, branchRef);
@@ -401,9 +363,6 @@ async function carryForwardRewrittenHistory(ctx, args) {
401
363
  * subprocesses against a shared filesystem.
402
364
  */
403
365
  export async function runRebaseCycle(ctx) {
404
- // PR-W1: collected across the loop below and returned as a summary
405
- // (folded into worker-status.json by syncGit()). Purely additive
406
- // bookkeeping -- doesn't change any control flow or existing logging.
407
366
  const rebased = [];
408
367
  const skippedDirty = [];
409
368
  const skippedLocked = [];
@@ -416,11 +375,11 @@ export async function runRebaseCycle(ctx) {
416
375
  return { rebased, skippedDirty, skippedLocked, failed };
417
376
  }
418
377
  for (const branchDir of branchDirs) {
419
- // Known structural entries under content-branches/, not branch
420
- // workspaces: branches.json (registry snapshot) and dot-prefixed
421
- // entries (.canopy-meta/, transient lock dirs). Skip them silently so
422
- // the no-.git skip logs below don't fire for them every cycle.
423
- // Everything else still logs loudly on skip.
378
+ // Known structural entries under content-branches/, not branch workspaces:
379
+ // branches.json (registry snapshot) and dot-prefixed entries
380
+ // (.canopy-meta/, transient lock dirs). Skipped SILENTLY so the no-.git
381
+ // skip logs below don't fire for them every cycle; everything else still
382
+ // logs loudly on skip.
424
383
  if (branchDir.startsWith('.') || branchDir === 'branches.json') {
425
384
  continue;
426
385
  }
@@ -438,11 +397,11 @@ export async function runRebaseCycle(ctx) {
438
397
  continue;
439
398
  }
440
399
  // The base branch's own clone is refreshed ff-only by
441
- // refreshBaseBranchWorkspace() earlier in syncGit(). Routing it
442
- // through this conflict-resolution rebase loop could rewrite its
443
- // history (the --theirs loop below) and stamp meaningless conflict
444
- // metadata on it. Compare sanitized-vs-sanitized: branchDir is a
445
- // filesystem name (already sanitized), ctx.baseBranch is raw.
400
+ // refreshBaseBranchWorkspace() earlier in syncGit(). Routing it through
401
+ // this conflict-resolution loop could rewrite its history (the --theirs
402
+ // loop below) and stamp meaningless conflict metadata on it. Compare
403
+ // sanitized-vs-sanitized: branchDir is a filesystem name, ctx.baseBranch
404
+ // is raw.
446
405
  if (branchDir === ctx.sanitizedBaseBranch) {
447
406
  workerLog(` Skipping ${branchDir}: base branch (refreshed separately)`);
448
407
  continue;
@@ -477,19 +436,17 @@ export async function runRebaseCycle(ctx) {
477
436
  * rest of the cycle ([SYNC-M2]).
478
437
  */
479
438
  async function rebaseOneBranch(ctx, branchDir, branchPath) {
480
- // Set where the original loop pushed to `rebased[]`, and read only by the
481
- // catch below. See the `rebased` rider on BranchRebaseOutcome.
439
+ // Read only by the catch below. See the `rebased` rider on
440
+ // BranchRebaseOutcome.
482
441
  let didRebase = false;
483
442
  try {
484
- // Load metadata before any git ops to check branch status
485
443
  const metaFile = await BranchMetadataFileManager.loadOnly(branchPath);
486
444
  const branchStatus = metaFile?.branch.status;
487
- // Skip branches that shouldn't be mutated:
488
- // - submitted/approved: in review, don't rewrite history under an
489
- // active PR -- but do poll GitHub for the PR's resolution, since
490
- // nothing else tells the worker a merge/close happened.
491
- // - archived: already merged, no reason to rebase and no PR left to
492
- // poll (avoid the wasted API call).
445
+ // Skip branches that must not be mutated:
446
+ // - submitted/approved: in review, so never rewrite history under an active
447
+ // PR -- but do poll GitHub for the PR's resolution, since nothing else
448
+ // tells the worker a merge/close happened;
449
+ // - archived: already merged, with no PR left to poll.
493
450
  if (branchStatus === 'submitted' || branchStatus === 'approved') {
494
451
  workerLog(` Skipping ${branchDir} (${branchStatus})`);
495
452
  await pollMergeState(ctx, branchDir, branchPath, metaFile);
@@ -501,35 +458,31 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
501
458
  }
502
459
  const branchGit = simpleGit({
503
460
  baseDir: branchPath,
504
- // Keep git non-interactive during rebase/merge so it never blocks on an editor.
505
- // simple-git >=3.32 blocks setting core.editor unless explicitly opted in; the
506
- // value here is a hardcoded literal ("true", the shell no-op), not user input,
507
- // so enabling allowUnsafeEditor carries no injection risk.
461
+ // Keep git non-interactive during rebase/merge so it never blocks on an
462
+ // editor. simple-git >=3.32 requires opting in to set core.editor; the
463
+ // value is a hardcoded literal ("true", the shell no-op), not user input,
464
+ // so allowUnsafeEditor carries no injection risk here.
508
465
  config: ['core.editor=true'],
509
466
  unsafe: { allowUnsafeEditor: true },
510
467
  });
511
468
  // [SYNC-C1] Take the branch's cross-host content-write lock BEFORE the
512
- // dirty check, and hold it for the whole rebase.
513
- //
514
- // The dirty check alone is check-then-act. The old comment here claimed
515
- // the residual window was safe ("the rebase will fail and the catch
516
- // block will abort safely"), which only holds for a save landing before
517
- // `git rebase` STARTS. After that -- a window spanning fetch, replay and
518
- // N conflict rounds of awaited git subprocesses on EFS -- a save is
519
- // destroyed two ways: `checkout --theirs` below overwrites the
520
- // just-saved file with the branch's committed version and the rebase
521
- // then SUCCEEDS (nothing logs a failure at all), and `rebase --abort`
469
+ // dirty check, and hold it for the whole rebase. The dirty check alone is
470
+ // check-then-act, and its residual window spans fetch, replay and N
471
+ // conflict rounds of awaited git subprocesses on EFS. A save landing in
472
+ // there is destroyed two ways: `checkout --theirs` below overwrites the
473
+ // just-saved file with the branch's committed version and the rebase then
474
+ // SUCCEEDS (nothing logs a failure at all), and `rebase --abort`
522
475
  // hard-resets the tree. The editor already got its 200 either way.
523
476
  //
524
477
  // Zero-retry acquisition, deliberately: on contention this branch is
525
- // skipped and retried on the next sync cycle (~5 min), which is the
526
- // same principle as the skip-dirty-branches behavior below. Writers get
527
- // the patient side of the asymmetry -- see utils/content-write-lock.ts.
478
+ // skipped and retried next sync cycle (~5 min), the same principle as the
479
+ // skip-dirty-branches behaviour below. Writers get the patient side of the
480
+ // asymmetry -- see utils/content-write-lock.ts.
528
481
  //
529
- // The heartbeat that keeps this lock fresh (proper-lockfile refreshes
530
- // every `stale`/2 = 15s) is a timer on this event loop; every git step
531
- // below is an awaited subprocess, never a synchronous block, so the
532
- // refresh keeps firing for the whole hold.
482
+ // The heartbeat keeping this lock fresh (proper-lockfile refreshes every
483
+ // `stale`/2 = 15s) is a timer on this event loop, and every git step below
484
+ // is an awaited subprocess rather than a synchronous block, so the refresh
485
+ // keeps firing for the whole hold.
533
486
  let releaseContentLock;
534
487
  // [SYNC-C1] If the lock is lost mid-hold, a writer may now be live
535
488
  // against this same tree. Every git step below is destructive, so
@@ -552,55 +505,47 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
552
505
  throw lockErr;
553
506
  }
554
507
  try {
555
- // Recover an INTERRUPTED rebase before anything else looks at this
556
- // tree. A clone left with .git/rebase-merge (or rebase-apply) reports
557
- // uncommitted changes, so without this the dirty check below would
558
- // classify it `skippedDirty` on every cycle FOREVER while
559
- // branch-health scanned it as healthy -- and editors would meanwhile
560
- // read, and be able to save over, conflict-marker content.
561
- //
562
- // An in-progress rebase is always this worker's own abandoned work:
563
- // it is the only thing that ever rebases these clones, and it got
564
- // here via a crash, an OOM, a spot interruption, or the ASG rolling
565
- // the instance (which happens on EVERY `cdk deploy`, while `stop()`
566
- // drains for at most taskTimeoutMs).
508
+ // Recover an INTERRUPTED rebase before anything else looks at this tree.
509
+ // A clone left with .git/rebase-merge (or rebase-apply) reports
510
+ // uncommitted changes, so without this the dirty check below classifies
511
+ // it `skippedDirty` on every cycle FOREVER while branch-health scans it
512
+ // as healthy -- and editors meanwhile read, and can save over,
513
+ // conflict-marker content. An in-progress rebase here is always this
514
+ // worker's own abandoned work (nothing else rebases these clones) left by
515
+ // a crash, an OOM, a spot interruption, or the ASG rolling the instance.
567
516
  //
568
- // NOT LOSSLESS, and it is important not to claim otherwise. `git
569
- // rebase --abort` hard-resets tracked files to the pre-rebase head.
570
- // While the worker was DOWN nothing held the [SYNC-C1] content-write
571
- // lock, so an editor could have saved into this wedged clone and
572
- // received a 200; that save is a working-tree modification, and the
573
- // abort reverts it. (New, untracked entry files survive; edits to
574
- // existing ones do not.) Taking the lock here stops any FURTHER save
575
- // racing the abort, but cannot recover one that already landed.
517
+ // NOT LOSSLESS, and it is important not to claim otherwise. `git rebase
518
+ // --abort` hard-resets tracked files to the pre-rebase head. While the
519
+ // worker was DOWN nothing held the [SYNC-C1] content-write lock, so an
520
+ // editor could have saved into this wedged clone and received a 200; that
521
+ // save is a working-tree modification and the abort reverts it (new,
522
+ // untracked entry files survive; edits to existing ones do not). Taking
523
+ // the lock here stops any FURTHER save racing the abort but cannot
524
+ // recover one that already landed.
576
525
  //
577
- // Aborting anyway is still the right call: the alternative is a
578
- // branch wedged forever whose tree serves conflict-marker content to
579
- // editors. What must not happen is doing it SILENTLY -- so anything
580
- // modified beyond the rebase's own conflict state is logged by path
581
- // first, which is the only record an operator would have.
526
+ // Aborting anyway is still right -- the alternative is a branch wedged
527
+ // forever serving conflict-marker content to editors -- but it must not
528
+ // happen SILENTLY, so anything modified beyond the rebase's own conflict
529
+ // state is logged by path first. That log is the operator's only record.
582
530
  if (await isRebaseInProgress(branchPath)) {
583
531
  const preAbort = await branchGit.status().catch(() => null);
584
- // Keyed on the WORKING-TREE column only. The two porcelain columns
585
- // mean different things here, and conflating them produces a false
586
- // data-loss report on essentially every conflict-wedged recovery
587
- // (verified against real git, mid-rebase):
532
+ // Keyed on the WORKING-TREE column ONLY. The two porcelain columns mean
533
+ // different things here, and conflating them reports false data loss on
534
+ // essentially every conflict-wedged recovery (verified against real
535
+ // git, mid-rebase):
588
536
  //
589
537
  // `M ` index=M, wd=' ' -- the interrupted replay's own cleanly
590
- // merged files, already STAGED. These
591
- // are committed history and survive the
592
- // abort untouched. Not collateral.
593
- // ` M` index=' ', wd=M -- a working-tree modification nothing
594
- // staged: an editor's save landing while
595
- // the worker was down. The abort
596
- // discards exactly these.
538
+ // merged files, already STAGED: committed
539
+ // history that survives the abort untouched.
540
+ // ` M` index=' ', wd=M -- an unstaged working-tree modification: an
541
+ // editor's save landing while the worker was
542
+ // down. The abort discards exactly these.
597
543
  // `??` -- untracked; the abort leaves them.
598
544
  //
599
- // KNOWN GAP, stated rather than hidden: a save onto one of the
600
- // rebase's own conflicted paths (the "saved over conflict-marker
601
- // content" case) is excluded below, because the file reads `UU`
602
- // whether or not an editor touched it -- status alone cannot tell
603
- // the two apart. Those discards go unlogged.
545
+ // KNOWN GAP: a save onto one of the rebase's own conflicted paths (the
546
+ // "saved over conflict-marker content" case) is excluded below, because
547
+ // the file reads `UU` whether or not an editor touched it and status
548
+ // alone cannot tell the two apart. Those discards go unlogged.
604
549
  const collateral = (preAbort?.files ?? [])
605
550
  .filter((f) => !preAbort?.conflicted.includes(f.path))
606
551
  .filter((f) => f.working_dir !== ' ' && f.working_dir !== '?')
@@ -619,37 +564,34 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
619
564
  const reason = redactCredentials(`could not abort interrupted rebase: ${getErrorMessage(abortErr)}`);
620
565
  workerLogWarn(` Skipping ${branchDir}: ${reason}`);
621
566
  // Record on branch metadata too, like the other two failure exits
622
- // (`!completed` and the outer catch). Without this a persistently
623
- // un-abortable wedge appeared in worker-status.json but never set
624
- // a `syncFailureReason`, so the admin branch panel showed nothing
625
- // -- and this is precisely the state that needs an operator,
626
- // since it is the one the next cycle cannot fix by itself.
567
+ // (`!completed` and the outer catch): without a `syncFailureReason`
568
+ // the admin branch panel shows nothing for a persistently
569
+ // un-abortable wedge, which is precisely the state needing an
570
+ // operator, since the next cycle cannot fix it by itself.
627
571
  await recordRebaseFailure(ctx, branchPath, branchDir, reason);
628
572
  return { kind: 'failed', error: reason };
629
573
  }
630
574
  }
631
- // Skip dirty branches — editor has unsaved changes that can't be rebased.
632
- // Now inside the lock, so no write can land between this check and the
633
- // rebase below.
575
+ // Skip dirty branches — the editor has changes that cannot be rebased.
576
+ // Inside the lock, so no write can land between this check and the rebase
577
+ // below.
634
578
  const dirtyCheck = await branchGit.status();
635
579
  if (dirtyCheck.files.length > 0) {
636
580
  workerLog(` Skipping ${branchDir}: has uncommitted changes`);
637
581
  return { kind: 'skippedDirty' };
638
582
  }
639
- // The clone's own ref name: branchDir is the sanitized DIRECTORY
640
- // name and need not match it. A literal 'HEAD' means a detached
641
- // clone (e.g. a crashed rebase left one behind) -- nothing below can
642
- // safely name a ref then, so every publish path stays disarmed,
643
- // which is the safe direction.
583
+ // The clone's own ref name: branchDir is the sanitized DIRECTORY name and
584
+ // need not match it. A literal 'HEAD' means a detached clone (a crashed
585
+ // rebase can leave one), where nothing below can safely name a ref, so
586
+ // every publish path stays disarmed -- the safe direction.
644
587
  const branchRef = (await branchGit.revparse(['--abbrev-ref', 'HEAD'])).trim();
645
588
  const canPublish = branchRef.length > 0 && branchRef !== 'HEAD';
646
589
  // [SYNC-H1] Self-heal: finish an interrupted publish even when this
647
- // branch is not behind base. Every crash window in the arming
648
- // sequence below leaves the marker set with the work unfinished, and
649
- // this is what completes it -- without it, one lost lease race would
650
- // strand the branch until the base branch happened to advance again.
651
- // Gated on the loop-top snapshot, so unmarked branches (nearly all
652
- // of them, every cycle) cost nothing extra.
590
+ // branch is not behind base. Every crash window in the arming sequence
591
+ // below leaves the marker set with the work unfinished, and this is what
592
+ // completes it -- without it one lost lease race strands the branch until
593
+ // the base happens to advance again. Gated on the loop-top snapshot, so
594
+ // unmarked branches (nearly all of them, every cycle) cost nothing.
653
595
  if (canPublish && metaFile?.branch.historyRewrittenFrom) {
654
596
  await reconcilePendingRewrite(ctx, {
655
597
  branchPath,
@@ -660,38 +602,34 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
660
602
  });
661
603
  }
662
604
  await branchGit.fetch('origin', ctx.baseBranch);
663
- // Use rev-list instead of status.behind — status.behind only works when the
664
- // branch has an upstream tracking branch configured, which isn't guaranteed
665
- // (checkoutBranch fallback paths create branches without --track).
666
- // The just-fetched tip, not origin/<base>: branch clones are
667
- // --single-branch, so no remote-tracking ref exists for a base branch
668
- // other than the one they were cloned from (see the base-refresh
669
- // comment above). Pinned to a SHA immediately — FETCH_HEAD is one
670
- // shared mutable file per repo, repointed by any concurrent fetch.
605
+ // rev-list, not status.behind, which needs an upstream tracking branch
606
+ // that checkoutBranch's fallback paths do not always configure. Against
607
+ // the just-fetched tip, not origin/<base>: branch clones are
608
+ // --single-branch, so no remote-tracking ref exists for any base branch
609
+ // other than the one they were cloned from. Pinned to a SHA immediately
610
+ // — FETCH_HEAD is one shared mutable file per repo, repointed by any
611
+ // concurrent fetch.
671
612
  const fetchedBaseTip = (await branchGit.revparse(['FETCH_HEAD'])).trim();
672
613
  const behindCount = parseInt((await branchGit.raw(['rev-list', '--count', `HEAD..${fetchedBaseTip}`])).trim(), 10);
673
614
  const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
674
615
  if (behindCount === 0) {
675
- // Already in sync. This is the overwhelmingly common outcome per
676
- // branch per cycle (most branches are caught up most of the
677
- // time), so skip the save entirely when metadata already reflects
678
- // a clean state -- every save() now eager-regenerates the branch
679
- // registry (branch-metadata.ts's invalidateRegistry(), O(branch
680
- // count) fs reads on EFS), so an unconditional save here turns
681
- // every rebase cycle into O(N^2) registry work across N branches
682
- // for what is otherwise a true no-op. Re-load fresh (not the
683
- // `metaFile` snapshot from before the fetch/rev-list above) so a
684
- // concurrent editor-driven metadata change during that window
685
- // isn't clobbered by a stale skip decision.
616
+ // Already in sync -- the overwhelmingly common outcome per branch per
617
+ // cycle, so skip the save entirely when metadata already reflects a
618
+ // clean state. save() eager-regenerates the branch registry
619
+ // (branch-metadata.ts's invalidateRegistry(), O(branch count) EFS
620
+ // reads), so an unconditional save here makes every rebase cycle
621
+ // O(N^2) registry work for a true no-op. Re-load fresh, not the
622
+ // `metaFile` snapshot from before the fetch/rev-list above, so a
623
+ // concurrent editor-driven metadata change in that window is not
624
+ // clobbered by a stale skip decision.
686
625
  const currentMeta = await BranchMetadataFileManager.loadOnly(branchPath);
687
626
  const conflictStatus = currentMeta?.branch.conflictStatus;
688
627
  const conflictFiles = currentMeta?.branch.conflictFiles;
689
628
  const conflictAlreadyClean = (conflictStatus === undefined || conflictStatus === 'clean') &&
690
629
  (conflictFiles === undefined || conflictFiles.length === 0);
691
- // PR-W2: a lingering rebaseFailure must also be cleared once the
692
- // branch catches up clean -- otherwise it sticks as a stale
693
- // warning forever (nothing else touches this branch once it's
694
- // caught up, so no other save site would ever clear it).
630
+ // A lingering rebaseFailure must also be cleared once the branch
631
+ // catches up clean, or it sticks as a stale warning forever: nothing
632
+ // else touches a caught-up branch, so no other save site clears it.
695
633
  const alreadyClean = conflictAlreadyClean && currentMeta?.branch.rebaseFailure === undefined;
696
634
  if (alreadyClean) {
697
635
  return { kind: 'none' };
@@ -707,10 +645,10 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
707
645
  return { kind: 'none' };
708
646
  }
709
647
  workerLog(`Rebasing ${branchDir} (${behindCount} commits behind)...`);
710
- // Read BOTH sides before rewriting anything: the arming guard after
711
- // the rebase compares what remote.git published against what this
712
- // clone is about to rebase away. Reading them afterwards would be
713
- // useless -- the clone's pre-rebase tip is exactly what disappears.
648
+ // Read BOTH sides before rewriting anything: the arming guard after the
649
+ // rebase compares what remote.git published against what this clone is
650
+ // about to rebase away, and the clone's pre-rebase tip is exactly what
651
+ // the rebase makes unreadable.
714
652
  const preRebaseHead = (await branchGit.revparse(['HEAD'])).trim();
715
653
  const publishedSha = canPublish ? await readPublishedSha(ctx, branchRef) : null;
716
654
  const { completed, conflictedFiles, failureReason } = await runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, () => contentLockCompromised);
@@ -719,27 +657,25 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
719
657
  if (completed)
720
658
  await ctx.afterRebaseCompletedForTesting();
721
659
  // [SYNC-C1] A lost lock is a RETRY, not a rebase failure: nothing is
722
- // wrong with the branch, we simply can no longer prove we were the
723
- // only writer. Handled ahead of the `!completed` block so it never
724
- // records a user-visible rebaseFailure or lands in `failed[]`.
660
+ // wrong with the branch, we simply can no longer prove we were the only
661
+ // writer. Handled ahead of the `!completed` block so it never records a
662
+ // user-visible rebaseFailure or lands in `failed[]`.
725
663
  //
726
664
  // ONLY when the rebase did not complete. Bailing out of a COMPLETED
727
- // rebase would be worse than useless: the history is already
728
- // rewritten (so `--abort` is a no-op), and skipping the completion
729
- // path below strands three things the next cycle will never redo,
730
- // because a caught-up branch short-circuits at the `behindCount === 0`
731
- // check above -- the [SYNC-H1] `markHistoryRewritten` marker (without
732
- // which the editor's next submit is rejected non-fast-forward and
733
- // mis-diagnosed as another deployment), the content-cache
734
- // invalidation for a tree that DID change, and the conflictStatus
735
- // save. That converts a transient lock compromise into a permanently
736
- // wedged published branch.
665
+ // rebase is worse than useless: the history is already rewritten (so
666
+ // `--abort` is a no-op), and skipping the completion path below strands
667
+ // three things the next cycle never redoes, because a caught-up branch
668
+ // short-circuits at the `behindCount === 0` check above -- the [SYNC-H1]
669
+ // `markHistoryRewritten` marker (without which the editor's next submit
670
+ // is rejected non-fast-forward and mis-diagnosed as another deployment),
671
+ // the content-cache invalidation for a tree that DID change, and the
672
+ // conflictStatus save. That turns a transient lock compromise into a
673
+ // permanently wedged published branch.
737
674
  //
738
675
  // Nor does bailing protect a racing save: a rebase replays COMMITTED
739
- // history, while a concurrent save is uncommitted working-tree state,
740
- // and that writer is already told to retry via
741
- // ContentWriteLockBusyError. So when the rebase completed, log the
742
- // lost exclusivity loudly and finish the job.
676
+ // history while a concurrent save is uncommitted working-tree state, and
677
+ // that writer is already told to retry via ContentWriteLockBusyError. So
678
+ // when the rebase completed, log the lost exclusivity loudly and finish.
743
679
  if (contentLockCompromised && !completed) {
744
680
  workerLogWarn(` Skipping ${branchDir}: content-write lock was compromised mid-rebase (retrying next cycle)`);
745
681
  // No-op when no rebase is in progress; failure is expected there.
@@ -750,32 +686,30 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
750
686
  workerLogWarn(` Content-write lock for ${branchDir} was compromised, but its rebase had already completed -- finishing the sync (history is rewritten; skipping now would strand the history-rewrite marker and wedge the branch)`);
751
687
  }
752
688
  if (!completed) {
753
- // PR-W2 (M1 rider): failureReason is only set on the "unexpected
754
- // error" break above -- MAX_REBASE_ROUNDS exhaustion is a distinct exit
755
- // path with no error message of its own, so the warn text must
756
- // not conflate the two.
689
+ // failureReason is only set on the "unexpected error" break above;
690
+ // MAX_REBASE_ROUNDS exhaustion is a distinct exit with no message of
691
+ // its own, so the warn text must not conflate the two.
757
692
  workerLogWarn(failureReason !== undefined
758
693
  ? ` Rebase of ${branchDir} aborted due to unexpected error: ${failureReason}`
759
694
  : ` Rebase of ${branchDir} did not complete within ${MAX_REBASE_ROUNDS} rounds, aborting`);
760
695
  await branchGit.rebase(['--abort']).catch(() => { });
761
696
  const rebaseFailureMessage = failureReason ?? `did not complete within ${MAX_REBASE_ROUNDS} rounds`;
762
- // [REDACT] failed[] folds into worker-status.json's
763
- // lastGitSync.failed, served to the browser -- failureReason can
764
- // be an arbitrary git error message that embeds the bot token.
697
+ // [REDACT] failed[] folds into worker-status.json's lastGitSync.failed,
698
+ // served to the browser -- failureReason can be an arbitrary git error
699
+ // message that embeds the bot token.
765
700
  const redactedRebaseFailureMessage = redactCredentials(rebaseFailureMessage);
766
- // PR-W2: record once here for the "!completed" exit -- the
767
- // unexpected-error break above is NOT disjoint from this block (it
768
- // always falls through here), so recording at the break itself
769
- // would double-record. The outer catch below is the only other
770
- // record site (a distinct, non-overlapping failure class: errors
771
- // outside this round loop, e.g. fetch/rev-list failures).
701
+ // Record ONCE here for the "!completed" exit: the unexpected-error
702
+ // break above always falls through to this block, so recording at the
703
+ // break itself would double-record. The outer catch below is the only
704
+ // other record site, covering a disjoint failure class -- errors
705
+ // outside the round loop, e.g. fetch/rev-list failures.
772
706
  await recordRebaseFailure(ctx, branchPath, branchDir, redactedRebaseFailureMessage);
773
707
  return { kind: 'failed', error: redactedRebaseFailureMessage };
774
708
  }
775
- // The rebase rewrote the branch clone's working tree — mark ContentStore
776
- // ID indexes rooted here stale so lookups rebuild from disk, in this
777
- // process and (via the on-disk generation marker) in the Lambda
778
- // containers sharing this filesystem.
709
+ // The rebase rewrote the clone's working tree — mark ContentStore ID
710
+ // indexes rooted here stale so lookups rebuild from disk, in this process
711
+ // and (via the on-disk generation marker) in the Lambda containers
712
+ // sharing this filesystem.
779
713
  await invalidateBranchContentCaches(branchPath);
780
714
  const conflictIdsDeduped = conflictFilesToContentIds(conflictedFiles, ctx.contentRoot);
781
715
  const hadConflicts = conflictIdsDeduped.length > 0;
@@ -787,15 +721,11 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
787
721
  name: branchDir,
788
722
  conflictStatus: hadConflicts ? 'conflicts-detected' : 'clean',
789
723
  conflictFiles: conflictIdsDeduped,
790
- // PR-W2: the cycle completed successfully -- clear any prior
791
- // failure record regardless of conflict outcome.
724
+ // The cycle completed, so clear any prior failure record regardless
725
+ // of the conflict outcome.
792
726
  rebaseFailure: undefined,
793
727
  },
794
728
  });
795
- // PR-W1: the branch was behind and the rebase completed (with or
796
- // without --theirs conflict resolution) -- it moved, so it belongs
797
- // in the summary. Branches already up to date `continue`d above and
798
- // are deliberately not listed here.
799
729
  didRebase = true;
800
730
  await carryForwardRewrittenHistory(ctx, {
801
731
  branchPath,
@@ -807,24 +737,21 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
807
737
  return { kind: 'rebased' };
808
738
  }
809
739
  finally {
810
- // Last-resort guarantee that NO exit path leaves this clone
811
- // mid-rebase -- including an unexpected throw from any git step
812
- // above, which lands in the outer catch and previously only logged.
740
+ // Last-resort guarantee that NO exit path leaves this clone mid-rebase,
741
+ // including an unexpected throw from any git step above.
813
742
  //
814
- // It must live HERE rather than in that outer catch: the catch runs
815
- // AFTER this finally has released the content-write lock, so aborting
816
- // there would hard-reset a working tree an editor's save could
817
- // already be racing -- precisely the [SYNC-C1] hazard the lock
818
- // exists to prevent. Inside the finally the lock is still held --
819
- // EXCEPT on the narrow path where it was compromised mid-hold, in
820
- // which case a newly-admitted writer may already be live and this
821
- // abort carries the same exposure as the compromise path's own abort
822
- // above. Not special-cased: leaving a clone wedged mid-rebase is the
823
- // worse outcome, and the writer in that window is already being told
824
- // to retry.
743
+ // It must live HERE rather than in the outer catch: that catch runs AFTER
744
+ // this finally has released the content-write lock, so aborting there
745
+ // would hard-reset a working tree an editor's save could already be
746
+ // racing -- precisely the [SYNC-C1] hazard the lock exists to prevent.
747
+ // Inside the finally the lock is still held, EXCEPT on the narrow path
748
+ // where it was compromised mid-hold, where a newly-admitted writer may be
749
+ // live and this abort carries the same exposure as the compromise path's
750
+ // own abort above. Not special-cased: leaving a clone wedged mid-rebase
751
+ // is worse, and that writer is already being told to retry.
825
752
  //
826
- // Guarded on actual rebase state so the happy path and the `continue`
827
- // exits cost one stat and do nothing.
753
+ // Guarded on actual rebase state, so the happy path and the early returns
754
+ // cost one stat and do nothing.
828
755
  try {
829
756
  if (await isRebaseInProgress(branchPath)) {
830
757
  workerLogWarn(` ${branchDir}: rebase still in progress on exit -- aborting so the clone is not left wedged`);
@@ -835,9 +762,9 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
835
762
  // Best effort: the next cycle's recovery check retries this.
836
763
  workerLogWarn(` Failed to abort in-progress rebase for ${branchDir}: ${getErrorMessage(abortErr)}`);
837
764
  }
838
- // [SYNC-C1] Released on EVERY exit -- the `continue`s above, a throw
839
- // into the outer catch, and the happy path alike. A stranded lock
840
- // would wedge every write to this branch until it went stale.
765
+ // [SYNC-C1] Released on EVERY exit -- the early returns above, a throw
766
+ // into the outer catch, and the happy path alike. A stranded lock would
767
+ // wedge every write to this branch until it went stale.
841
768
  await releaseContentLock?.().catch((releaseErr) => {
842
769
  workerLogWarn(` Failed to release content-write lock for ${branchDir}: ${getErrorMessage(releaseErr)}`);
843
770
  });
@@ -846,13 +773,11 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
846
773
  catch (err) {
847
774
  const message = err instanceof Error ? err.message : 'Unknown error';
848
775
  workerLogWarn(` Failed to sync ${branchDir}: ${message}`);
849
- // [REDACT] Same rationale as the `if (!completed)` push site above --
850
- // this catches fetch/rev-list/unexpected errors, whose message can
851
- // embed the bot token.
776
+ // [REDACT] Same rationale as the `if (!completed)` site above: this catches
777
+ // fetch/rev-list/unexpected errors, whose message can embed the bot token.
852
778
  const redactedMessage = redactCredentials(message);
853
- // PR-W2: second (and only other) record site -- see the comment at
854
- // the `if (!completed)` block above for why these two sites are
855
- // disjoint.
779
+ // Second and only other record site -- see the `if (!completed)` block
780
+ // above for why the two are disjoint.
856
781
  await recordRebaseFailure(ctx, branchPath, branchDir, redactedMessage);
857
782
  return { kind: 'failed', error: redactedMessage, rebased: didRebase };
858
783
  }