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
@@ -1,23 +1,18 @@
1
1
  /**
2
2
  * Comment-preserving serialisation for YAML content files and md/mdx frontmatter.
3
3
  *
4
- * ContentStore used to write a content file by stringifying a fresh plain object
5
- * (`yamlStringify(data)` / `matter.stringify(body, data)`). Comments live in neither the object
6
- * nor that round trip, so **every editor save silently deleted every comment in the file** —
7
- * invisible to a dev team (`canopycms sync` copies files byte-for-byte) and certain for an
8
- * editorial team. See `.claude/future-tasks/resolved/content-comment-loss-on-editor-save.md`.
4
+ * Stringifying a fresh plain object (`yamlStringify(data)` / `matter.stringify(body, data)`)
5
+ * deletes every comment in the file, because comments live in neither the object nor that round
6
+ * trip. So these functions re-serialise onto the file's OWN parsed document: a node whose value
7
+ * did not change is left untouched, and an untouched node keeps its attached comments and its
8
+ * original quoting/block style. Only what actually changed is rewritten.
9
9
  *
10
- * The fix re-serialises onto the file's OWN parsed document instead of a fresh one: nodes whose
11
- * value did not change are left untouched, and an untouched node keeps its attached comments
12
- * (and its original quoting/block style). Only what actually changed is rewritten.
13
- *
14
- * What this module does NOT do is give the file any authority over its own content. The
15
- * reconciler makes the document's key set match `data` exactly — a key the caller dropped
16
- * disappears, a key the caller kept survives whether or not the schema still knows about it.
17
- * Data authority stays with the payload; comments are the only thing inherited from disk. That
18
- * separation is deliberate: whether a surviving key SHOULD still be there is a schema question,
19
- * answered one layer up by `findUnknownKeys` (validation/entry-validator.ts) at the API
20
- * boundary, not by a serialiser that has no schema.
10
+ * The file gets no authority over its own content. The reconciler makes the document's key set
11
+ * match `data` exactly — a key the caller dropped disappears, a key the caller kept survives
12
+ * whether or not the schema still knows about it. Data authority stays with the payload;
13
+ * comments are the only thing inherited from disk. Whether a surviving key SHOULD still be there
14
+ * is a schema question, answered one layer up by `findUnknownKeys`
15
+ * (validation/entry-validator.ts) at the API boundary, not by a schema-blind serialiser.
21
16
  */
22
17
  import matter from 'gray-matter';
23
18
  import { isCollection, isMap, isNode, isScalar, isSeq, parseDocument, stringify as yamlStringify, } from 'yaml';
@@ -25,20 +20,18 @@ import { isBlockStructuralKey } from '../validation/block-structural-keys.js';
25
20
  /**
26
21
  * True only for a PLAIN object — one that should be reconciled key-by-key against a YAML map.
27
22
  *
28
- * The prototype check is load-bearing, not defensive tidiness. A looser "object and not an array"
29
- * test classifies a class instance as a record, and the reconciler then walks its (empty) own
30
- * enumerable keys and emits `{}` — silently replacing the value. A `Date` is the case that
31
- * actually occurs: HTTP payloads carry none, but `ContentStore.write` is also reachable from
32
- * server-side callers (build scripts via `createBuildCanopy`, migrations), and `d: 2024-01-15`
33
- * became `d: {}` on save. Anything non-plain falls through to `doc.createNode`, which serialises
34
- * it exactly as the pre-fix `yaml.stringify` did.
23
+ * The prototype check is load-bearing. A looser "object and not an array" test classifies a class
24
+ * instance as a record, and the reconciler then walks its (empty) own enumerable keys and emits
25
+ * `{}`, silently replacing the value. `Date` is the case that occurs: HTTP payloads carry none,
26
+ * but `ContentStore.write` is also reachable server-side (build scripts via `createBuildCanopy`,
27
+ * migrations), where a date field came back as `{}`. Anything non-plain falls through to
28
+ * `doc.createNode`, which serialises it exactly as a plain `yaml.stringify` would.
35
29
  *
36
- * Known residual, and the reason this is a prototype check rather than a `toJSON` check: a PLAIN
37
- * object carrying its own `toJSON` is still walked as a record here, while `createNode` (the
38
- * create path, and the scalar-slot path) would call `toJSON` instead — so the same payload can
39
- * serialise differently depending on what is currently on disk, and a `toJSON` function value
40
- * makes `createNode` throw. Unreachable over HTTP, since JSON payloads carry no functions; a loud
41
- * failure rather than corruption if a server-side caller ever hits it.
30
+ * Residual, and the reason this is a prototype rather than a `toJSON` check: a PLAIN object
31
+ * carrying its own `toJSON` is walked as a record here while `createNode` would call `toJSON`,
32
+ * so the same payload can serialise differently depending on what is on disk, and a `toJSON`
33
+ * function value makes `createNode` throw. Unreachable over HTTP (JSON payloads carry no
34
+ * functions), and a loud failure rather than corruption if a server-side caller hits it.
42
35
  */
