canopycms 0.0.68-int.98 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (688) hide show
  1. package/CHANGELOG.md +2131 -0
  2. package/README.md +47 -63
  3. package/dist/ai/generate.d.ts +5 -0
  4. package/dist/ai/generate.js +244 -115
  5. package/dist/ai/index.d.ts +1 -1
  6. package/dist/ai/json-to-markdown.d.ts +21 -1
  7. package/dist/ai/json-to-markdown.js +120 -57
  8. package/dist/ai/references.d.ts +72 -0
  9. package/dist/ai/references.js +223 -0
  10. package/dist/ai/resolve-branch.js +9 -2
  11. package/dist/ai/to-plain-text.d.ts +17 -8
  12. package/dist/ai/to-plain-text.js +18 -15
  13. package/dist/ai/types.d.ts +84 -3
  14. package/dist/api/admin-branch-health.d.ts +24 -1
  15. package/dist/api/admin-branch-health.js +52 -20
  16. package/dist/api/admin.d.ts +23 -5
  17. package/dist/api/admin.js +36 -1
  18. package/dist/api/assets.js +88 -78
  19. package/dist/api/branch-create-window.d.ts +12 -0
  20. package/dist/api/branch-create-window.js +13 -0
  21. package/dist/api/branch-merge.d.ts +1 -1
  22. package/dist/api/branch-merge.js +1 -1
  23. package/dist/api/branch-review.d.ts +3 -3
  24. package/dist/api/branch-review.js +16 -7
  25. package/dist/api/branch-status.d.ts +16 -6
  26. package/dist/api/branch-status.js +57 -12
  27. package/dist/api/branch-withdraw.d.ts +2 -2
  28. package/dist/api/branch-withdraw.js +8 -3
  29. package/dist/api/branch.d.ts +25 -12
  30. package/dist/api/branch.js +164 -103
  31. package/dist/api/client.d.ts +78 -58
  32. package/dist/api/client.js +150 -117
  33. package/dist/api/comments.d.ts +12 -3
  34. package/dist/api/comments.js +118 -9
  35. package/dist/api/content.d.ts +9 -9
  36. package/dist/api/content.js +131 -22
  37. package/dist/api/entries-constants.d.ts +14 -3
  38. package/dist/api/entries-constants.js +14 -3
  39. package/dist/api/entries.d.ts +40 -2
  40. package/dist/api/entries.js +102 -6
  41. package/dist/api/github-sync.d.ts +19 -0
  42. package/dist/api/github-sync.js +50 -3
  43. package/dist/api/groups.d.ts +4 -4
  44. package/dist/api/groups.js +2 -1
  45. package/dist/api/guards.js +23 -3
  46. package/dist/api/index.d.ts +3 -1
  47. package/dist/api/index.js +1 -0
  48. package/dist/api/permissions.d.ts +21 -2
  49. package/dist/api/permissions.js +50 -3
  50. package/dist/api/request-deadline.d.ts +59 -0
  51. package/dist/api/request-deadline.js +73 -0
  52. package/dist/api/resolve-references.js +8 -7
  53. package/dist/api/route-builder.d.ts +14 -0
  54. package/dist/api/route-builder.js +14 -1
  55. package/dist/api/schema.d.ts +11 -9
  56. package/dist/api/schema.js +2 -0
  57. package/dist/api/settings-helpers.d.ts +5 -1
  58. package/dist/api/settings-helpers.js +10 -2
  59. package/dist/api/types.d.ts +18 -0
  60. package/dist/api/users-constants.d.ts +6 -0
  61. package/dist/api/users-constants.js +6 -0
  62. package/dist/assets/asset-url.d.ts +28 -16
  63. package/dist/assets/asset-url.js +51 -25
  64. package/dist/assets/finalize.js +3 -0
  65. package/dist/assets/index.d.ts +1 -1
  66. package/dist/assets/materialize.d.ts +153 -0
  67. package/dist/assets/materialize.js +402 -0
  68. package/dist/assets/store-local.d.ts +19 -5
  69. package/dist/assets/store-local.js +69 -10
  70. package/dist/assets/store-s3.d.ts +38 -7
  71. package/dist/assets/store-s3.js +128 -22
  72. package/dist/assets/transform-directives.d.ts +52 -13
  73. package/dist/assets/transform-directives.js +87 -27
  74. package/dist/assets/transform.d.ts +3 -3
  75. package/dist/assets/transform.js +13 -8
  76. package/dist/assets/types.d.ts +31 -4
  77. package/dist/auth/cache.d.ts +1 -1
  78. package/dist/auth/cache.js +1 -1
  79. package/dist/auth/caching-auth-plugin.d.ts +2 -0
  80. package/dist/auth/caching-auth-plugin.js +12 -2
  81. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  82. package/dist/auth/file-based-auth-cache.js +18 -8
  83. package/dist/auth/person-name.d.ts +5 -0
  84. package/dist/auth/person-name.js +8 -0
  85. package/dist/auth/plugin.d.ts +10 -1
  86. package/dist/auth/types.d.ts +2 -0
  87. package/dist/auth/user-metadata-lookup.d.ts +21 -0
  88. package/dist/auth/user-metadata-lookup.js +116 -0
  89. package/dist/authorization/branch.d.ts +1 -1
  90. package/dist/authorization/branch.js +12 -13
  91. package/dist/authorization/groups/schema.d.ts +4 -4
  92. package/dist/authorization/helpers.d.ts +8 -0
  93. package/dist/authorization/helpers.js +8 -0
  94. package/dist/authorization/index.d.ts +1 -1
  95. package/dist/authorization/index.js +1 -1
  96. package/dist/authorization/protected-branch.d.ts +5 -4
  97. package/dist/authorization/protected-branch.js +7 -5
  98. package/dist/branch-health.d.ts +35 -8
  99. package/dist/branch-health.js +54 -18
  100. package/dist/branch-metadata-error.d.ts +27 -0
  101. package/dist/branch-metadata-error.js +23 -0
  102. package/dist/branch-metadata-file.d.ts +205 -24
  103. package/dist/branch-metadata-file.js +126 -25
  104. package/dist/branch-metadata.d.ts +45 -6
  105. package/dist/branch-metadata.js +142 -50
  106. package/dist/branch-provisioning.d.ts +167 -0
  107. package/dist/branch-provisioning.js +501 -0
  108. package/dist/branch-registry.d.ts +8 -0
  109. package/dist/branch-registry.js +61 -9
  110. package/dist/branch-schema-cache.d.ts +29 -12
  111. package/dist/branch-schema-cache.js +64 -21
  112. package/dist/branch-sparse.d.ts +23 -0
  113. package/dist/branch-sparse.js +118 -0
  114. package/dist/branch-workspace.d.ts +42 -3
  115. package/dist/branch-workspace.js +239 -100
  116. package/dist/build/asset-refs.d.ts +94 -0
  117. package/dist/build/asset-refs.js +267 -0
  118. package/dist/build/generate-ai-content.js +5 -0
  119. package/dist/build/inline-flight.d.ts +37 -0
  120. package/dist/build/inline-flight.js +142 -0
  121. package/dist/cli/asset-refs.d.ts +52 -0
  122. package/dist/cli/asset-refs.js +194 -0
  123. package/dist/cli/cli.d.ts +15 -1
  124. package/dist/cli/cli.js +8543 -3193
  125. package/dist/cli/configured-asset-store.d.ts +12 -0
  126. package/dist/cli/configured-asset-store.js +46 -0
  127. package/dist/cli/generate-ai-content.js +4244 -1072
  128. package/dist/cli/init.js +217 -126
  129. package/dist/cli/project-detect.d.ts +2 -0
  130. package/dist/cli/project-detect.js +2 -0
  131. package/dist/cli/template-files/Dockerfile.cms.template +13 -5
  132. package/dist/cli/template-files/cms-stack.ts.template +16 -13
  133. package/dist/client.d.ts +1 -2
  134. package/dist/client.js +1 -2
  135. package/dist/comment-store.d.ts +25 -0
  136. package/dist/comment-store.js +43 -8
  137. package/dist/config/flatten.js +2 -0
  138. package/dist/config/helpers.js +2 -2
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/config/schemas/config.d.ts +12 -29
  141. package/dist/config/schemas/config.js +9 -12
  142. package/dist/config/schemas/field.d.ts +3 -0
  143. package/dist/config/schemas/field.js +58 -4
  144. package/dist/config/schemas/media.d.ts +4 -13
  145. package/dist/config/schemas/media.js +5 -8
  146. package/dist/config/schemas/url.d.ts +2 -20
  147. package/dist/config/schemas/url.js +2 -25
  148. package/dist/config/types.d.ts +116 -22
  149. package/dist/config/types.js +3 -9
  150. package/dist/config/validation.d.ts +25 -4
  151. package/dist/config/validation.js +116 -3
  152. package/dist/content-id-index.js +2 -2
  153. package/dist/content-index-generation.d.ts +1 -1
  154. package/dist/content-index-generation.js +1 -1
  155. package/dist/content-listing.d.ts +10 -1
  156. package/dist/content-listing.js +22 -5
  157. package/dist/content-reader.d.ts +9 -0
  158. package/dist/content-reader.js +16 -11
  159. package/dist/content-store.d.ts +75 -12
  160. package/dist/content-store.js +249 -86
  161. package/dist/content-tree.js +2 -2
  162. package/dist/context.js +5 -2
  163. package/dist/dangling-reference-log.d.ts +17 -0
  164. package/dist/dangling-reference-log.js +29 -0
  165. package/dist/dev-content-watcher.d.ts +1 -1
  166. package/dist/dev-content-watcher.js +1 -1
  167. package/dist/editor/BranchManager.d.ts +23 -7
  168. package/dist/editor/BranchManager.js +119 -54
  169. package/dist/editor/CanopyEditor.d.ts +1 -1
  170. package/dist/editor/CanopyEditor.js +1 -2
  171. package/dist/editor/CommentsPanel.d.ts +14 -0
  172. package/dist/editor/CommentsPanel.js +68 -43
  173. package/dist/editor/Editor.d.ts +11 -5
  174. package/dist/editor/Editor.js +891 -218
  175. package/dist/editor/EditorAuthGate.d.ts +7 -1
  176. package/dist/editor/EditorAuthGate.js +41 -9
  177. package/dist/editor/EntryNavigator.d.ts +22 -0
  178. package/dist/editor/EntryNavigator.js +353 -105
  179. package/dist/editor/FormHeader.d.ts +25 -0
  180. package/dist/editor/FormHeader.js +82 -0
  181. package/dist/editor/FormRenderer.d.ts +27 -3
  182. package/dist/editor/FormRenderer.js +171 -76
  183. package/dist/editor/PreviewFrame.d.ts +40 -0
  184. package/dist/editor/PreviewFrame.js +215 -0
  185. package/dist/editor/PreviewPane.d.ts +47 -0
  186. package/dist/editor/PreviewPane.js +147 -0
  187. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  188. package/dist/editor/PreviewToolbarTools.js +73 -0
  189. package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
  190. package/dist/editor/admin/SystemHealthPanel.js +163 -33
  191. package/dist/editor/admin/useSystemHealth.d.ts +18 -0
  192. package/dist/editor/admin/useSystemHealth.js +70 -1
  193. package/dist/editor/branch-status.d.ts +17 -0
  194. package/dist/editor/branch-status.js +29 -0
  195. package/dist/editor/canopy-path.d.ts +9 -6
  196. package/dist/editor/canopy-path.js +8 -11
  197. package/dist/editor/client-reference-resolver.d.ts +43 -16
  198. package/dist/editor/client-reference-resolver.js +155 -64
  199. package/dist/editor/comments/BranchComments.d.ts +1 -1
  200. package/dist/editor/comments/BranchComments.js +2 -2
  201. package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
  202. package/dist/editor/comments/CloseRefusedNotice.js +7 -0
  203. package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
  204. package/dist/editor/comments/CommentThreadsPopover.js +274 -0
  205. package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
  206. package/dist/editor/comments/CommentWriteAlert.js +13 -0
  207. package/dist/editor/comments/EntryComments.d.ts +10 -4
  208. package/dist/editor/comments/EntryComments.js +14 -17
  209. package/dist/editor/comments/FieldWrapper.d.ts +18 -4
  210. package/dist/editor/comments/FieldWrapper.js +62 -28
  211. package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
  212. package/dist/editor/comments/InlineCommentThread.js +11 -47
  213. package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
  214. package/dist/editor/comments/ThreadCarousel.js +18 -21
  215. package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
  216. package/dist/editor/comments/ThreadReplyControls.js +92 -0
  217. package/dist/editor/comments/field-comment-context.d.ts +14 -0
  218. package/dist/editor/comments/field-comment-context.js +14 -0
  219. package/dist/editor/comments/form-comments.d.ts +48 -0
  220. package/dist/editor/comments/form-comments.js +178 -0
  221. package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
  222. package/dist/editor/comments/item-anchor-copy.js +6 -0
  223. package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
  224. package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
  225. package/dist/editor/comments/popover-placement.d.ts +18 -0
  226. package/dist/editor/comments/popover-placement.js +58 -0
  227. package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
  228. package/dist/editor/comments/use-tracked-writes.js +53 -0
  229. package/dist/editor/components/BranchAccessList.d.ts +15 -0
  230. package/dist/editor/components/BranchAccessList.js +52 -0
  231. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  232. package/dist/editor/components/BranchLockBanner.js +39 -0
  233. package/dist/editor/components/BranchPill.d.ts +32 -0
  234. package/dist/editor/components/BranchPill.js +33 -0
  235. package/dist/editor/components/BranchesDrawer.d.ts +11 -0
  236. package/dist/editor/components/BranchesDrawer.js +5 -0
  237. package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
  238. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  239. package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
  240. package/dist/editor/components/DestructiveMenuItem.js +8 -0
  241. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  242. package/dist/editor/components/EditorCrashScreen.js +22 -0
  243. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  244. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  245. package/dist/editor/components/EditorHeader.d.ts +56 -106
  246. package/dist/editor/components/EditorHeader.js +130 -131
  247. package/dist/editor/components/EditorRail.d.ts +49 -0
  248. package/dist/editor/components/EditorRail.js +65 -0
  249. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  250. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  251. package/dist/editor/components/EntryCreateModal.d.ts +12 -7
  252. package/dist/editor/components/EntryCreateModal.js +51 -57
  253. package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
  254. package/dist/editor/components/NoEditPermissionNotice.js +5 -4
  255. package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
  256. package/dist/editor/components/QuickOpenPalette.js +66 -0
  257. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  258. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  259. package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
  260. package/dist/editor/components/ReasonedMenuItem.js +15 -0
  261. package/dist/editor/components/ReferencedByList.d.ts +11 -0
  262. package/dist/editor/components/ReferencedByList.js +28 -0
  263. package/dist/editor/components/RelativeTime.d.ts +10 -0
  264. package/dist/editor/components/RelativeTime.js +10 -0
  265. package/dist/editor/components/RenameEntryModal.d.ts +9 -1
  266. package/dist/editor/components/RenameEntryModal.js +19 -31
  267. package/dist/editor/components/RetryToastMessage.d.ts +9 -0
  268. package/dist/editor/components/RetryToastMessage.js +6 -0
  269. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  270. package/dist/editor/components/SaveAllResult.js +86 -0
  271. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  272. package/dist/editor/components/SaveSplitButton.js +108 -0
  273. package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
  274. package/dist/editor/components/StagedChangesDrawer.js +92 -0
  275. package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
  276. package/dist/editor/components/UnavailableEntryNotice.js +9 -0
  277. package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
  278. package/dist/editor/components/UnavailableTypeMessage.js +9 -0
  279. package/dist/editor/components/UndoToastMessage.d.ts +18 -0
  280. package/dist/editor/components/UndoToastMessage.js +10 -0
  281. package/dist/editor/components/UserBadge.d.ts +2 -5
  282. package/dist/editor/components/UserBadge.js +62 -33
  283. package/dist/editor/components/ValidationSummary.d.ts +14 -0
  284. package/dist/editor/components/ValidationSummary.js +24 -0
  285. package/dist/editor/components/index.d.ts +2 -2
  286. package/dist/editor/components/index.js +2 -2
  287. package/dist/editor/context/ApiClientContext.d.ts +8 -2
  288. package/dist/editor/context/ApiClientContext.js +19 -8
  289. package/dist/editor/context/AssetContext.d.ts +13 -35
  290. package/dist/editor/context/AssetContext.js +12 -31
  291. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  292. package/dist/editor/context/EditorStateContext.js +4 -4
  293. package/dist/editor/context/SWRProvider.d.ts +3 -3
  294. package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
  295. package/dist/editor/context/SiteMdxAllowContext.js +7 -0
  296. package/dist/editor/context/index.d.ts +2 -1
  297. package/dist/editor/context/index.js +2 -1
  298. package/dist/editor/contrast.d.ts +19 -0
  299. package/dist/editor/contrast.js +41 -0
  300. package/dist/editor/copy.d.ts +18 -0
  301. package/dist/editor/copy.js +18 -0
  302. package/dist/editor/editor-config.js +16 -0
  303. package/dist/editor/editor-utils.d.ts +34 -8
  304. package/dist/editor/editor-utils.js +42 -20
  305. package/dist/editor/entry-search.d.ts +20 -0
  306. package/dist/editor/entry-search.js +34 -0
  307. package/dist/editor/field-breadcrumb.d.ts +12 -0
  308. package/dist/editor/field-breadcrumb.js +85 -0
  309. package/dist/editor/field-props.d.ts +86 -0
  310. package/dist/editor/field-props.js +27 -0
  311. package/dist/editor/field-target.d.ts +30 -0
  312. package/dist/editor/field-target.js +77 -0
  313. package/dist/editor/fields/BlockField.d.ts +12 -0
  314. package/dist/editor/fields/BlockField.js +89 -73
  315. package/dist/editor/fields/CodeField.d.ts +2 -0
  316. package/dist/editor/fields/CodeField.js +6 -2
  317. package/dist/editor/fields/DateTimeField.d.ts +4 -29
  318. package/dist/editor/fields/DateTimeField.js +79 -41
  319. package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
  320. package/dist/editor/fields/ExpandAllButton.js +4 -0
  321. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  322. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  323. package/dist/editor/fields/FieldLabel.d.ts +37 -0
  324. package/dist/editor/fields/FieldLabel.js +19 -0
  325. package/dist/editor/fields/ImageField.d.ts +4 -0
  326. package/dist/editor/fields/ImageField.js +29 -14
  327. package/dist/editor/fields/InlineGroupField.js +4 -3
  328. package/dist/editor/fields/ItemList.d.ts +42 -0
  329. package/dist/editor/fields/ItemList.js +130 -0
  330. package/dist/editor/fields/MarkdownField.d.ts +32 -0
  331. package/dist/editor/fields/MarkdownField.js +223 -46
  332. package/dist/editor/fields/MdxImageDialog.js +1 -1
  333. package/dist/editor/fields/NumberField.d.ts +2 -0
  334. package/dist/editor/fields/NumberField.js +8 -5
  335. package/dist/editor/fields/NumberListField.d.ts +2 -0
  336. package/dist/editor/fields/NumberListField.js +19 -16
  337. package/dist/editor/fields/ObjectField.d.ts +2 -7
  338. package/dist/editor/fields/ObjectField.js +15 -5
  339. package/dist/editor/fields/ObjectListField.d.ts +29 -0
  340. package/dist/editor/fields/ObjectListField.js +57 -0
  341. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  342. package/dist/editor/fields/ReferenceField.js +34 -9
  343. package/dist/editor/fields/SelectField.d.ts +2 -0
  344. package/dist/editor/fields/SelectField.js +11 -2
  345. package/dist/editor/fields/StringListField.d.ts +4 -1
  346. package/dist/editor/fields/StringListField.js +22 -8
  347. package/dist/editor/fields/TextField.d.ts +2 -0
  348. package/dist/editor/fields/TextField.js +6 -2
  349. package/dist/editor/fields/ToggleField.d.ts +2 -0
  350. package/dist/editor/fields/ToggleField.js +8 -3
  351. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  352. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  353. package/dist/editor/fields/form-expansion.d.ts +9 -0
  354. package/dist/editor/fields/form-expansion.js +2 -0
  355. package/dist/editor/fields/item-expansion.d.ts +15 -0
  356. package/dist/editor/fields/item-expansion.js +60 -0
  357. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  358. package/dist/editor/fields/list-item-keys.js +50 -0
  359. package/dist/editor/fields/list-item-title.d.ts +20 -0
  360. package/dist/editor/fields/list-item-title.js +85 -0
  361. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  362. package/dist/editor/fields/markdown-export-options.js +8 -0
  363. package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
  364. package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
  365. package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
  366. package/dist/editor/fields/mdx-jsx-support.js +157 -69
  367. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  368. package/dist/editor/fields/rich-text-failures.js +26 -0
  369. package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
  370. package/dist/editor/fields/toolbar-wrap.js +35 -0
  371. package/dist/editor/form-outline.d.ts +13 -0
  372. package/dist/editor/form-outline.js +70 -0
  373. package/dist/editor/group-manager/GroupCard.d.ts +4 -1
  374. package/dist/editor/group-manager/GroupCard.js +12 -3
  375. package/dist/editor/group-manager/GroupForm.js +1 -1
  376. package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
  377. package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
  378. package/dist/editor/group-manager/MemberList.d.ts +2 -1
  379. package/dist/editor/group-manager/MemberList.js +15 -4
  380. package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
  381. package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
  382. package/dist/editor/group-manager/index.js +34 -5
  383. package/dist/editor/group-manager/types.d.ts +5 -2
  384. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  385. package/dist/editor/hooks/create-branch-request.js +54 -0
  386. package/dist/editor/hooks/save-all.d.ts +68 -0
  387. package/dist/editor/hooks/save-all.js +61 -0
  388. package/dist/editor/hooks/useBranchActions.d.ts +11 -1
  389. package/dist/editor/hooks/useBranchActions.js +42 -19
  390. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  391. package/dist/editor/hooks/useBranchManager.js +321 -125
  392. package/dist/editor/hooks/useBranchesData.d.ts +8 -4
  393. package/dist/editor/hooks/useBranchesData.js +9 -5
  394. package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
  395. package/dist/editor/hooks/useCommentSystem.js +79 -55
  396. package/dist/editor/hooks/useCommentsData.d.ts +11 -7
  397. package/dist/editor/hooks/useCommentsData.js +20 -19
  398. package/dist/editor/hooks/useDraftManager.d.ts +77 -3
  399. package/dist/editor/hooks/useDraftManager.js +887 -94
  400. package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
  401. package/dist/editor/hooks/useEditorLayout.js +98 -20
  402. package/dist/editor/hooks/useEntriesData.d.ts +2 -1
  403. package/dist/editor/hooks/useEntriesData.js +18 -4
  404. package/dist/editor/hooks/useEntryManager.d.ts +41 -2
  405. package/dist/editor/hooks/useEntryManager.js +227 -41
  406. package/dist/editor/hooks/useGroupManager.js +13 -16
  407. package/dist/editor/hooks/usePermissionManager.js +14 -6
  408. package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
  409. package/dist/editor/hooks/usePreviewMarks.js +33 -0
  410. package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
  411. package/dist/editor/hooks/useReferenceResolution.js +34 -177
  412. package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
  413. package/dist/editor/hooks/useSchemaManager.js +97 -39
  414. package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
  415. package/dist/editor/hooks/useUserMetadata.js +76 -47
  416. package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
  417. package/dist/editor/hooks/user-metadata-batcher.js +33 -0
  418. package/dist/editor/media/AssetCard.d.ts +6 -7
  419. package/dist/editor/media/AssetCard.js +24 -31
  420. package/dist/editor/media/AssetListRow.d.ts +16 -0
  421. package/dist/editor/media/AssetListRow.js +41 -0
  422. package/dist/editor/media/CropStep.js +6 -1
  423. package/dist/editor/media/MediaLibrary.d.ts +4 -3
  424. package/dist/editor/media/MediaLibrary.js +11 -4
  425. package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
  426. package/dist/editor/media/MediaLibraryBody.js +195 -80
  427. package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
  428. package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
  429. package/dist/editor/media/crop-math.d.ts +3 -4
  430. package/dist/editor/media/crop-math.js +11 -22
  431. package/dist/editor/media/editor-image-src.d.ts +8 -0
  432. package/dist/editor/media/editor-image-src.js +10 -0
  433. package/dist/editor/media/format-bytes.d.ts +2 -0
  434. package/dist/editor/media/format-bytes.js +23 -0
  435. package/dist/editor/media/media-library-prefs.d.ts +23 -0
  436. package/dist/editor/media/media-library-prefs.js +49 -0
  437. package/dist/editor/media/media-library-view.d.ts +34 -0
  438. package/dist/editor/media/media-library-view.js +40 -0
  439. package/dist/editor/media/useAssetLibrary.d.ts +30 -0
  440. package/dist/editor/media/useAssetLibrary.js +126 -0
  441. package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
  442. package/dist/editor/media/useStableAssetOrder.js +37 -0
  443. package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
  444. package/dist/editor/media/useThumbnailFailure.js +16 -0
  445. package/dist/editor/permission-manager/GroupSelector.js +2 -2
  446. package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
  447. package/dist/editor/permission-manager/PermissionEditor.js +15 -4
  448. package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
  449. package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
  450. package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
  451. package/dist/editor/permission-manager/UserSelector.js +3 -3
  452. package/dist/editor/permission-manager/index.js +12 -3
  453. package/dist/editor/permission-manager/types.d.ts +5 -2
  454. package/dist/editor/platform.d.ts +3 -0
  455. package/dist/editor/platform.js +9 -0
  456. package/dist/editor/preview-asset-base.d.ts +9 -0
  457. package/dist/editor/preview-asset-base.js +17 -0
  458. package/dist/editor/preview-bridge.d.ts +51 -27
  459. package/dist/editor/preview-bridge.js +103 -144
  460. package/dist/editor/preview-marks.d.ts +15 -0
  461. package/dist/editor/preview-marks.js +74 -0
  462. package/dist/editor/preview-path.d.ts +12 -0
  463. package/dist/editor/preview-path.js +45 -1
  464. package/dist/editor/preview-scale.d.ts +27 -0
  465. package/dist/editor/preview-scale.js +34 -0
  466. package/dist/editor/raw-asset-base.d.ts +5 -0
  467. package/dist/editor/raw-asset-base.js +14 -0
  468. package/dist/editor/relative-time.d.ts +4 -4
  469. package/dist/editor/relative-time.js +16 -8
  470. package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
  471. package/dist/editor/schema-editor/CollectionEditor.js +361 -62
  472. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
  473. package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
  474. package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
  475. package/dist/editor/schema-editor/collection-draft.js +40 -0
  476. package/dist/editor/schema-editor/index.d.ts +1 -1
  477. package/dist/editor/schema-label.d.ts +10 -0
  478. package/dist/editor/schema-label.js +17 -0
  479. package/dist/editor/settings-conflict.d.ts +5 -0
  480. package/dist/editor/settings-conflict.js +8 -0
  481. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  482. package/dist/editor/shell/ActivityRail.js +91 -0
  483. package/dist/editor/shell/EditorShell.d.ts +17 -0
  484. package/dist/editor/shell/EditorShell.js +31 -0
  485. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  486. package/dist/editor/shell/PaneLayout.js +116 -0
  487. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  488. package/dist/editor/shell/SidePanelHost.js +184 -0
  489. package/dist/editor/shell/TopBar.d.ts +12 -0
  490. package/dist/editor/shell/TopBar.js +14 -0
  491. package/dist/editor/shell/host-chrome.d.ts +16 -0
  492. package/dist/editor/shell/host-chrome.js +69 -0
  493. package/dist/editor/slug.d.ts +16 -0
  494. package/dist/editor/slug.js +43 -0
  495. package/dist/editor/theme.d.ts +2 -1
  496. package/dist/editor/theme.js +314 -13
  497. package/dist/editor/unanswered.d.ts +17 -0
  498. package/dist/editor/unanswered.js +26 -0
  499. package/dist/editor/unavailable-entry-type.d.ts +20 -0
  500. package/dist/editor/unavailable-entry-type.js +28 -0
  501. package/dist/editor/undo-removal.d.ts +57 -0
  502. package/dist/editor/undo-removal.js +163 -0
  503. package/dist/editor/user-display.d.ts +4 -0
  504. package/dist/editor/user-display.js +5 -0
  505. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  506. package/dist/editor/utils/confirm-modal.js +12 -0
  507. package/dist/editor/utils/editor-errors.d.ts +19 -0
  508. package/dist/editor/utils/editor-errors.js +32 -0
  509. package/dist/editor/utils/focus-return.d.ts +56 -0
  510. package/dist/editor/utils/focus-return.js +106 -0
  511. package/dist/entry-link-resolver.d.ts +11 -1
  512. package/dist/entry-link-resolver.js +65 -5
  513. package/dist/entry-schema-registry.d.ts +3 -3
  514. package/dist/entry-schema-registry.js +7 -4
  515. package/dist/entry-schema.d.ts +49 -3
  516. package/dist/entry-schema.js +18 -0
  517. package/dist/git-manager.d.ts +172 -10
  518. package/dist/git-manager.js +449 -107
  519. package/dist/github-service.d.ts +39 -2
  520. package/dist/github-service.js +101 -7
  521. package/dist/http/handler.js +74 -9
  522. package/dist/http/types.d.ts +2 -0
  523. package/dist/http/worker-not-ready.d.ts +9 -4
  524. package/dist/http/worker-not-ready.js +59 -6
  525. package/dist/index.d.ts +2 -0
  526. package/dist/index.js +3 -0
  527. package/dist/item-keys.d.ts +46 -0
  528. package/dist/item-keys.js +235 -0
  529. package/dist/operating-mode/editor-mode-check.d.ts +15 -0
  530. package/dist/operating-mode/editor-mode-check.js +24 -0
  531. package/dist/operating-mode/mode-env.d.ts +13 -14
  532. package/dist/operating-mode/mode-env.js +13 -14
  533. package/dist/operating-mode/types.d.ts +2 -2
  534. package/dist/paths/branch.d.ts +0 -1
  535. package/dist/paths/branch.js +1 -8
  536. package/dist/paths/index.d.ts +1 -1
  537. package/dist/paths/index.js +1 -1
  538. package/dist/paths/validation.js +0 -2
  539. package/dist/preview.d.ts +8 -0
  540. package/dist/preview.js +7 -0
  541. package/dist/reference-resolver.js +1 -0
  542. package/dist/resolve-canopy-user.d.ts +1 -1
  543. package/dist/resolve-canopy-user.js +1 -1
  544. package/dist/schema/meta-loader.d.ts +18 -8
  545. package/dist/schema/meta-loader.js +42 -9
  546. package/dist/schema/registry-fingerprint.d.ts +14 -0
  547. package/dist/schema/registry-fingerprint.js +63 -0
  548. package/dist/schema/resolver.d.ts +7 -4
  549. package/dist/schema/resolver.js +7 -6
  550. package/dist/schema/schema-store.d.ts +7 -10
  551. package/dist/schema/schema-store.js +13 -11
  552. package/dist/schema/schema-unavailable-error.d.ts +11 -0
  553. package/dist/schema/schema-unavailable-error.js +15 -0
  554. package/dist/schema/types.d.ts +22 -0
  555. package/dist/schema-registry-record.d.ts +35 -0
  556. package/dist/schema-registry-record.js +134 -0
  557. package/dist/server.d.ts +79 -9
  558. package/dist/server.js +62 -8
  559. package/dist/services.d.ts +33 -5
  560. package/dist/services.js +155 -37
  561. package/dist/settings-workspace.d.ts +23 -3
  562. package/dist/settings-workspace.js +159 -45
  563. package/dist/stamp-on-publish.d.ts +36 -0
  564. package/dist/stamp-on-publish.js +71 -0
  565. package/dist/static/index.d.ts +58 -5
  566. package/dist/static/index.js +133 -3
  567. package/dist/static/seo.d.ts +5 -5
  568. package/dist/static/seo.js +5 -5
  569. package/dist/submission-attribution.d.ts +8 -0
  570. package/dist/submission-attribution.js +22 -0
  571. package/dist/system-users.d.ts +12 -0
  572. package/dist/system-users.js +12 -0
  573. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  574. package/dist/task-queue/cms-task-queue.js +1 -1
  575. package/dist/task-queue/index.d.ts +1 -1
  576. package/dist/task-queue/index.js +1 -1
  577. package/dist/task-queue/task-queue.d.ts +6 -0
  578. package/dist/task-queue/task-queue.js +29 -0
  579. package/dist/task-queue/worker-status.d.ts +35 -9
  580. package/dist/task-queue/worker-status.js +84 -17
  581. package/dist/types.d.ts +90 -9
  582. package/dist/url-collision.js +3 -3
  583. package/dist/url-exclusivity-fixtures.d.ts +8 -1
  584. package/dist/url-exclusivity-fixtures.js +11 -1
  585. package/dist/user.js +2 -1
  586. package/dist/utils/atomic-write.js +2 -1
  587. package/dist/utils/base-branch.d.ts +13 -0
  588. package/dist/utils/base-branch.js +23 -0
  589. package/dist/utils/content-serialize.d.ts +15 -7
  590. package/dist/utils/content-serialize.js +353 -51
  591. package/dist/utils/content-write-lock.d.ts +6 -1
  592. package/dist/utils/content-write-lock.js +21 -6
  593. package/dist/utils/debug.d.ts +1 -1
  594. package/dist/utils/debug.js +5 -3
  595. package/dist/utils/error.js +13 -2
  596. package/dist/utils/escape-text.d.ts +7 -0
  597. package/dist/utils/escape-text.js +27 -0
  598. package/dist/utils/fs.d.ts +12 -4
  599. package/dist/utils/fs.js +18 -5
  600. package/dist/utils/git.d.ts +21 -1
  601. package/dist/utils/git.js +63 -8
  602. package/dist/utils/json-source-splice.d.ts +14 -0
  603. package/dist/utils/json-source-splice.js +120 -0
  604. package/dist/utils/markdown-body-splice.d.ts +39 -0
  605. package/dist/utils/markdown-body-splice.js +421 -0
  606. package/dist/utils/occ-json-write.js +18 -12
  607. package/dist/utils/provision-log.d.ts +25 -0
  608. package/dist/utils/provision-log.js +49 -0
  609. package/dist/utils/provisioning-lock.d.ts +32 -1
  610. package/dist/utils/provisioning-lock.js +78 -32
  611. package/dist/utils/sanitize-href.d.ts +9 -23
  612. package/dist/utils/sanitize-href.js +14 -28
  613. package/dist/utils/title-field.d.ts +10 -0
  614. package/dist/utils/title-field.js +47 -0
  615. package/dist/utils/yaml-source-splice.d.ts +58 -0
  616. package/dist/utils/yaml-source-splice.js +515 -0
  617. package/dist/validation/block-structural-keys.d.ts +1 -1
  618. package/dist/validation/block-structural-keys.js +1 -1
  619. package/dist/validation/deletion-checker.d.ts +21 -29
  620. package/dist/validation/deletion-checker.js +41 -117
  621. package/dist/validation/entry-validator.d.ts +6 -10
  622. package/dist/validation/entry-validator.js +19 -26
  623. package/dist/validation/field-traversal.d.ts +22 -0
  624. package/dist/validation/field-traversal.js +39 -8
  625. package/dist/validation/markdown-safety.d.ts +68 -0
  626. package/dist/validation/markdown-safety.js +596 -0
  627. package/dist/validation/mdx-allowlist.d.ts +55 -0
  628. package/dist/validation/mdx-allowlist.js +183 -0
  629. package/dist/validation/reference-validator.d.ts +2 -0
  630. package/dist/validation/reference-validator.js +3 -0
  631. package/dist/version.d.ts +1 -1
  632. package/dist/version.js +1 -1
  633. package/dist/worker/branch-policy.d.ts +124 -0
  634. package/dist/worker/branch-policy.js +243 -0
  635. package/dist/worker/bundle-header.d.ts +117 -0
  636. package/dist/worker/bundle-header.js +253 -0
  637. package/dist/worker/cms-worker.d.ts +236 -142
  638. package/dist/worker/cms-worker.js +772 -285
  639. package/dist/worker/gateway-egress.d.ts +43 -0
  640. package/dist/worker/gateway-egress.js +209 -0
  641. package/dist/worker/gateway-instance.d.ts +42 -0
  642. package/dist/worker/gateway-instance.js +129 -0
  643. package/dist/worker/git-process.d.ts +81 -0
  644. package/dist/worker/git-process.js +234 -0
  645. package/dist/worker/git-sync.d.ts +34 -17
  646. package/dist/worker/git-sync.js +394 -131
  647. package/dist/worker/github-auth.d.ts +9 -9
  648. package/dist/worker/github-auth.js +13 -15
  649. package/dist/worker/github-gateway-http.d.ts +62 -0
  650. package/dist/worker/github-gateway-http.js +977 -0
  651. package/dist/worker/github-gateway-wire.d.ts +590 -0
  652. package/dist/worker/github-gateway-wire.js +488 -0
  653. package/dist/worker/github-gateway.d.ts +238 -0
  654. package/dist/worker/github-gateway.js +726 -0
  655. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  656. package/dist/worker/github-mirror-upkeep.js +266 -0
  657. package/dist/worker/github-mirror.d.ts +364 -0
  658. package/dist/worker/github-mirror.js +1030 -0
  659. package/dist/worker/github-transfer.d.ts +56 -0
  660. package/dist/worker/github-transfer.js +444 -0
  661. package/dist/worker/history-rewrite.d.ts +4 -5
  662. package/dist/worker/history-rewrite.js +12 -17
  663. package/dist/worker/provisioned-workspace.d.ts +5 -5
  664. package/dist/worker/provisioned-workspace.js +5 -5
  665. package/dist/worker/rebase.d.ts +1 -1
  666. package/dist/worker/rebase.js +58 -54
  667. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  668. package/dist/worker/remote-git-maintenance.js +12 -0
  669. package/dist/worker/schema-gate.d.ts +49 -0
  670. package/dist/worker/schema-gate.js +226 -0
  671. package/dist/worker/self-checks.d.ts +107 -0
  672. package/dist/worker/self-checks.js +358 -0
  673. package/dist/worker/shared-repo-git.d.ts +96 -0
  674. package/dist/worker/shared-repo-git.js +459 -0
  675. package/dist/worker/sparse-cone.d.ts +20 -0
  676. package/dist/worker/sparse-cone.js +136 -0
  677. package/dist/worker/stale-mirror.d.ts +82 -0
  678. package/dist/worker/stale-mirror.js +130 -0
  679. package/dist/worker/task-runner.d.ts +5 -3
  680. package/dist/worker/task-runner.js +287 -186
  681. package/dist/worker/worker-context.d.ts +36 -37
  682. package/package.json +35 -7
  683. package/dist/editor/EditorPanes.d.ts +0 -10
  684. package/dist/editor/EditorPanes.js +0 -98
  685. package/dist/editor/components/EditorFooter.d.ts +0 -11
  686. package/dist/editor/components/EditorFooter.js +0 -9
  687. package/dist/editor/components/EditorSidebar.d.ts +0 -67
  688. package/dist/editor/components/EditorSidebar.js +0 -22
@@ -0,0 +1,23 @@
1
+ /** The base branch is unknown, and prod never assumes one. */
2
+ export class BaseBranchUnresolvedError extends Error {
3
+ constructor(detail) {
4
+ super(`CanopyCMS: ${detail} Set defaultBaseBranch in your CanopyCMS config to the branch ` +
5
+ 'editing branches fork from.');
6
+ this.name = 'BaseBranchUnresolvedError';
7
+ }
8
+ }
9
+ /**
10
+ * `config`'s resolved base branch. Prod resolves an unset one from the remote's HEAD
11
+ * (createCanopyServices, then resolvePendingBaseBranch per request), so an unset value in prod
12
+ * means that has not happened yet, or the path skipped it, and it throws rather than guessing
13
+ * 'main'. In the other modes a config that never went through
14
+ * services keeps the 'main' default; services itself resolves dev's from git HEAD.
15
+ */
16
+ export function baseBranchOf(config) {
17
+ if (config.defaultBaseBranch)
18
+ return config.defaultBaseBranch;
19
+ if (config.mode === 'prod') {
20
+ throw new BaseBranchUnresolvedError('the prod base branch has not been resolved.');
21
+ }
22
+ return 'main';
23
+ }
@@ -5,7 +5,9 @@
5
5
  * deletes every comment in the file, because comments live in neither the object nor that round