43
36
  function isPlainRecord(value) {
44
37
  if (typeof value !== 'object' || value === null || Array.isArray(value))
@@ -51,8 +44,7 @@ function isPlainRecord(value) {
51
44
  *
52
45
  * `String(...)` mirrors how parsing into a plain object projects non-string scalar keys
53
46
  * (`1: x` reads back as `{ '1': x }`). A non-scalar (complex) key — `? [a, b] : v` — has no
54
- * record counterpart at all; those pairs are dropped, which is what the old
55
- * stringify-a-fresh-object write did to them too.
47
+ * record counterpart at all; those pairs are dropped.
56
48
  */
57
49
  function recordKeyOf(keyNode) {
58
50
  if (!isScalar(keyNode))
@@ -65,8 +57,8 @@ function recordKeyOf(keyNode) {
65
57
  return String(value);
66
58
  }
67
59
  /**
68
- * Copy the comment metadata attached to a node being REPLACED onto its replacement, so a changed
69
- * value keeps the comments written about it.
60
+ * Copy a REPLACED node's comment metadata onto its replacement, so a changed value keeps the
61
+ * comments written about it.
70
62
  */
71
63
  function carryComments(from, to) {
72
64
  if (!isNode(from) || !isNode(to))
@@ -81,11 +73,10 @@ function carryComments(from, to) {
81
73
  target.spaceBefore = source.spaceBefore;
82
74
  }
83
75
  /**
84
- * Identity key for sequence alignment: the JSON form of a value.
85
- *
86
- * Returns undefined when the value cannot be keyed — a cyclic structure (reachable through YAML
87
- * anchors) makes `JSON.stringify` throw, and a save must never fail because of it. An unkeyable
88
- * item simply matches nothing and falls through to positional reconciliation.
76
+ * Identity key for sequence alignment: the JSON form of a value. Undefined when the value cannot
77
+ * be keyed — a cyclic structure (reachable through YAML anchors) makes `JSON.stringify` throw,
78
+ * and a save must never fail because of it. An unkeyable item matches nothing and falls through
79
+ * to positional reconciliation.
89
80
  */
90
81
  function identityKey(value) {
91
82
  try {
@@ -112,47 +103,33 @@ function nodeIdentityKey(node) {
112
103
  /**
113
104
  * How far {@link sharesFieldEvidence} descends into nested records before giving up.
114
105
  *
115
- * Termination does not actually depend on this — it is defence in depth, deliberately kept
116
- * because the alternative is depending on two other layers behaving as expected, which is the
117
- * shape of assumption that produced the bug {@link looksLikeSameItem} documents. Spelling out
118
- * what the cap is and is not doing, since the obvious guess is wrong in both directions:
119
- *
120
- * - A cyclic PAYLOAD is genuinely reachable (`ContentStore.write` is callable server-side with
121
- * arbitrary objects, which is why `identityKey` guards `JSON.stringify` at all). It cannot run
122
- * away here on its own, because the descent below only happens when BOTH sides are records.
123
- * - The on-disk side cannot supply the matching cycle: `existing` comes from `yaml`'s `toJSON()`,
124
- * and a self-referential anchor (`- &b {self: *b}`) does not round-trip into a cyclic JS
125
- * object — `toJSON` degrades the unresolvable alias to a plain `{ source }` marker (verified
126
- * against the `yaml` version pinned here; the cycle case `identityKey`'s comment describes is
127
- * the `JSON.stringify` hazard, not this one).
128
- *
129
- * So the real bound today is the finite depth of the parsed document — a guarantee owned by
130
- * `yaml`, not by this module. The cap makes it local and explicit, and incidentally bounds cost
131
- * on pathologically nested content. Real content bottoms out far shallower: a block is item ->
132
- * `value` -> fields, and an object field inside one adds a level. Exceeding the cap simply means
133
- * "no evidence found", which drops a comment rather than risking a move — the safe direction.
106
+ * Termination does not depend on it — the cap is defence in depth. A cyclic PAYLOAD is reachable
107
+ * server-side (which is why `identityKey` guards `JSON.stringify`) but cannot run away here: the
108
+ * descent happens only when BOTH sides are records, and the on-disk side comes from `yaml`'s
109
+ * `toJSON()`, which degrades an unresolvable self-referential anchor to a plain `{ source }`
110
+ * marker rather than a cyclic object. The real bound is the parsed document's finite depth, a
111
+ * guarantee owned by `yaml`; the cap makes it local and bounds cost on pathologically nested
112
+ * content. Exceeding it means "no evidence found", which drops a comment rather than risking a
113
+ * move — the safe direction.
134
114
  */
135
115
  const EVIDENCE_MAX_DEPTH = 6;
136
116
  /**
137
117
  * Does `value` share at least one non-structural leaf with `existing` — i.e. is there any field
138
118
  * whose value an edit left alone?
139
119
  *
140
- * Two things this looks past, both learned the hard way:
120
+ * Two things it looks past, and both halves are required together:
141
121
  *
142
122
  * - **Block discriminators are not evidence.** `template` (and the inline shape's `_type`) names
143
- * a block's TEMPLATE, so every `hero` on the page carries the same one. Counting it made the
144
- * check below true for any two blocks of the same kind. See `../validation/block-structural-keys`.
123
+ * a block's TEMPLATE, so every `hero` on the page carries the same one; counting it makes the
124
+ * check true for any two blocks of the same kind. See `../validation/block-structural-keys`.
145
125
  * - **A block's real fields are one level down.** The canonical shape is
146
- * `{ template, value: { ...fields } }`, so comparing top-level values compares `value` whole —
147
- * which differs the moment ANY field in it changes. Together with the point above that left a
148
- * block with no reachable evidence at all, i.e. "drop every block comment on every edit". So
149
- * nested records are descended into.
126
+ * `{ template, value: { ...fields } }`, so comparing top-level values compares `value` whole,
127
+ * which differs the moment ANY field in it changes — with the point above, that leaves a block
128
+ * no reachable evidence at all, i.e. drops every block comment on every edit. So nested
129
+ * records are descended into.
150
130
  *
151
- * Arrays are compared whole rather than element-wise, deliberately. A list has no item identity
152
- * — that is the premise `reconcileSeq` is built on — so pairing elements by index to harvest
153
- * evidence would be the same positional guess this module exists to refuse, just one level down.
154
- * The cost is that a record whose only field is a list loses its comment when that list changes,
155
- * which is the single-field residual documented on {@link looksLikeSameItem}, not a new class.
131
+ * Arrays are compared whole rather than element-wise: pairing elements by index to harvest
132
+ * evidence would be the same positional guess {@link reconcileSeq} refuses, one level down.
156
133
  */
157
134
  function sharesFieldEvidence(existing, value, depth) {
158
135
  for (const key of Object.keys(value)) {
@@ -162,9 +139,8 @@ function sharesFieldEvidence(existing, value, depth) {
162
139
  continue;
163
140
  const before = existing[key];
164
141
  const after = value[key];
165
- // Descending is only ever an EXTRA chance to find evidence: the whole-value comparison below
166
- // still runs, so a pair of structurally-empty records ({} vs {}) still matches on identity
167
- // even though there is no leaf inside them to match on.
142
+ // Descending is only an EXTRA chance to find evidence: the whole-value comparison below still
143
+ // runs, so a pair of structurally-empty records ({} vs {}) still matches on identity.
168
144
  if (depth < EVIDENCE_MAX_DEPTH &&
169
145
  isPlainRecord(before) &&
170
146
  isPlainRecord(after) &&
@@ -179,41 +155,26 @@ function sharesFieldEvidence(existing, value, depth) {
179
155
  }
180
156
  /**
181
157
  * Is `value` plausibly an EDITED version of the item currently at this index, rather than a
182
- * different item that merely landed on the same index?
183
- *
184
- * Position alone is not evidence. A save that replaces one list item wholesale — delete this
185
- * block, add that one — leaves the new item sitting exactly where the old one was, and pairing
186
- * them purely by index moved the old item's comments onto content they do not describe. For the
187
- * comments this change exists to protect ("do not delete this block") that is worse than losing
188
- * them, because a lost comment reads as a deletion in review while a moved one reads as intact.
158
+ * different item that merely landed on the same index? Position alone is not evidence: a save
159
+ * that replaces one list item wholesale leaves the new item exactly where the old one was, and
160
+ * {@link reconcileSeq} explains why pairing them anyway is the worse failure.
189
161
  *
190
162
  * Records carry usable evidence: an edit changes some fields and leaves others alone, so one
191
- * surviving field value means "same item, edited". The evidence has to be a real FIELD, though.
192
- * An earlier version of this rule accepted any shared key/value pair on the reasoning that "a
193
- * wholesale replacement shares nothing" — true of arbitrary records, false of this CMS's block
194
- * shape, where `template: <name>` is a category label every block of that kind carries. A save
195
- * that deleted one `hero` and edited the next shifted the survivor onto the deleted item's
196
- * index, and the discriminator alone was enough to pair them: the deleted block's "keep this
197
- * verbatim" comment silently migrated onto unrelated content. So the module was failing in the
198
- * exact direction it declared unacceptable, through the case it assumed could not arise.
199
- * {@link sharesFieldEvidence} is therefore discriminator-blind and record-deep.
163
+ * surviving field value means "same item, edited". The evidence has to be a real FIELD, which is
164
+ * why {@link sharesFieldEvidence} is discriminator-blind and record-deep — `template: <name>` is
165
+ * a category label every block of that kind carries, so counting it pairs a block deleted from an
166
+ * index with the unrelated survivor that shifted onto it, migrating a "keep this verbatim"
167
+ * comment onto other content. One shared field is the bar, not two: raising it would take a
168
+ * two-field block — the common size — from "keeps its comment when one field is edited" to
169
+ * "never keeps it".
200
170
  *
201
- * Scalars carry no evidence at all, so they keep the plain same-index rule — an edited string in
202
- * a list is overwhelmingly the common case there, and a short annotation on a scalar is far less
203
- * load-bearing than a block comment. That residual is unchanged by the above and is pinned as
204
- * accepted rather than fixed: giving scalars the same treatment would drop the comment on every
205
- * ordinary one-line edit, which is a large, certain loss traded against a small, speculative one.
171
+ * Scalars carry no evidence and keep the plain same-index rule: an edited string in a list is the
172
+ * common case, a short scalar annotation is far less load-bearing than a block comment, and
173
+ * treating scalars the same way would drop the comment on every ordinary one-line edit.
206
174
  *
207
- * One shared field is still the bar, not two. Raising it would take a two-field block — the
208
- * common size — from "keeps its comment when one field is edited" to "never keeps it", which
209
- * empties the rule out for the shape it was just repaired for; the discriminator was the thing
210
- * making one pair meaningless, and it is now excluded.
211
- *
212
- * The residuals, deliberately accepted: a record whose every field changed shares nothing, so
213
- * its comment is dropped rather than risked (this now includes a block edited in ALL its fields,
214
- * which previously kept its comment via the discriminator — a deliberate move toward the losing
215
- * side of the trade-off); and a genuine schema field NAMED `template` or `_type` is not counted
216
- * as evidence, which can only ever drop a comment, never move one.
175
+ * Residuals, deliberately accepted: a record whose every field changed shares nothing, so its
176
+ * comment is dropped rather than risked; and a genuine schema field NAMED `template` or `_type`
177
+ * is not counted as evidence, which can only drop a comment, never move one.
217
178
  */
218
179
  function looksLikeSameItem(node, value) {
219
180
  const existing = nodePlainValue(node);
@@ -223,9 +184,8 @@ function looksLikeSameItem(node, value) {
223
184
  return sharesFieldEvidence(existing, value, 0);
224
185
  }
225
186
  /**
226
- * Reconcile one slot of the document against the value that must occupy it, returning the node
227
- * to put there. `existing` is the node currently in that slot (or null/undefined for a slot that
228
- * did not exist).
187
+ * Reconcile one slot of the document against the value that must occupy it, returning the node to
188
+ * put there. `existing` is the node in that slot, or null/undefined for a slot that did not exist.
229
189
  */
230
190
  function reconcileNode(doc, existing, value) {
231
191
  if (isMap(existing) && isPlainRecord(value)) {
@@ -236,38 +196,34 @@ function reconcileNode(doc, existing, value) {
236
196
  reconcileSeq(doc, existing, value);
237
197
  return existing;
238
198
  }
239
- // Unchanged scalar: return the node itself, untouched. This is the case that preserves
240
- // comments in practice, and it also keeps the author's original quoting and block style.
199
+ // Unchanged scalar: return the node itself, untouched. This is the case that preserves comments
200
+ // in practice, and it keeps the author's original quoting and block style.
241
201
  if (isScalar(existing) && Object.is(existing.value, value))
242
202
  return existing;
243
- // Changed, or a shape change (scalar <-> collection). Build a fresh node rather than mutating
244
- // `scalar.value` in place: an in-place type change would keep the old node's representation,
245
- // emitting `'42'` where the number 42 was meant.
203
+ // Changed, or a shape change (scalar <-> collection). A fresh node rather than mutating
204
+ // `scalar.value` in place, which would keep the old node's representation and emit `'42'` where
205
+ // the number 42 was meant.
246
206
  const fresh = doc.createNode(value);
247
207
  // Comments move with a changed VALUE, but not off a replaced STRUCTURE. `yaml` attaches a
248
- // comment written above a collection's first entry to the collection node itself (the same
249
- // rule that makes a leading list comment a list-head comment), so that node's comments are
250
- // about its innards. Carrying them onto whatever replaces the collection put "# FLAG: explains
251
- // the heading below" above a bare string that has no heading -- a comment over content it does
252
- // not describe, which this module treats as worse than losing it. A comment written above the
253
- // KEY is unaffected either way: it lives on the pair's key node, which is never replaced here.
208
+ // comment written above a collection's first entry to the collection node itself, so that
209
+ // node's comments are about its innards; carrying them onto whatever replaces the collection
210
+ // puts a comment over content it does not describe, which this module treats as worse than
211
+ // losing it. A comment written above the KEY is unaffected: it lives on the pair's key node,
212
+ // which is never replaced here.
254
213
  if (!isCollection(existing))
255
214
  carryComments(existing, fresh);
256
215
  return fresh;
257
216
  }
258
217
  /**
259
- * Make a map's key set match `value` exactly.
260
- *
261
- * Retained pairs are reconciled in place, so their key order and the comments attached to their
262
- * keys survive; keys new to `value` are appended in `value` order, which keeps the diff of a
263
- * save down to the lines that actually changed.
218
+ * Make a map's key set match `value` exactly. Retained pairs are reconciled in place, so their key
219
+ * order and the comments attached to their keys survive; keys new to `value` are appended in
220
+ * `value` order, keeping a save's diff down to the lines that actually changed.
264
221
  */
265
222
  function reconcileMap(doc, map, value) {
266
- // An explicitly-undefined key is NOT a key. `Object.keys` reports it, but both `JSON.stringify`
267
- // and `yaml.stringify` omit it — so without this filter a key present on disk and set to
268
- // `undefined` in the payload would be rewritten as `key: null` here while the create path
269
- // dropped it entirely. Same rule on both paths, so "the key set matches the payload" holds
270
- // exactly rather than approximately.
223
+ // An explicitly-undefined key is NOT a key: `Object.keys` reports it but `JSON.stringify` and
224
+ // `yaml.stringify` omit it, so without this filter a key present on disk and set to `undefined`
225
+ // in the payload is rewritten as `key: null` here while the create path drops it. Same rule on
226
+ // both paths, so "the key set matches the payload" holds exactly rather than approximately.
271
227
  const wanted = new Set(Object.keys(value).filter((key) => value[key] !== undefined));
272
228
  const seen = new Set();
273
229
  const retained = [];
@@ -291,23 +247,19 @@ function reconcileMap(doc, map, value) {
291
247
  /**
292
248
  * Make a sequence's items match `value` exactly, aligning by VALUE first and position second.
293
249
  *
294
- * A list has no item identity — nothing in the payload says "this is the item that used to be
295
- * third" — so any alignment is a guess, and the two failure modes are not equally bad. Losing a
296
- * comment shows up in review as a deletion; silently MOVING a comment onto content it does not
297
- * describe does not, and the comments this fix exists to protect are exactly the load-bearing
298
- * kind ("do not delete this block") that must never end up over the wrong thing. The rules are
299
- * therefore ordered from most evidence to least, and stop rather than guessing:
250
+ * A list has no item identity, so any alignment is a guess and the failure modes are not equally
251
+ * bad: losing a comment shows up in review as a deletion, silently MOVING one onto content it does
252
+ * not describe does not, and the comments this protects are the load-bearing kind ("do not delete
253
+ * this block"). The rules run from most evidence to least, and stop rather than guessing:
300
254
  *
301
- * 1. **Exact value match** — reuse that old node whole, comments and all. This is what carries a
302
- * comment through a pure reorder, instead of stranding it on whatever moved into its index.
255
+ * 1. **Exact value match** — reuse that old node whole, comments and all. This carries a comment
256
+ * through a pure reorder instead of stranding it on whatever moved into its index.
303
257
  * 2. **Same index, still unclaimed, and recognisably the same item** (`looksLikeSameItem`) —
304
- * reconcile against it. This is the edit-in-place case.
258
+ * reconcile against it: the edit-in-place case. Same-index rather than "next unclaimed old
259
+ * node in order", which pairs a newly-inserted item with an unrelated deleted one whenever one
260
+ * save both removes and adds; and evidence as well as position, because a wholesale
261
+ * replacement lands on the index it replaced.
305
262
  * 3. **Otherwise a fresh node, with no comments.**
306
- *
307
- * Rule 2 is deliberately same-index rather than "next unclaimed old node in order": the looser
308
- * form paired a newly-inserted item with an unrelated deleted one whenever one save both removed
309
- * and added an item. Position alone is still not enough, though — a wholesale replacement lands
310
- * on the index it replaced — which is why rule 2 also demands evidence of identity.
311
263
  */
312
264
  function reconcileSeq(doc, seq, value) {
313
265
  const oldItems = seq.items;
@@ -342,9 +294,8 @@ function reconcileSeq(doc, seq, value) {
342
294
  const matched = matches[index];
343
295
  if (matched !== undefined)
344
296
  return oldItems[matched];
345
- // `consumed` holds OLD indices claimed by rule 1, so this asks "is the node that sits at my
346
- // index still unclaimed?". Each iteration owns a distinct index, so no bookkeeping is needed
347
- // here — a candidate cannot be taken twice.
297
+ // `consumed` holds OLD indices claimed by rule 1, so this asks "is the node at my index still
298
+ // unclaimed?". Each iteration owns a distinct index, so a candidate cannot be taken twice.
348
299
  const candidate = index < oldItems.length && !consumed.has(index) ? oldItems[index] : undefined;
349
300
  const sameItem = candidate !== undefined && looksLikeSameItem(candidate, item) ? candidate : undefined;
350
301
  return reconcileNode(doc, sameItem, item);
@@ -357,9 +308,9 @@ function applyDataToDocument(doc, data) {
357
308
  /**
358
309
  * Serialise entry data as a YAML file, carrying the comments of `existingRaw` through.
359
310
  *
360
- * Falls back to a plain stringify — byte-identical to the pre-fix behaviour — when there is
361
- * nothing to preserve (a new file) or nothing trustworthy to preserve (the bytes on disk do not
362
- * parse). A save must not fail because the previous content was malformed.
311
+ * Falls back to a plain stringify — byte-identical to serialising without preservation — when
312
+ * there is nothing to preserve (a new file) or nothing trustworthy to preserve (the bytes on disk
313
+ * do not parse). A save must not fail because the previous content was malformed.
363
314
  */
364
315
  export function serializeYaml(data, existingRaw) {
365
316
  if (existingRaw === undefined)
@@ -371,16 +322,15 @@ export function serializeYaml(data, existingRaw) {
371
322
  return doc.toString();
372
323
  }
373
324
  /**
374
- * Split a file into its raw frontmatter string, or undefined when there is nothing usable.
375
- *
376
- * Two gray-matter hazards are handled here, both of which cost a real bug when discovered:
325
+ * Split a file into its raw frontmatter string, or undefined when there is nothing usable. Two
326
+ * gray-matter hazards:
377
327
  *
378
328
  * 1. **The options argument is load-bearing.** `matter(str)` with no options reads and writes a
379
- * process-global content-keyed cache, and the object it hands back on a HIT has lost
380
- * `.matter` — so the second save of the same file would silently see no frontmatter and drop
381
- * every comment. Passing an options object skips the cache on both sides (see the `if
382
- * (!options)` guard in gray-matter's index.js), which also keeps the write path from
383
- * polluting that cache for everyone else — the same class of problem as `42aede48`.
329
+ * process-global content-keyed cache whose HIT returns an object that has lost `.matter`, so
330
+ * the second save of the same file silently sees no frontmatter and drops every comment.
331
+ * Passing an options object skips the cache on both sides (the `if (!options)` guard in
332
+ * gray-matter's index.js), which also keeps this write path from polluting that cache for
333
+ * everyone else.
384
334
  * 2. **It throws on malformed frontmatter.** js-yaml raises rather than returning an error list,
385
335
  * so a file whose bytes do not parse must not be allowed to fail the save.
386
336
  *
@@ -401,11 +351,10 @@ function extractRawFrontmatter(raw) {
401
351
  return frontmatter;
402
352
  }
403
353
  /**
404
- * Serialise an md/mdx entry, carrying the comments of `existingRaw`'s frontmatter through.
405
- *
406
- * The reconciled YAML goes back through `matter.stringify` via a custom stringify engine rather
407
- * than being spliced between hand-written `---` lines, so the delimiters, blank lines and
408
- * trailing newline stay exactly what gray-matter would have produced.
354
+ * Serialise an md/mdx entry, carrying the comments of `existingRaw`'s frontmatter through. The
355
+ * reconciled YAML goes back through `matter.stringify` via a custom stringify engine rather than
356
+ * being spliced between hand-written `---` lines, so delimiters, blank lines and the trailing
357
+ * newline stay exactly what gray-matter would have produced.
409
358
  */
410
359
  export function serializeFrontmatter(body, data, existingRaw) {
411
360
  if (existingRaw === undefined)
@@ -1,124 +1,69 @@
1
1
  /**
2
- * [SYNC-C1] Cross-host mutual exclusion between the worker's rebase loop and
3
- * content writes against the same branch working tree.
4
- *
5
- * Production runs a Lambda (API) and an EC2 worker over one EFS filesystem.
6
- * `ContentStore`'s write/delete/rename critical sections were guarded only by
7
- * the in-process mutex (`utils/async-mutex`), which does not cross that
8
- * boundary, while the worker's rebase loop (`rebaseOneBranch` in
9
- * worker/rebase.ts) rewrites the very same
10
- * working tree. A write landing after `git rebase` started is destroyed two
11
- * ways -- `git checkout --theirs <file>` overwrites it with the branch's
12
- * committed version (and the rebase then SUCCEEDS, logging nothing), and
13
- * `git rebase --abort` hard-resets the tree. Either way the editor already got
14
- * its 200. This lock is what serializes the two.
15
- *
16
- * **Asymmetric by design.** The worker retries every branch on its next sync
17
- * cycle (~5 minutes) while the editor is a person waiting on a save, so the
18
- * worker yields and the writer waits:
19
- *
20
- * - Worker: {@link tryAcquireContentWriteLock} -- zero retries. On contention
21
- * it skips that branch this cycle (an extension of the existing
22
- * skip-dirty-branches behavior) and picks it up next cycle.
23
- * - Writers: {@link withContentWriteLock} -- a short bounded wait, then a
24
- * retriable failure the API surfaces as a 409.
25
- *
26
- * **Reads never take this lock.** An extra EFS round-trip on every read is not
27
- * an acceptable price, and a read racing a rebase gets a consistent-enough
28
- * older or newer file, never a destroyed one.
29
- *
30
- * ## Why the lock marker lives under `{branchRoot}/.canopy-meta`
31
- *
32
- * proper-lockfile keys its module-level `locks{}` bookkeeping (refresh timer,
33
- * release function) by the **target path** passed to `lock()`, NOT by
34
- * `lockfilePath`. `provisioning-lock.ts` now anchors every lock on its own
35
- * marker path, so the registry key equals the on-disk lock identity and two
36
- * live locks can no longer share a key -- aliasing is structurally impossible
37
- * rather than merely avoided by convention. Keeping the content lock under
38
- * `{branchRoot}/.canopy-meta` still buys:
39
- *
40
- * - a per-branch marker, so one branch's content lock is independent of every
41
- * other branch's and of that branch's provisioning lock; and
42
- * - no deadlock potential: the two locks are never both required, and the only
43
- * possible acquisition order (provision, then write) is consistent.
44
- *
45
- * `.canopy-meta/` is git-excluded in every branch clone (`ensureGitExclude`),
46
- * so the lock directory can never dirty the working tree or be swept into
47
- * `git add .` at publish time.
48
- *
49
- * ## Honest caveat (do not oversell this)
50
- *
51
- * `proper-lockfile` decides a holder is dead by reading the lock directory's
52
- * mtime with `fs.stat`, which on EFS is served through the NFS attribute
53
- * cache. A live holder refreshes every `stale/2`, but a waiter can read a
54
- * cached mtime and conclude the lock is stale when it is not, taking it over.
55
- * The failure mode of a bad takeover is exactly today's behavior (two
56
- * unsynchronized writers), so this is a strict improvement -- not a proof of
57
- * mutual exclusion. See docs/concurrency.md.
2
+ * [SYNC-C1] Cross-host mutual exclusion between the worker's rebase loop and content writes
3
+ * against the same branch working tree. docs/concurrency.md has the layering.
4
+ *
5
+ * Production runs a Lambda (API) and an EC2 worker over one EFS filesystem, so `ContentStore`'s
6
+ * in-process mutex (`utils/async-mutex`) does not cover the worker's rebase (`rebaseOneBranch` in
7
+ * worker/rebase.ts) rewriting the same tree. A write landing after `git rebase` started is
8
+ * destroyed either way -- `git checkout --theirs` overwrites it and the rebase then SUCCEEDS
9
+ * silently, or `git rebase --abort` hard-resets the tree -- and the editor already got its 200.
10
+ *
11
+ * **Asymmetric by design.** The worker retries every branch on its next sync cycle (~5 minutes)
12
+ * while the editor is a person waiting on a save, so the worker yields and the writer waits:
13
+ *
14
+ * - Worker: {@link tryAcquireContentWriteLock} -- zero retries, skips the branch this cycle.
15
+ * - Writers: {@link withContentWriteLock} -- a short bounded wait, then
16
+ * {@link ContentWriteLockBusyError}, which `ContentStore` maps to `BranchSyncingError` and the
17
+ * API to a 409.
18
+ *
19
+ * **Reads never take this lock.** An extra EFS round-trip on every read is not an acceptable
20
+ * price, and a read racing a rebase gets an older or newer file, never a destroyed one.
21
+ *
22
+ * The marker lives under `{branchRoot}/.canopy-meta` and the lock anchors on that marker path like
23
+ * every other lock (see provisioning-lock.ts), so it can never alias the branch's provisioning
24
+ * lock; the two are never both required and the only possible order (provision, then write) is
25
+ * consistent, so they cannot deadlock. `.canopy-meta/` is git-excluded in every branch clone
26
+ * (`ensureGitExclude`), so the lock directory cannot dirty the tree or be swept into `git add .`.
27
+ *
28
+ * Mutual exclusion is not proven: on EFS a stale cached mtime lets a waiter take over a live lock,
29
+ * leaving two unsynchronized writers -- the unlocked behaviour, so still a strict improvement.
58
30
  */
59
31
  import { type OnLockCompromised } from './provisioning-lock.js';
60
- /** On-disk name of the lock marker (a directory, created by mkdir). */
61
- export declare const CONTENT_WRITE_LOCK_NAME = "content-write.lock";
62
32
  /**
63
33
  * Default bounded wait for a content write.
64
34
  *
65
- * Deliberately short. A rebase holds this lock for a fetch plus a replay plus
66
- * N conflict rounds of git subprocesses on EFS -- far longer than any wait an
67
- * interactive save can absorb, and longer than the API's own latency budget.
68
- * So the wait is not sized to outlast a rebase (nothing reasonable could); it
69
- * is sized to absorb lock handoff and the short holds of other writers, and to
70
- * turn everything longer into a fast, explicit "retry" the editor can act on
71
- * rather than a request that hangs toward the Lambda timeout. The worker
72
- * revisits the branch next cycle, so the blocked state is transient.
73
- *
74
- * This is ALSO the writer-vs-writer budget, not only the rebase one. The lock
75
- * is per-branch-root, so every write to one branch now serializes behind it
76
- * where in-process serialization used to be per-entry -- bounded on Lambda
77
- * (one invocation per container), but real under `next dev` and build-time
78
- * provisioning across worker processes. The case to watch is a write whose
79
- * in-lock path triggers a full `idIndex()` rescan of a large tree over EFS:
80
- * that can plausibly exceed this budget and start 409ing unrelated saves.
81
- * Making it configurable for prod tuning is tracked in
35
+ * Deliberately short. A rebase holds this lock for a fetch, a replay and N conflict rounds of git
36
+ * subprocesses on EFS -- longer than an interactive save can absorb -- so the wait is not sized to
37
+ * outlast one. It absorbs lock handoff and other writers' short holds, and turns everything longer
38
+ * into a fast, explicit "retry" rather than a request hanging toward the Lambda timeout; the
39
+ * worker revisits the branch next cycle, so the blocked state is transient.
40
+ *
41
+ * It is ALSO the writer-vs-writer budget: the lock is per-branch-root, so every write to one
42
+ * branch serializes behind it -- bounded on Lambda (one invocation per container), but real under
43
+ * `next dev` and build-time provisioning across worker processes. The case to watch is a write
44
+ * whose in-lock path triggers a full `idIndex()` rescan of a large tree over EFS, which can exceed
45
+ * this budget and start 409ing unrelated saves. Making it configurable is tracked in
82
46
  * .claude/future-tasks/content-write-lock-tuning-and-granularity.md.
83
47
  */
84
48
  export declare const DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2000;
85
49
  /**
86
- * Thrown when the bounded wait expires with the branch's content lock still
87
- * held. Retriable -- callers translate this into a 409 with a message that
88
- * says so.
89
- *
90
- * The message says "syncing OR another save" because BOTH produce it. The
91
- * lock is taken by `write`/`delete`/`renameEntry` and by the admin
92
- * repair-content-duplicates action as well as by the worker's rebase, so a
93
- * message naming only the rebase was a confident claim about something that
94
- * may not be happening. `api/content.ts` routes this error ahead of the
95
- * generic conflict specifically so the editor sees this wording, which makes
96
- * the wording load-bearing rather than cosmetic.
50
+ * Thrown when the bounded wait expires with the branch's content lock still held. Retriable --
51
+ * callers translate it into a 409 with a message that says so.
52
+ *
53
+ * The message says "syncing OR another save" because BOTH produce it: the lock is taken by
54
+ * `write`/`delete`/`renameEntry` and the admin repair-content-duplicates action as well as by the
55
+ * worker's rebase, so naming only the rebase would claim more than is known. `api/content.ts`
56
+ * routes this error ahead of the generic conflict so the editor sees this wording, making it
57
+ * load-bearing rather than cosmetic.
97
58
  */
98
59
  export declare class ContentWriteLockBusyError extends Error {
99
60
  constructor(message?: string);
100
61
  }
101
62
  /**
102
- * Acquire the branch's content-write lock WITHOUT waiting.
103
- *
104
- * Throws an error with `code === 'ELOCKED'` when a live holder has it. Used by
105
- * the worker's rebase loop (which skips the branch and retries next cycle) and
106
- * by tests standing in for a writer.
63
+ * Acquire the branch's content-write lock WITHOUT waiting, for the worker's rebase loop, which
64
+ * skips the branch and retries next cycle. Throws with `code === 'ELOCKED'` on a live holder.
107
65
  */
108
66
  export declare function tryAcquireContentWriteLock(branchRoot: string, onCompromised?: OnLockCompromised): Promise<() => Promise<void>>;
109
- /**
110
- * Acquire the branch's content-write lock with a short bounded wait.
111
- *
112
- * Retries ONLY on genuine contention (`ELOCKED`), the same discipline
113
- * `withOccFileLock` uses: proper-lockfile's own retry loop retries blindly on
114
- * any error, which would burn the whole budget re-hitting e.g. ENOENT after
115
- * the branch directory was deleted out from under the caller.
116
- *
117
- * @throws ContentWriteLockBusyError when the budget expires under contention.
118
- */
119
- export declare function acquireContentWriteLock(branchRoot: string, waitMs?: number, onCompromised?: OnLockCompromised): Promise<() => Promise<void>>;
120
- /**
121
- * Run `fn` holding the branch's content-write lock, releasing it in a
122
- * `finally` so a throw can never strand it.
123
- */
67
+ /** Run `fn` holding the branch's content-write lock, releasing in a `finally` so a throw cannot
68
+ * strand it. */
124
69
  export declare function withContentWriteLock<T>(branchRoot: string, fn: () => Promise<T>, waitMs?: number): Promise<T>;