6
6
  * trip. So these functions re-serialise onto the file's OWN parsed document: a node whose value
7
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.
8
+ * original quoting/block style. Only what actually changed is rewritten, and
9
+ * `yaml-source-splice.ts` writes it into the file's own text, so untouched lines keep their bytes
10
+ * wherever it can vouch for the result.
9
11
  *
10
12
  * The file gets no authority over its own content. The reconciler makes the document's key set
11
13
  * match `data` exactly — a key the caller dropped disappears, a key the caller kept survives
@@ -14,18 +16,24 @@
14
16
  * is a schema question, answered one layer up by `findUnknownKeys`
15
17
  * (validation/entry-validator.ts) at the API boundary, not by a schema-blind serialiser.
16
18
  */
19
+ import { type MarkdownBodyFormat } from './markdown-body-splice.js';
17
20
  /**
18
21
  * Serialise entry data as a YAML file, carrying the comments of `existingRaw` through.
19
22
  *
20
23
  * Falls back to a plain stringify — byte-identical to serialising without preservation — when
21
24
  * there is nothing to preserve (a new file) or nothing trustworthy to preserve (the bytes on disk
22
- * do not parse). A save must not fail because the previous content was malformed.
25
+ * do not parse, or the reconciled document does not read back as its own data). A save must not
26
+ * fail because the previous content was malformed.
23
27
  */
24
28
  export declare function serializeYaml(data: Record<string, unknown>, existingRaw?: string): string;
25
29
  /**
26
- * Serialise an md/mdx entry, carrying the comments of `existingRaw`'s frontmatter through. The
27
- * reconciled YAML goes back through `matter.stringify` via a custom stringify engine rather than
28
- * being spliced between hand-written `---` lines, so delimiters, blank lines and the trailing
29
- * newline stay exactly what gray-matter would have produced.
30
+ * Serialise an md/mdx entry, carrying the comments of `existingRaw`'s frontmatter and the source
31
+ * text of its unchanged body blocks through. The reconciled YAML goes back through
32
+ * `matter.stringify` via a custom stringify engine rather than being spliced between
33
+ * hand-written `---` lines, so delimiters and the trailing newline stay exactly what gray-matter
34
+ * would have produced. The result is kept only if the read path reads `data` back from it;
35
+ * otherwise gray-matter writes it, without the comments. A fresh string the read path would
36
+ * misread is quoted (`readsAsAnotherValue`), so this is left for a changed value meeting text the
37
+ * two parsers read differently, as 14 meets a file's `014`.
30
38
  */
31
- export declare function serializeFrontmatter(body: string, data: Record<string, unknown>, existingRaw?: string): string;
39
+ export declare function serializeFrontmatter(editorBody: string, data: Record<string, unknown>, existingRaw: string | undefined, format: MarkdownBodyFormat): string;
@@ -5,7 +5,9 @@
5
5
  * deletes every comment in the file, because comments live in neither the object nor that round
6
6
  * trip. So these functions re-serialise onto the file's OWN parsed document: a node whose value
7
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.
8
+ * original quoting/block style. Only what actually changed is rewritten, and
9
+ * `yaml-source-splice.ts` writes it into the file's own text, so untouched lines keep their bytes
10
+ * wherever it can vouch for the result.
9
11
  *
10
12
  * The file gets no authority over its own content. The reconciler makes the document's key set
11
13
  * match `data` exactly — a key the caller dropped disappears, a key the caller kept survives
@@ -14,9 +16,31 @@
14
16
  * is a schema question, answered one layer up by `findUnknownKeys`
15
17
  * (validation/entry-validator.ts) at the API boundary, not by a schema-blind serialiser.
16
18
  */
19
+ import { isDeepStrictEqual } from 'node:util';
17
20
  import matter from 'gray-matter';
18
- import { isCollection, isMap, isNode, isScalar, isSeq, parseDocument, stringify as yamlStringify, } from 'yaml';
21
+ import { isCollection, isMap, isNode, isScalar, isSeq, parseDocument, Scalar, stringify as yamlStringify, visit, } from 'yaml';
19
22
  import { isBlockStructuralKey } from '../validation/block-structural-keys.js';
23
+ import { ITEM_KEY, isItemKey } from '../item-keys.js';
24
+ import { createDebugLogger } from './debug.js';
25
+ import { getErrorMessage } from './error.js';
26
+ import { preserveMarkdownSource } from './markdown-body-splice.js';
27
+ import { snapshotDocument, spliceSource, withSourceLineEndings } from './yaml-source-splice.js';
28
+ const log = createDebugLogger({ prefix: 'ContentSerialize' });
29
+ /** A fresh node for `value`, its strings that `ctx.quote` names single-quoted. */
30
+ function createNode(ctx, value, asKey = false) {
31
+ const node = ctx.doc.createNode(value);
32
+ const { quote } = ctx;
33
+ if (quote !== undefined) {
34
+ visit(node, {
35
+ Scalar(key, scalar) {
36
+ if (typeof scalar.value === 'string' && quote(scalar.value, asKey || key === 'key')) {
37
+ scalar.type = Scalar.QUOTE_SINGLE;
38
+ }
39
+ },
40
+ });
41
+ }
42
+ return node;
43
+ }
20
44
  /**
21
45
  * True only for a PLAIN object — one that should be reconciled key-by-key against a YAML map.
22
46
  *
@@ -86,6 +110,27 @@ function identityKey(value) {
86
110
  return undefined;
87
111
  }
88
112
  }
113
+ /** A list item's usable `ITEM_KEY`, or undefined. */
114
+ function itemKeyOf(value) {
115
+ const key = isPlainRecord(value) ? value[ITEM_KEY] : undefined;
116
+ return isItemKey(key) ? key : undefined;
117
+ }
118
+ /** {@link identityKey} with every `ITEM_KEY` left out, or undefined when there were none. */
119
+ function unkeyedIdentityKey(value) {
120
+ let found = false;
121
+ try {
122
+ const key = JSON.stringify(value, (name, v) => {
123
+ if (name !== ITEM_KEY)
124
+ return v;
125
+ found = true;
126
+ return undefined;
127
+ });
128
+ return found ? key : undefined;
129
+ }
130
+ catch {
131
+ return undefined;
132
+ }
133
+ }
89
134
  function nodePlainValue(node) {
90
135
  if (!isNode(node))
91
136
  return node;
@@ -181,19 +226,24 @@ function looksLikeSameItem(node, value) {
181
226
  // No record evidence available on either side — fall back to position.
182
227
  if (!isPlainRecord(existing) || !isPlainRecord(value))
183
228
  return true;
229
+ // Two keyed list items are the same item exactly when their keys match (`item-keys.ts`).
230
+ const before = existing[ITEM_KEY];
231
+ const after = value[ITEM_KEY];
232
+ if (isItemKey(before) && isItemKey(after))
233
+ return before === after;
184
234
  return sharesFieldEvidence(existing, value, 0);
185
235
  }
186
236
  /**
187
237
  * Reconcile one slot of the document against the value that must occupy it, returning the node to
188
238
  * put there. `existing` is the node in that slot, or null/undefined for a slot that did not exist.
189
239
  */
190
- function reconcileNode(doc, existing, value) {
240
+ function reconcileNode(ctx, existing, value) {
191
241
  if (isMap(existing) && isPlainRecord(value)) {
192
- reconcileMap(doc, existing, value);
242
+ reconcileMap(ctx, existing, value);
193
243
  return existing;
194
244
  }
195
245
  if (isSeq(existing) && Array.isArray(value)) {
196
- reconcileSeq(doc, existing, value);
246
+ reconcileSeq(ctx, existing, value);
197
247
  return existing;
198
248
  }
199
249
  // Unchanged scalar: return the node itself, untouched. This is the case that preserves comments
@@ -203,7 +253,16 @@ function reconcileNode(doc, existing, value) {
203
253
  // Changed, or a shape change (scalar <-> collection). A fresh node rather than mutating
204
254
  // `scalar.value` in place, which would keep the old node's representation and emit `'42'` where
205
255
  // the number 42 was meant.
206
- const fresh = doc.createNode(value);
256
+ const fresh = createNode(ctx, value);
257
+ if (isNode(existing))
258
+ ctx.replaced.set(fresh, existing);
259
+ if (isScalar(existing) && isScalar(fresh) && typeof value === 'string') {
260
+ const style = carriedStyle(existing, value);
261
+ if (style !== undefined) {
262
+ fresh.type = style;
263
+ ctx.restyled.push(fresh);
264
+ }
265
+ }
207
266
  // Comments move with a changed VALUE, but not off a replaced STRUCTURE. `yaml` attaches a
208
267
  // comment written above a collection's first entry to the collection node itself, so that
209
268
  // node's comments are about its innards; carrying them onto whatever replaces the collection
@@ -214,12 +273,33 @@ function reconcileNode(doc, existing, value) {
214
273
  carryComments(existing, fresh);
215
274
  return fresh;
216
275
  }
276
+ /**
277
+ * The style a changed string inherits from the string it replaces: its quoting, or its block
278
+ * style (`>-`, `|`) unless the new value has no content or leads with whitespace, which `yaml`
279
+ * can print lossily in a block (`" "` reads back as `""`). Never `PLAIN`: forcing it stops `yaml` choosing a
280
+ * block for a multi-line value, and some (`Requirements:\nBring a laptop`) then print as
281
+ * unparseable YAML. An unset style is plain where plain is safe.
282
+ */
283
+ function carriedStyle(existing, value) {
284
+ if (typeof existing.value !== 'string')
285
+ return undefined;
286
+ switch (existing.type) {
287
+ case Scalar.QUOTE_DOUBLE:
288
+ case Scalar.QUOTE_SINGLE:
289
+ return existing.type;
290
+ case Scalar.BLOCK_FOLDED:
291
+ case Scalar.BLOCK_LITERAL:
292
+ return /\S/.test(value) && !/^\s/.test(value) ? existing.type : undefined;
293
+ default:
294
+ return undefined;
295
+ }
296
+ }
217
297
  /**
218
298
  * Make a map's key set match `value` exactly. Retained pairs are reconciled in place, so their key
219
299
  * order and the comments attached to their keys survive; keys new to `value` are appended in
220
300
  * `value` order, keeping a save's diff down to the lines that actually changed.
221
301
  */
222
- function reconcileMap(doc, map, value) {
302
+ function reconcileMap(ctx, map, value) {
223
303
  // An explicitly-undefined key is NOT a key: `Object.keys` reports it but `JSON.stringify` and
224
304
  // `yaml.stringify` omit it, so without this filter a key present on disk and set to `undefined`
225
305
  // in the payload is rewritten as `key: null` here while the create path drops it. Same rule on
@@ -234,34 +314,40 @@ function reconcileMap(doc, map, value) {
234
314
  if (key === undefined || !wanted.has(key) || seen.has(key))
235
315
  continue;
236
316
  seen.add(key);
237
- pair.value = reconcileNode(doc, pair.value, value[key]);
317
+ pair.value = reconcileNode(ctx, pair.value, value[key]);
238
318
  retained.push(pair);
239
319
  }
240
320
  map.items = retained;
241
321
  for (const key of Object.keys(value)) {
242
322
  if (seen.has(key) || !wanted.has(key))
243
323
  continue;
244
- map.set(doc.createNode(key), doc.createNode(value[key]));
324
+ map.set(createNode(ctx, key, true), createNode(ctx, value[key]));
245
325
  }
246
326
  }
247
327
  /**
248
- * Make a sequence's items match `value` exactly, aligning by VALUE first and position second.
328
+ * Make a sequence's items match `value` exactly, aligning by VALUE first, then by item key
329
+ * (`item-keys.ts`), then by position.
249
330
  *
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:
331
+ * Without keys a list has no item identity, so any alignment is a guess and the failure modes are
332
+ * not equally bad: losing a comment shows up in review as a deletion, silently MOVING one onto
333
+ * content it does not describe does not, and the comments this protects are the load-bearing kind
334
+ * ("do not delete this block"). The rules run from most evidence to least, and stop rather than
335
+ * guessing:
254
336
  *
255
337
  * 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.
257
- * 2. **Same index, still unclaimed, and recognisably the same item** (`looksLikeSameItem`) —
338
+ * through a pure reorder instead of stranding it on whatever moved into its index. On the save
339
+ * that first writes keys, an item also matches the un-keyed node it was read from, reconciled
340
+ * so the keys are written.
341
+ * 2. **Same key** — an edited keyed item is reconciled against the node holding its key, wherever
342
+ * it was, so its comments move with it.
343
+ * 3. **Same index, still unclaimed, and recognisably the same item** (`looksLikeSameItem`) —
258
344
  * reconcile against it: the edit-in-place case. Same-index rather than "next unclaimed old
259
345
  * node in order", which pairs a newly-inserted item with an unrelated deleted one whenever one
260
346
  * save both removes and adds; and evidence as well as position, because a wholesale
261
- * replacement lands on the index it replaced.
262
- * 3. **Otherwise a fresh node, with no comments.**
347
+ * replacement lands on the index it replaced. Items whose keys differ are never the same.
348
+ * 4. **Otherwise a fresh node, with no comments.**
263
349
  */
264
- function reconcileSeq(doc, seq, value) {
350
+ function reconcileSeq(ctx, seq, value) {
265
351
  const oldItems = seq.items;
266
352
  // Old indices by identity, in order, so equal items are consumed first-come-first-served.
267
353
  const byIdentity = new Map();
@@ -276,11 +362,8 @@ function reconcileSeq(doc, seq, value) {
276
362
  byIdentity.set(key, [index]);
277
363
  });
278
364
  const consumed = new Set();
279
- const matches = value.map((item) => {
280
- const key = identityKey(item);
281
- if (key === undefined)
282
- return undefined;
283
- const bucket = byIdentity.get(key);
365
+ const claim = (key) => {
366
+ const bucket = key === undefined ? undefined : byIdentity.get(key);
284
367
  while (bucket && bucket.length > 0) {
285
368
  const index = bucket.shift();
286
369
  if (index !== undefined && !consumed.has(index)) {
@@ -289,37 +372,104 @@ function reconcileSeq(doc, seq, value) {
289
372
  }
290
373
  }
291
374
  return undefined;
375
+ };
376
+ // The save that first writes item keys (`item-keys.ts`) adds them to items the file holds
377
+ // without, so an item is also the same as an un-keyed one equal to it but for its keys. That
378
+ // node is reconciled rather than kept whole, which is what writes the keys in.
379
+ const reconciles = new Set();
380
+ const matches = value.map((item, index) => {
381
+ const exact = claim(identityKey(item));
382
+ if (exact !== undefined)
383
+ return exact;
384
+ const unkeyed = claim(unkeyedIdentityKey(item));
385
+ if (unkeyed !== undefined)
386
+ reconciles.add(index);
387
+ return unkeyed;
388
+ });
389
+ // An edited keyed item is the node holding its key, wherever it moved, so its comments go with
390
+ // it. Claimed after every unedited item, so none of those loses its node to it.
391
+ const oldKeys = oldItems.map((node) => itemKeyOf(nodePlainValue(node)));
392
+ value.forEach((item, index) => {
393
+ const key = matches[index] === undefined ? itemKeyOf(item) : undefined;
394
+ if (key === undefined)
395
+ return;
396
+ const at = oldKeys.findIndex((k, j) => k === key && !consumed.has(j));
397
+ if (at === -1)
398
+ return;
399
+ consumed.add(at);
400
+ matches[index] = at;
401
+ reconciles.add(index);
292
402
  });
293
403
  seq.items = value.map((item, index) => {
294
404
  const matched = matches[index];
295
- if (matched !== undefined)
296
- return oldItems[matched];
297
- // `consumed` holds OLD indices claimed by rule 1, so this asks "is the node at my index still
405
+ if (matched !== undefined) {
406
+ return reconciles.has(index) ? reconcileNode(ctx, oldItems[matched], item) : oldItems[matched];
407
+ }
408
+ // `consumed` holds OLD indices claimed by rules 1 and 2, so this asks "is the node at my index still
298
409
  // unclaimed?". Each iteration owns a distinct index, so a candidate cannot be taken twice.
299
410
  const candidate = index < oldItems.length && !consumed.has(index) ? oldItems[index] : undefined;
300
411
  const sameItem = candidate !== undefined && looksLikeSameItem(candidate, item) ? candidate : undefined;
301
- return reconcileNode(doc, sameItem, item);
412
+ return reconcileNode(ctx, sameItem, item);
302
413
  });
303
414
  }
304
- /** Apply `data` onto a parsed document, preserving every node the data did not change. */
305
- function applyDataToDocument(doc, data) {
306
- doc.contents = reconcileNode(doc, doc.contents, data);
415
+ /**
416
+ * Re-serialise `data` onto the YAML text `raw`, or undefined when `raw` does not parse or the
417
+ * reconciled document cannot be printed so that it reads back as its own data.
418
+ *
419
+ * The reconciler decides WHAT changes; {@link spliceSource} then writes those changes into the
420
+ * source text, so untouched lines keep the author's own folding and spacing. When it cannot
421
+ * vouch for a splice, the result is the reconciled document's own `toString()`, which is itself
422
+ * kept only if it parses back to the same data: a carried style that `yaml` prints lossily is
423
+ * dropped and the document re-printed, and failing that the caller writes a plain stringify.
424
+ * The worst case is a re-folded file, or one without its comments; never different data.
425
+ * `rootAtColumnZero` refuses a splice whose first line is indented, for frontmatter, whose
426
+ * framing trims the first line's indentation.
427
+ */
428
+ function reconcileYamlSource(raw, data, { rootAtColumnZero = false, quote, } = {}) {
429
+ const doc = parseDocument(raw);
430
+ if (doc.errors.length > 0)
431
+ return undefined;
432
+ const snapshot = snapshotDocument(doc);
433
+ const ctx = { doc, replaced: new WeakMap(), restyled: [], quote };
434
+ doc.contents = reconcileNode(ctx, doc.contents, data);
435
+ let reconciled = withSourceLineEndings(doc.toString(), raw);
436
+ if (!readsBackAs(reconciled, doc)) {
437
+ for (const scalar of ctx.restyled)
438
+ scalar.type = undefined;
439
+ reconciled = withSourceLineEndings(doc.toString(), raw);
440
+ if (!readsBackAs(reconciled, doc))
441
+ return undefined;
442
+ }
443
+ try {
444
+ const spliced = spliceSource(raw, doc, snapshot, ctx.replaced, reconciled);
445
+ if (spliced !== undefined && !(rootAtColumnZero && /^\s*[ \t]\S/.test(spliced))) {
446
+ return spliced;
447
+ }
448
+ log.debug('content-serialize', 'source splice not used; writing as yaml prints it');
449
+ }
450
+ catch (err) {
451
+ log.debug('content-serialize', 'source splice failed; writing as yaml prints it', {
452
+ error: getErrorMessage(err),
453
+ });
454
+ }
455
+ return reconciled;
456
+ }
457
+ function readsBackAs(text, doc) {
458
+ const reparsed = parseDocument(text);
459
+ return reparsed.errors.length === 0 && isDeepStrictEqual(reparsed.toJS(), doc.toJS());
307
460
  }
308
461
  /**
309
462
  * Serialise entry data as a YAML file, carrying the comments of `existingRaw` through.
310
463
  *
311
464
  * Falls back to a plain stringify — byte-identical to serialising without preservation — when
312
465
  * 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.
466
+ * do not parse, or the reconciled document does not read back as its own data). A save must not
467
+ * fail because the previous content was malformed.
314
468
  */
315
469
  export function serializeYaml(data, existingRaw) {
316
470
  if (existingRaw === undefined)
317
471
  return yamlStringify(data);
318
- const doc = parseDocument(existingRaw);
319
- if (doc.errors.length > 0)
320
- return yamlStringify(data);
321
- applyDataToDocument(doc, data);
322
- return doc.toString();
472
+ return reconcileYamlSource(existingRaw, data) ?? yamlStringify(data);
323
473
  }
324
474
  /**
325
475
  * Split a file into its raw frontmatter string, or undefined when there is nothing usable. Two
@@ -348,26 +498,174 @@ function extractRawFrontmatter(raw) {
348
498
  const frontmatter = parsed.matter;
349
499
  if (typeof frontmatter !== 'string' || frontmatter.trim() === '')
350
500
  return undefined;
351
- return frontmatter;
501
+ // gray-matter ends the frontmatter at `\n---`, so a CRLF file's keeps the closing line's `\r`.
502
+ return frontmatter.replace(/\r$/, '');
503
+ }
504
+ /** The body of a file as gray-matter splits it, or undefined when it cannot split it. */
505
+ function extractBody(raw) {
506
+ try {
507
+ return matter(raw, {}).content;
508
+ }
509
+ catch {
510
+ return undefined;
511
+ }
512
+ }
513
+ /**
514
+ * Whether `data` has a value to write. Without one, gray-matter's YAML engine writes no frontmatter
515
+ * block (it skips `{}`); the reconcile engine can still write a comment-only block, under which a
516
+ * flush body is also read back intact.
517
+ */
518
+ function writesFrontmatter(data) {
519
+ return Object.values(data).some((value) => value !== undefined);
520
+ }
521
+ /**
522
+ * The body to write: `body`'s content in `existingRaw`'s text wherever they agree
523
+ * (`preserveMarkdownSource`), opening with the blank line the frontmatter is followed by on disk.
524
+ * gray-matter leaves that blank line in the body, and the editor's body does not carry it, so it
525
+ * is restored here even when the splice falls back. A body under frontmatter the file did not
526
+ * have, or under frontmatter with no body yet, gets one blank line, Prettier's style.
527
+ *
528
+ * With no frontmatter written, the body opens the file: it loses its leading blank lines, except
529
+ * the one newline that stops a body starting with `---` (after any BOM, which gray-matter skips)
530
+ * reading back as frontmatter.
531
+ */
532
+ function bodyToWrite(body, data, format, existingRaw) {
533
+ const priorBody = existingRaw === undefined ? undefined : extractBody(existingRaw);
534
+ const spliced = priorBody === undefined ? body : preserveMarkdownSource(priorBody, body, format);
535
+ if (!writesFrontmatter(data)) {
536
+ const flush = spliced.replace(/^(?:\r?\n)+/, '');
537
+ return /^\uFEFF?---/.test(flush) ? `\n${flush}` : flush;
538
+ }
539
+ if (/^\r?\n/.test(spliced) || !/\S/.test(spliced))
540
+ return spliced;
541
+ const priorKeptBody = priorBody !== undefined &&
542
+ /\S/.test(priorBody) &&
543
+ existingRaw !== undefined &&
544
+ extractRawFrontmatter(existingRaw) !== undefined;
545
+ const lead = priorKeptBody ? (/^(?:\r?\n)*/.exec(priorBody)?.[0] ?? '') : '\n';
546
+ return lead + spliced;
547
+ }
548
+ /** `value` as JSON carries it, or undefined when it cannot be serialised. */
549
+ function asJson(value) {
550
+ const key = identityKey(value);
551
+ return key === undefined || key === 'undefined' ? undefined : JSON.parse(key);
552
+ }
553
+ /** Frontmatter data as the read path hands it over the API, or undefined when it cannot split. */
554
+ function frontmatterAsRead(raw) {
555
+ try {
556
+ return asJson(matter(raw, {}).data);
557
+ }
558
+ catch {
559
+ return undefined;
560
+ }
561
+ }
562
+ /**
563
+ * `value` with every part it carries unchanged from `read` replaced by `disk`'s value for that
564
+ * part. The read path parses frontmatter with gray-matter's js-yaml, which reads YAML 1.1 (a bare
565
+ * date is a `Date`, sent back as a timestamp string; `014` is octal), and the reconciler compares
566
+ * against `yaml`'s YAML 1.2 reading; so a value the editor sent back as it read it would otherwise
567
+ * count as changed and rewrite its line.
568
+ */
569
+ function withUnchangedAsOnDisk(value, read, disk) {
570
+ const sent = identityKey(value);
571
+ if (sent !== undefined && sent === identityKey(read))
572
+ return disk;
573
+ if (isPlainRecord(value) && isPlainRecord(read) && isPlainRecord(disk)) {
574
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
575
+ key,
576
+ withUnchangedAsOnDisk(child, read[key], disk[key]),
577
+ ]));
578
+ }
579
+ if (Array.isArray(value) && Array.isArray(read) && Array.isArray(disk)) {
580
+ if (read.length !== disk.length)
581
+ return value;
582
+ // An item sent as read anywhere in the list, so an insert or a reorder keeps the rest as read.
583
+ // On the save that first writes item keys, it is found by what it was read as without them,
584
+ // and keeps the keys while its unchanged values take their disk form.
585
+ const readKeys = read.map(identityKey);
586
+ const claimed = new Set();
587
+ const claim = (key) => {
588
+ const at = readKeys.findIndex((k, j) => k !== undefined && k === key && !claimed.has(j));
589
+ if (at >= 0)
590
+ claimed.add(at);
591
+ return at;
592
+ };
593
+ const readItemKeys = read.map(itemKeyOf);
594
+ const exact = value.map((child) => claim(identityKey(child)));
595
+ return value.map((child, i) => {
596
+ if (exact[i] >= 0)
597
+ return disk[exact[i]];
598
+ // An edited keyed item compares against the item holding its key, wherever it was read.
599
+ const key = itemKeyOf(child);
600
+ const byKey = key === undefined ? -1 : readItemKeys.findIndex((k, j) => k === key && !claimed.has(j));
601
+ if (byKey >= 0) {
602
+ claimed.add(byKey);
603
+ return withUnchangedAsOnDisk(child, read[byKey], disk[byKey]);
604
+ }
605
+ const unkeyed = unkeyedIdentityKey(child);
606
+ const asRead = unkeyed === undefined ? -1 : claim(unkeyed);
607
+ if (asRead >= 0)
608
+ return withUnchangedAsOnDisk(child, read[asRead], disk[asRead]);
609
+ // By position only when nothing says these are different items.
610
+ const other = i < read.length ? readItemKeys[i] : undefined;
611
+ const differs = key !== undefined && other !== undefined && other !== key;
612
+ return i < read.length && !differs ? withUnchangedAsOnDisk(child, read[i], disk[i]) : child;
613
+ });
614
+ }
615
+ return value;
616
+ }
617
+ /**
618
+ * Whether gray-matter, the read path's reader, reads `text` written plain as anything the API
619
+ * would not carry as that string: as a value, `1:30` is 90 and a bare date a `Date`, while a
620
+ * timestamp the editor's date field writes reads as a date the API carries as the same text, and
621
+ * stays plain; as a key, anything but the key itself.
622
+ */
623
+ function readsAsAnotherValue(text, asKey) {
624
+ if (/[\r\n]/.test(text))
625
+ return false;
626
+ // `matter.stringify` trims the frontmatter it writes, so an edge character `trim` drops is lost.
627
+ if (text !== text.trim())
628
+ return true;
629
+ try {
630
+ const { data } = matter(`---\n${asKey ? `${text}: v` : `v: ${text}`}\n---\n`, {});
631
+ if (!isPlainRecord(data))
632
+ return true;
633
+ const keys = Object.keys(data);
634
+ if (asKey)
635
+ return keys.length !== 1 || keys[0] !== text;
636
+ return identityKey(data.v) !== identityKey(text);
637
+ }
638
+ catch {
639
+ return true;
640
+ }
352
641
  }
353
642
  /**
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.
643
+ * Serialise an md/mdx entry, carrying the comments of `existingRaw`'s frontmatter and the source
644
+ * text of its unchanged body blocks through. The reconciled YAML goes back through
645
+ * `matter.stringify` via a custom stringify engine rather than being spliced between
646
+ * hand-written `---` lines, so delimiters and the trailing newline stay exactly what gray-matter
647
+ * would have produced. The result is kept only if the read path reads `data` back from it;
648
+ * otherwise gray-matter writes it, without the comments. A fresh string the read path would
649
+ * misread is quoted (`readsAsAnotherValue`), so this is left for a changed value meeting text the
650
+ * two parsers read differently, as 14 meets a file's `014`.
358
651
  */
359
- export function serializeFrontmatter(body, data, existingRaw) {
652
+ export function serializeFrontmatter(editorBody, data, existingRaw, format) {
653
+ // An object, never the body string: `matter.stringify` re-parses a string as a whole file, so a
654
+ // body starting with `---` would be read as frontmatter and dropped.
655
+ const file = { content: bodyToWrite(editorBody, data, format, existingRaw) };
360
656
  if (existingRaw === undefined)
361
- return matter.stringify(body, data);
657
+ return matter.stringify(file, data);
362
658
  const existingFrontmatter = extractRawFrontmatter(existingRaw);
363
659
  if (existingFrontmatter === undefined)
364
- return matter.stringify(body, data);
365
- const doc = parseDocument(existingFrontmatter);
366
- if (doc.errors.length > 0)
367
- return matter.stringify(body, data);
368
- applyDataToDocument(doc, data);
369
- const reconciled = doc.toString();
370
- return matter.stringify(body, data, {
660
+ return matter.stringify(file, data);
661
+ const onDisk = parseDocument(existingFrontmatter);
662
+ const toReconcile = onDisk.errors.length === 0
663
+ ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw), onDisk.toJS())
664
+ : data;
665
+ const reconciled = reconcileYamlSource(existingFrontmatter, isPlainRecord(toReconcile) ? toReconcile : data, { rootAtColumnZero: true, quote: readsAsAnotherValue });
666
+ if (reconciled === undefined)
667
+ return matter.stringify(file, data);
668
+ const written = matter.stringify(file, data, {
371
669
  engines: {
372
670
  yaml: {
373
671
  parse: () => ({}),
@@ -375,4 +673,8 @@ export function serializeFrontmatter(body, data, existingRaw) {
375
673
  },
376
674
  },
377
675
  });
676
+ if (isDeepStrictEqual(frontmatterAsRead(written), asJson(data)))
677
+ return written;
678
+ log.debug('content-serialize', 'reconciled frontmatter reads back differently; writing as gray-matter prints it');
679
+ return matter.stringify(file, data);
378
680
  }
@@ -44,7 +44,7 @@ import { type OnLockCompromised } from './provisioning-lock.js';
44
44
  * `next dev` and build-time provisioning across worker processes. The case to watch is a write
45
45
  * whose in-lock path triggers a full `idIndex()` rescan of a large tree over EFS, which can exceed
46
46
  * this budget and start 409ing unrelated saves. Making it configurable is tracked in
47
- * .claude/future-tasks/content-write-lock-tuning-and-granularity.md.
47
+ * safeinsights/canopycms#640.
48
48
  */
49
49
  export declare const DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2000;
50
50
  /**
@@ -75,6 +75,11 @@ export declare class ContentWriteLockBusyError extends Error {
75
75
  * skips the branch and retries next cycle. Throws with `code === 'ELOCKED'` on a live holder.
76
76
  */
77
77
  export declare function tryAcquireContentWriteLock(branchRoot: string, onCompromised?: OnLockCompromised): Promise<() => Promise<void>>;
78
+ /**
79
+ * Throw unless the calling async chain runs inside {@link withContentWriteLock} for `branchRoot`:
80
+ * the check for code that writes under a lock its caller took.
81
+ */
82
+ export declare function assertContentWriteLockHeld(branchRoot: string): void;
78
83
  /** Run `fn` holding the branch's content-write lock, releasing in a `finally` so a throw cannot
79
84
  * strand it. */
80
85
  export declare function withContentWriteLock<T>(branchRoot: string, fn: () => Promise<T>, waitMs?: number): Promise<T>;