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
@@ -17,21 +17,27 @@ import matter from 'gray-matter';
17
17
  import { parse as yamlParse } from 'yaml';
18
18
  import { atomicWriteFile } from './utils/atomic-write.js';
19
19
  import { serializeFrontmatter, serializeYaml } from './utils/content-serialize.js';
20
+ import { serializeJson } from './utils/json-source-splice.js';
20
21
  import { withLock } from './utils/async-mutex.js';
21
- import { ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
22
+ import { assertContentWriteLockHeld, ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
22
23
  import { findBodyFieldName } from './utils/body-field.js';
23
- import { buildResolvedReference, buildRestrictedReference } from './entry-schema.js';
24
+ import { withItemKeys } from './item-keys.js';
25
+ import { buildMissingReference, buildResolvedReference, buildRestrictedReference, } from './entry-schema.js';
26
+ import { warnDanglingReference } from './dangling-reference-log.js';
27
+ import { canopyLogError } from './utils/logger.js';
28
+ import { SchemaUnavailableError } from './schema/schema-unavailable-error.js';
24
29
  import { resolveEntryTitle } from './utils/title-field.js';
25
30
  import { computeEntryUrl } from './utils/entry-url.js';
26
31
  import { findUrlPathClaimant } from './url-collision.js';
27
32
  import { ContentIdIndex, extractIdFromFilename, extractSlugFromFilename, extractEntryTypeFromFilename, resolveCollectionPath, } from './content-id-index.js';
28
33
  import { registerContentIndexForInvalidation } from './content-index-registry.js';
34
+ import { isPlainRecord } from './validation/field-traversal.js';
29
35
  import { bumpContentIndexGeneration, readContentIndexGeneration } from './content-index-generation.js';
30
36
  import { generateId } from './id.js';
31
37
  import { isNodeError } from './utils/error.js';
32
- import { filePathExists, readFileIfExists } from './utils/fs.js';
38
+ import { filePathExists, readFileWithMtimeIfExists } from './utils/fs.js';
33
39
  import { asRecord, getFormatExtension } from './utils/format.js';
34
- import { entryLogicalPath, normalizeFilesystemPath, parseSlug, } from './paths/index.js';
40
+ import { entryLogicalPath, normalizeFilesystemPath, parseSlug, validateAndNormalizePath, } from './paths/index.js';
35
41
  /**
36
42
  * Acquire `keys` in canonical (sorted) order before running `fn`, ruling out AB-BA
37
43
  * deadlocks between callers with overlapping key sets. Only renameEntry() needs two
@@ -60,6 +66,22 @@ export class ContentConflictError extends Error {
60
66
  this.name = 'ContentConflictError';
61
67
  }
62
68
  }
69
+ /**
70
+ * Thrown when a write carrying a numeric `expectedVersion` finds no file: the entry was deleted
71
+ * or moved since that version was read. A missing file is a change since the read, so the write
72
+ * is refused rather than re-creating the entry; re-creating it is a create (`expectedVersion:
73
+ * null`) the caller makes deliberately.
74
+ *
75
+ * A `ContentConflictError` subclass so every 409 mapping keeps working; the distinct type lets
76
+ * the API tell the editor the entry is gone, where "reload to see the latest version" has
77
+ * nothing to reload.
78
+ */
79
+ export class EntryGoneError extends ContentConflictError {
80
+ constructor() {
81
+ super('This entry was deleted or moved since it was read');
82
+ this.name = 'EntryGoneError';
83
+ }
84
+ }
63
85
  /**
64
86
  * [SYNC-C1] Thrown when a mutation could not take the branch's cross-host
65
87
  * content-write lock within its bounded wait (utils/content-write-lock.ts) --
@@ -71,8 +93,11 @@ export class ContentConflictError extends Error {
71
93
  * instead of the misleading generic "modified by another editor".
72
94
  */
73
95
  export class BranchSyncingError extends ContentConflictError {
74
- constructor(message) {
96
+ constructor(message,
97
+ /** As on `ContentWriteLockBusyError`: `'unknown'` means the write ran and may have landed. */
98
+ outcome = 'not-run') {
75
99
  super(message);
100
+ this.outcome = outcome;
76
101
  this.name = 'BranchSyncingError';
77
102
  }
78
103
  }
@@ -94,15 +119,13 @@ export class DuplicateContentIdError extends ContentConflictError {
94
119
  constructor(contentId, paths) {
95
120
  const sorted = Array.from(new Set(paths)).sort();
96
121
  super(
97
- // Names the STATE, not an action: the repair-content-duplicates endpoint
98
- // exists but nothing in the editor renders it, and the admin panel's
99
- // duplicate list (SystemHealthPanel) is the diagnosis half only. The repair
100
- // UI is tracked in .claude/future-tasks/duplicate-content-id-repair-ui.md.
122
+ // Names where an administrator fixes it, not the endpoint: the editor
123
+ // reading this cannot run an admin action.
101
124
  `Content ID ${contentId} is on more than one file (${sorted
102
125
  .map((p) => `"${p}"`)
103
126
  .join(' and ')}), so this save was refused rather than risk overwriting or ` +
104
- `deleting the wrong one. An administrator needs to resolve the duplicate on the ` +
105
- `server before this entry can be saved.`);
127
+ `deleting the wrong one. An administrator can fix this in System health, on the ` +
128
+ `Branches tab; until then this entry cannot be saved.`);
106
129
  this.name = 'DuplicateContentIdError';
107
130
  this.contentId = contentId;
108
131
  this.paths = sorted;
@@ -151,6 +174,54 @@ function validateSlug(slug) {
151
174
  throw new ContentStoreError('Slugs cannot contain backslashes. Use nested collections instead.', 'VALIDATION');
152
175
  }
153
176
  }
177
+ const isEmptyFieldValue = (value) => value === undefined || value === null || value === '';
178
+ /**
179
+ * A parsed entry document's top-level record. Anything but a plain object throws, so a fill never
180
+ * replaces an array or scalar root; YAML reads an empty document as null.
181
+ */
182
+ function entryRecord(parsed, emptyIsRecord) {
183
+ if (isPlainRecord(parsed))
184
+ return parsed;
185
+ if (emptyIsRecord && (parsed === null || parsed === undefined))
186
+ return {};
187
+ throw new Error('the file does not hold an object at its top level');
188
+ }
189
+ /**
190
+ * `raw` with top-level `field` set to the string `value`, through the same source-preserving
191
+ * serialisers `write()` uses, or undefined when `raw` already holds a non-empty value there.
192
+ * Throws when the result would not read back as that string.
193
+ */
194
+ function fillEmptyTopLevelField(raw, format, field, value) {
195
+ if (format === 'json' || format === 'yaml') {
196
+ const parse = (text) => format === 'json' ? entryRecord(JSON.parse(text), false) : entryRecord(yamlParse(text), true);
197
+ const data = parse(raw);
198
+ if (!isEmptyFieldValue(data[field]))
199
+ return undefined;
200
+ const next = { ...data, [field]: value };
201
+ const content = format === 'json' ? serializeJson(next, raw) : serializeYaml(next, raw);
202
+ if (parse(content)[field] !== value)
203
+ throw new Error(`"${field}" does not read back as written`);
204
+ return content;
205
+ }
206
+ const frontmatter = (text) => entryRecord(matter(text, {}).data, true);
207
+ const fill = (from, fieldValue) => {
208
+ const parsed = matter(from, {});
209
+ const data = { ...entryRecord(parsed.data, true), [field]: fieldValue };
210
+ return serializeFrontmatter(parsed.content, data, from, format);
211
+ };
212
+ const readsBack = (content) => frontmatter(content)[field] === value;
213
+ if (!isEmptyFieldValue(frontmatter(raw)[field]))
214
+ return undefined;
215
+ const content = fill(raw, value);
216
+ if (readsBack(content))
217
+ return content;
218
+ // A key the serialiser inserts is written plain, and gray-matter reads a plain timestamp as a
219
+ // Date. Inserted as '' it is written quoted, and replacing it keeps that quoting.
220
+ const quoted = fill(fill(raw, ''), value);
221
+ if (readsBack(quoted))
222
+ return quoted;
223
+ throw new Error(`"${field}" does not read back as written`);
224
+ }
154
225
  const DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1000;
155
226
  /**
156
227
  * At most one suspicious-lookup forced rescan per store per this window, so
@@ -159,6 +230,8 @@ const DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1000;
159
230
  const FORCED_REFRESH_MIN_INTERVAL_MS = 5000;
160
231
  /** Internal sentinel: a lookup result that suggests this store's index is stale. */
161
232
  const STALE_LOOKUP = Symbol('stale-index-lookup');
233
+ /** A resolver's own `MissingReference`; a full target never carries `unavailable`. */
234
+ const isMissingReference = (value) => value.unavailable === true && value.reason === 'missing';
162
235
  /** Create a cache for one batch of reference resolution. See {@link ReferenceResolveCache}. */
163
236
  export const createReferenceResolveCache = () => new Map();
164
237
  export class ContentStore {
@@ -208,7 +281,7 @@ export class ContentStore {
208
281
  // Translate at the boundary, after the bounded wait -- never inside the
209
282
  // acquire loop, which would disable its ELOCKED retry predicate.
210
283
  if (err instanceof ContentWriteLockBusyError)
211
- throw new BranchSyncingError(err.message);
284
+ throw new BranchSyncingError(err.message, err.outcome);
212
285
  throw err;
213
286
  }
214
287
  }
@@ -604,6 +677,8 @@ export class ContentStore {
604
677
  absolutePath: resolved,
605
678
  relativePath: path.relative(this.root, resolved),
606
679
  logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
680
+ collectionPath: schemaItem.logicalPath,
681
+ slug: safeSlug,
607
682
  id,
608
683
  entryTypeName: finalEntryTypeName,
609
684
  existed,
@@ -640,7 +715,11 @@ export class ContentStore {
640
715
  */
641
716
  async read(collectionPath, slug = '', options = {}) {
642
717
  const schemaItem = this.assertSchemaItem(collectionPath);
643
- const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName, } = await this.buildPaths(schemaItem, slug);
718
+ const { absolutePath, relativePath, existed, entryTypeName: resolvedEntryTypeName, } = await this.buildPaths(schemaItem, slug);
719
+ // A missing entry stays a not-found below, whatever its collection's default type.
720
+ if (existed && !options.allowUnavailableEntryType) {
721
+ this.assertEntryTypeAvailable(schemaItem, resolvedEntryTypeName);
722
+ }
644
723
  // stat BEFORE readFile: a conservative version token can only produce false-positive
645
724
  // conflicts. A write landing between stat and readFile gives the client newer content with
646
725
  // an older token, so their next save 409s (safe); stat-after or a parallel stat+read risks
@@ -707,8 +786,14 @@ export class ContentStore {
707
786
  absolutePath,
708
787
  };
709
788
  }
789
+ // Keys an item saved without one by its content, before references resolve into it, as the
790
+ // listing and the write path do (`item-keys.ts`).
791
+ doc.data = withItemKeys(fields, doc.data, 'content');
710
792
  if (options.resolveReferences !== false) {
711
- doc.data = await this.resolveReferencesInData(doc.data, fields, undefined, options.referenceAccess);
793
+ doc.data = await this.resolveReferencesInData(doc.data, fields, undefined, options.referenceAccess, {
794
+ entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
795
+ prefix: '',
796
+ });
712
797
  }
713
798
  doc.version = stat.mtimeMs;
714
799
  return doc;
@@ -747,6 +832,8 @@ export class ContentStore {
747
832
  // concurrent renameEntry() completes, so it must be re-resolved as ground truth once the
748
833
  // lock is held (see entryLockKey()). The format check above still runs unlocked.
749
834
  const prePass = await this.buildPaths(schemaItem, slug, { entryTypeName, existingId });
835
+ // The type the entry is written as: its on-disk one when it exists, else the requested/default.
836
+ this.assertEntryTypeAvailable(schemaItem, prePass.entryTypeName);
750
837
  let lockKey = this.entryLockKey(schemaItem, slug, prePass);
751
838
  // Reclassification loop: the world can change between the pre-pass and lock acquisition (a
752
839
  // concurrent renameEntry() freeing this slug flips existing->new; a concurrent create
@@ -814,6 +901,33 @@ export class ContentStore {
814
901
  }
815
902
  }
816
903
  }
904
+ // The target as it is now, from one open: its mtime is the OCC version checked below,
905
+ // and its bytes are what the comment-preserving serialisation further down re-applies
906
+ // the payload onto, so the bytes kept are the bytes OCC validated. One open rather
907
+ // than a path `stat` plus a later read because opening revalidates against the NFS
908
+ // server, where a `stat` can answer from this host's cache for a file another host has
909
+ // since replaced or deleted (utils/fs.ts, docs/concurrency.md). Inside withLock(lockKey) and
910
+ // withContentWriteExclusion ([SYNC-C1]), so no writer or rebase moves it after this.
911
+ const current = await readFileWithMtimeIfExists(absolutePath);
912
+ // OCC: undefined means no opinion (skip entirely, a blind write). A number means
913
+ // "must match this mtime", and a missing file fails it too: the entry was deleted or
914
+ // moved since the read, and writing would re-create it (`EntryGoneError`). `null`
915
+ // means "must NOT exist yet" — the create-intent guard, without which a create
916
+ // request against a slug that already has content falls through to an ordinary
917
+ // blind overwrite. This is the authoritative check, so it holds even if a caller's
918
+ // own pre-write existence check (e.g. the API layer's `documentExists`) went stale
919
+ // under concurrency. It runs before the create-only guards below and before `mkdir`,
920
+ // so a refused update neither re-creates a deleted collection directory nor reports
921
+ // a create-only failure for a write that was never a create.
922
+ if (input.expectedVersion === null && current) {
923
+ throw new ContentConflictError('An entry with this slug already exists');
924
+ }
925
+ if (typeof input.expectedVersion === 'number') {
926
+ if (!current)
927
+ throw new EntryGoneError();
928
+ if (current.mtimeMs !== input.expectedVersion)
929
+ throw new ContentConflictError();
930
+ }
817
931
  // [URL] Contested-URL guard, create only: an ordinary save cannot contest a URL it
818
932
  // already holds, and blocking it would trap the author in an entry they can no
819
933
  // longer edit.
@@ -831,7 +945,7 @@ export class ContentStore {
831
945
  // Serialization comes from `withContentWriteExclusion` (the branch-wide, cross-host
832
946
  // lock wrapping this whole loop), NOT the per-entry lock: the two halves of a
833
947
  // contested pair live in DIFFERENT collections and take different entry-lock keys.
834
- if (!(await filePathExists(absolutePath))) {
948
+ if (!current) {
835
949
  const chosenSlug = extractSlugFromFilename(path.basename(absolutePath));
836
950
  // [SLUG] Routability guard, create only. `validateSlug` (in buildPaths) is the
837
951
  // path-traversal check and rejects only `/` and `\`, and has to stay that weak
@@ -857,36 +971,6 @@ export class ContentStore {
857
971
  await this.assertUrlPathAvailable(path.dirname(absolutePath), chosenSlug);
858
972
  }
859
973
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
860
- // OCC: undefined means no opinion (skip entirely, a blind write). A number means
861
- // "must match this mtime" (stale-write rejection). `null` means "must NOT exist
862
- // yet" — the create-intent guard, without which a create request against a slug
863
- // that already has content falls through to an ordinary blind overwrite. This is
864
- // the authoritative check: it runs inside the per-entry lock against a fresh stat,
865
- // so it holds even if a caller's own pre-write existence check (e.g. the API
866
- // layer's `documentExists`) went stale under concurrency.
867
- if (input.expectedVersion !== undefined) {
868
- try {
869
- const existing = await fs.stat(absolutePath);
870
- if (input.expectedVersion === null) {
871
- throw new ContentConflictError('An entry with this slug already exists');
872
- }
873
- if (existing.mtimeMs !== input.expectedVersion) {
874
- throw new ContentConflictError();
875
- }
876
- }
877
- catch (err) {
878
- if (err instanceof ContentConflictError)
879
- throw err;
880
- if (isNodeError(err) && err.code === 'ENOENT') {
881
- // No file yet: for a numeric expectedVersion this is the "first write,
882
- // skip the version check" back-compat behavior; for null it is create-only
883
- // correctly seeing no collision. Either way, proceed with the write.
884
- }
885
- else {
886
- throw err;
887
- }
888
- }
889
- }
890
974
  // Existence guard (cross-process): the caller asserts this entry exists
891
975
  // (existingId), so a missing file at the path about to be written means this store's
892
976
  // index may be stale -- another process may have renamed the entry. Recreating a
@@ -895,8 +979,9 @@ export class ContentStore {
895
979
  // this host: if the ID's actual on-disk location differs from what the index
896
980
  // believes, fail with a conflict so the caller reloads fresh state. (An intentional
897
981
  // slug-change save passes: index and directory agree on the entry's current,
898
- // old-slug path. External deletes also pass: the ID is nowhere on disk, so
899
- // recreating is last-writer-wins.)
982
+ // old-slug path. An external delete passes here, since the ID is nowhere on disk; a
983
+ // write carrying a version has already been refused by the OCC check above, so only
984
+ // a blind write re-creates it.)
900
985
  //
901
986
  // indexedRelPath reads the LIVE index synchronously — NOT idIndex(), which would
902
987
  // run a full rescan while holding the entry lock if invalidateIndex() fired since
@@ -922,32 +1007,31 @@ export class ContentStore {
922
1007
  }
923
1008
  // Comment preservation: re-serialise onto the file's OWN parsed document rather
924
1009
  // than a fresh one, so nodes the payload did not change -- and their attached
925
- // comments -- survive the write. JSON has no comment syntax and skips the read. See
926
- // utils/content-serialize.ts.
1010
+ // comments -- survive the write. JSON has no comments; it reads the file to keep
1011
+ // untouched lines' formatting. See utils/content-serialize.ts and
1012
+ // utils/json-source-splice.ts.
927
1013
  //
928
- // This makes the write a genuine read-modify-write of the content file, so WHERE
929
- // the read happens matters: inside withLock(lockKey), inside
930
- // withContentWriteExclusion ([SYNC-C1]), and after the expectedVersion stat -- so
931
- // the bytes read here are the bytes that OCC check validated, and no rebase can be
932
- // running against this branch. Do not hoist it out of the critical section.
1014
+ // This makes the write a genuine read-modify-write of the content file. The bytes are
1015
+ // `current`'s, read in the same open the OCC check took its version from, inside
1016
+ // both locks; do not replace them with a read outside the critical section.
933
1017
  //
934
- // INVARIANT, and the one case that does NOT preserve comments: this reads the path
1018
+ // INVARIANT, and the one case that does NOT preserve comments: `current` is the path
935
1019
  // being WRITTEN. A relocating write -- the ID resolves to a different relativePath,
936
1020
  // so the block below unlinks `staleOldAbsPath` -- finds nothing at the new path and
937
1021
  // falls back to a plain stringify, losing the old file's comments. Unreachable
938
1022
  // today: the editor renames through renameEntry(), which link()s the bytes across
939
1023
  // intact, and no caller passes `existingId`. If a relocating write becomes
940
- // reachable, read the ID's current path here instead of `absolutePath`.
941
- const existingRaw = input.format === 'json' ? undefined : await readFileIfExists(absolutePath);
1024
+ // reachable, read the ID's current path instead of `absolutePath`.
1025
+ const existingRaw = current?.content;
942
1026
  let content;
943
1027
  if (input.format === 'json') {
944
- content = `${JSON.stringify(input.data ?? {}, null, 2)}\n`;
1028
+ content = serializeJson(input.data ?? {}, existingRaw);
945
1029
  }
946
1030
  else if (input.format === 'yaml') {
947
1031
  content = serializeYaml(input.data ?? {}, existingRaw);
948
1032
  }
949
1033
  else {
950
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw);
1034
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
951
1035
  }
952
1036
  await atomicWriteFile(absolutePath, content);
953
1037
  // Update the ID index after a successful write. Look up and mutate the LIVE index
@@ -1015,6 +1099,34 @@ export class ContentStore {
1015
1099
  throw new ContentConflictError();
1016
1100
  });
1017
1101
  }
1102
+ /**
1103
+ * Set top-level `field` of the entry file at `physicalPath` to the string `value` when the
1104
+ * file holds it empty (absent, null or ''), keeping every other byte. Resolves to the file's
1105
+ * version (the mtime `read()` reports) before and after the write, or undefined when it left
1106
+ * the file alone.
1107
+ *
1108
+ * PRECONDITION: the caller already holds this branch's content-write lock, which makes the
1109
+ * read and the write one critical section; a call outside it throws. This takes no lock
1110
+ * itself, since a nested acquisition fails busy and is why nothing holding that lock may call
1111
+ * `write()` (docs/concurrency.md, "Lock acquisition order").
1112
+ */
1113
+ async fillEmptyFieldWhileHoldingWriteLock(physicalPath, format, field, value) {
1114
+ assertContentWriteLockHeld(this.root);
1115
+ const checked = validateAndNormalizePath(this.root, path.join(this.root, physicalPath));
1116
+ if (!checked.valid || !checked.normalizedPath) {
1117
+ throw new ContentStoreError(checked.error ?? 'Path traversal detected', 'VALIDATION');
1118
+ }
1119
+ const absolutePath = path.join(this.root, checked.normalizedPath);
1120
+ const current = await readFileWithMtimeIfExists(absolutePath);
1121
+ if (!current)
1122
+ throw new EntryGoneError();
1123
+ const content = fillEmptyTopLevelField(current.content, format, field, value);
1124
+ if (content === undefined)
1125
+ return undefined;
1126
+ await atomicWriteFile(absolutePath, content);
1127
+ await this.recordOwnMutation(this._idIndex);
1128
+ return { previousVersion: current.mtimeMs, version: (await fs.stat(absolutePath)).mtimeMs };
1129
+ }
1018
1130
  /**
1019
1131
  * Read an entry by its ID (UUID).
1020
1132
  * Returns null if the ID doesn't exist or points to a collection.
@@ -1124,6 +1236,8 @@ export class ContentStore {
1124
1236
  // Pre-pass: classify existing-vs-already-gone by directory scan (local ground truth), not
1125
1237
  // this._idIndex, which can be stale in exactly the way this locking scheme guards against.
1126
1238
  const prePass = await this.buildPaths(collection, slug);
1239
+ if (prePass.existed)
1240
+ this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
1127
1241
  if (!prePass.existed) {
1128
1242
  // Nothing on disk for this slug -- no shared resource to lock on, and fs.unlink on the
1129
1243
  // freshly generated (necessarily nonexistent) path throws ENOENT, as callers expect.
@@ -1206,6 +1320,7 @@ export class ContentStore {
1206
1320
  if (!prePass.existed) {
1207
1321
  throw new ContentStoreError(`Entry not found: ${currentSlug}`, 'NOT_FOUND');
1208
1322
  }
1323
+ this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
1209
1324
  let sourceLockKey = this.entryLockKey(collection, currentSlug, prePass);
1210
1325
  let sourceId = prePass.id;
1211
1326
  // Also lock the destination create-key. Content ID is rename-invariant, so the SOURCE side
@@ -1389,40 +1504,56 @@ export class ContentStore {
1389
1504
  * unmemoized behavior.
1390
1505
  *
1391
1506
  * `access` applies to every target exactly as in `read()`; see {@link ReferenceTargetAccess}.
1507
+ * `origin`, the referring entry's logical path, names it in the dangling-reference warning.
1392
1508
  */
1393
- async resolveReferences(data, fields, cache, access) {
1394
- return this.resolveReferencesInData(data, fields, cache, access);
1509
+ async resolveReferences(data, fields, cache, access, origin) {
1510
+ return this.resolveReferencesInData(data, fields, cache, access, { entry: origin, prefix: '' });
1395
1511
  }
1396
1512
  /**
1397
- * Resolve one reference id the way a reference field resolves it, for the editor's
1398
- * live-preview endpoint (api/resolve-references.ts), which has ids but no field to walk. Null
1399
- * when the id names no entry. No body is embedded, since there is no field to ask for one.
1513
+ * Resolve one reference id the way a reference field resolves it, for callers with ids but no
1514
+ * field to walk: the editor's live-preview endpoint (api/resolve-references.ts) and the AI
1515
+ * export (ai/references.ts). An id
1516
+ * naming no readable entry resolves to a `MissingReference`, as in `read()`. No body is
1517
+ * embedded, since there is no field to ask for one.
1400
1518
  */
1401
1519
  async resolveReferenceTarget(id, access) {
1402
1520
  return this.resolveSingleReference(id, await this.idIndex(), false, undefined, access);
1403
1521
  }
1404
- async resolveReferencesInData(data, fields, cache, access) {
1522
+ /**
1523
+ * `trail` names where `data` sits, for the dangling-reference warning: the referring entry and
1524
+ * the field path so far, in `traverseFields`' format (`blocks[2].author`, `reviewers[1]`).
1525
+ */
1526
+ async resolveReferencesInData(data, fields, cache, access, trail) {
1405
1527
  const resolved = { ...data };
1406
1528
  const idIndex = await this.idIndex();
1529
+ const at = (name) => (trail.prefix ? `${trail.prefix}.${name}` : name);
1530
+ const resolveAt = async (id, includeBody, fieldPath) => {
1531
+ const value = await this.resolveSingleReference(id, idIndex, includeBody, cache, access);
1532
+ if (isMissingReference(value)) {
1533
+ warnDanglingReference({ root: this.root, entry: trail.entry, path: fieldPath, id });
1534
+ }
1535
+ return value;
1536
+ };
1407
1537
  for (const field of fields) {
1408
1538
  // Inline groups are transparent — recurse into their children at the same data level
1409
1539
  if (field.type === 'group') {
1410
- const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access);
1540
+ const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access, trail);
1411
1541
  Object.assign(resolved, groupResolved);
1412
1542
  continue;
1413
1543
  }
1414
1544
  const value = data[field.name];
1545
+ const fieldPath = at(field.name);
1415
1546
  if (field.type === 'reference') {
1416
1547
  // Whether this reference EMBEDS its target (wants the target's body) or merely LINKS
1417
1548
  // to it is a property of the field, declared once in the schema -- not of the call,
1418
1549
  // which routinely contains both kinds at once. See ReferenceFieldConfig.includeBody.
1419
1550
  const includeBody = field.includeBody === true;
1420
1551
  if (typeof value === 'string' && value) {
1421
- resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache, access);
1552
+ resolved[field.name] = await resolveAt(value, includeBody, fieldPath);
1422
1553
  }
1423
1554
  else if (field.list && Array.isArray(value)) {
1424
- resolved[field.name] = await Promise.all(value.map((id) => typeof id === 'string'
1425
- ? this.resolveSingleReference(id, idIndex, includeBody, cache, access)
1555
+ resolved[field.name] = await Promise.all(value.map((id, index) => typeof id === 'string' && id
1556
+ ? resolveAt(id, includeBody, `${fieldPath}[${index}]`)
1426
1557
  : null));
1427
1558
  }
1428
1559
  }
@@ -1431,26 +1562,30 @@ export class ContentStore {
1431
1562
  if (!objectField.fields)
1432
1563
  continue;
1433
1564
  if (objectField.list && Array.isArray(value)) {
1434
- resolved[field.name] = await Promise.all(value.map((item) => typeof item === 'object' && item !== null
1435
- ? this.resolveReferencesInData(item, objectField.fields, cache, access)
1565
+ resolved[field.name] = await Promise.all(value.map((item, index) => isPlainRecord(item)
1566
+ ? this.resolveReferencesInData(item, objectField.fields, cache, access, {
1567
+ entry: trail.entry,
1568
+ prefix: `${fieldPath}[${index}]`,
1569
+ })
1436
1570
  : item));
1437
1571
  }
1438
- else if (typeof value === 'object') {
1439
- resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access);
1572
+ else if (isPlainRecord(value)) {
1573
+ resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access, { entry: trail.entry, prefix: fieldPath });
1440
1574
  }
1441
1575
  }
1442
1576
  else if (field.type === 'block' && Array.isArray(value)) {
1443
1577
  const blockField = field;
1444
- resolved[field.name] = await Promise.all(value.map(async (block) => {
1445
- const b = block;
1446
- if (!b || typeof b.value !== 'object')
1578
+ resolved[field.name] = await Promise.all(value.map(async (block, index) => {
1579
+ // An item or value that is not a record (null, an array, a scalar) passes through
1580
+ // untouched: it has no named fields, and reshaping it here would be saved to the file.
1581
+ if (!isPlainRecord(block) || !isPlainRecord(block.value))
1447
1582
  return block;
1448
- const template = blockField.templates.find((t) => t.name === b.template);
1583
+ const template = blockField.templates.find((t) => t.name === block.template);
1449
1584
  if (!template)
1450
1585
  return block;
1451
1586
  return {
1452
- ...b,
1453
- value: await this.resolveReferencesInData(b.value, template.fields, cache, access),
1587
+ ...block,
1588
+ value: await this.resolveReferencesInData(block.value, template.fields, cache, access, { entry: trail.entry, prefix: `${fieldPath}[${index}]` }),
1454
1589
  };
1455
1590
  }));
1456
1591
  }
@@ -1471,7 +1606,7 @@ export class ContentStore {
1471
1606
  * `data` from a process-global content-keyed cache, so `resolveSingleReferenceOnce`'s
1472
1607
  * top-level spread severs exactly one level and NESTED frontmatter objects alias across
1473
1608
  * occurrences, calls and requests. Pre-existing `read()` behavior, deliberately out of scope
1474
- * here; see `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
1609
+ * here; see safeinsights/canopycms#708.
1475
1610
  *
1476
1611
  * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
1477
1612
  * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
@@ -1499,10 +1634,11 @@ export class ContentStore {
1499
1634
  // The cached promise always has this handler attached, so it is never an unhandled
1500
1635
  // rejection; entry data is plain parsed JSON/YAML/frontmatter and so always cloneable (a
1501
1636
  // `!!binary` Buffer is the sole shape not preserved exactly: it clones to a Uint8Array).
1502
- return pending.then((resolved) => (resolved === null ? null : structuredClone(resolved)));
1637
+ return pending.then((resolved) => structuredClone(resolved));
1503
1638
  }
1504
1639
  /**
1505
- * Returns null if the reference is invalid or missing. Includes id, slug and collection.
1640
+ * The target's data with its reference metadata, a `RestrictedReference` for a denied target,
1641
+ * or a `MissingReference` when the id names no readable entry.
1506
1642
  *
1507
1643
  * A suspicious result (ID missing from the index, or an index hit whose file is gone)
1508
1644
  * triggers one forced index refresh and a retry — self-healing for mutations by other
@@ -1523,7 +1659,32 @@ export class ContentStore {
1523
1659
  // dedupes in-flight builds), so every miss in a Promise.all batch heals.
1524
1660
  await this.refreshIndexForSuspiciousLookup();
1525
1661
  const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody, access);
1526
- return second === STALE_LOOKUP ? null : second;
1662
+ return second === STALE_LOOKUP ? buildMissingReference(id) : second;
1663
+ }
1664
+ /**
1665
+ * Throw {@link SchemaUnavailableError} when an entry exists at `slug` and is of an
1666
+ * `unavailable` entry type, for a handler to refuse before any other check answers first.
1667
+ */
1668
+ async assertEntryAvailable(collectionPath, slug = '') {
1669
+ const schemaItem = this.assertSchemaItem(collectionPath);
1670
+ const { existed, entryTypeName } = await this.buildPaths(schemaItem, slug);
1671
+ if (existed)
1672
+ this.assertEntryTypeAvailable(schemaItem, entryTypeName);
1673
+ }
1674
+ /**
1675
+ * Throw {@link SchemaUnavailableError} when the entry type an entry is read or written as --
1676
+ * `entryTypeName`, else the collection's default -- is `unavailable`. A name the collection
1677
+ * does not declare is never charged to the default type, which it was not written as.
1678
+ */
1679
+ assertEntryTypeAvailable(schemaItem, entryTypeName) {
1680
+ const entryType = schemaItem.type === 'entry-type'
1681
+ ? schemaItem
1682
+ : entryTypeName
1683
+ ? schemaItem.entries?.find((e) => e.name === entryTypeName)
1684
+ : getDefaultEntryType(schemaItem.entries);
1685
+ if (entryType?.unavailable) {
1686
+ throw new SchemaUnavailableError(entryType.name, entryType.unavailable);
1687
+ }
1527
1688
  }
1528
1689
  /**
1529
1690
  * The entry type an entry file was written as, by its filename's type token, falling back to
@@ -1543,11 +1704,12 @@ export class ContentStore {
1543
1704
  if (!location)
1544
1705
  return STALE_LOOKUP;
1545
1706
  if (location.type !== 'entry' || !location.collection || !location.slug) {
1546
- return null;
1707
+ return buildMissingReference(id);
1547
1708
  }
1548
1709
  // Read the referenced entry WITHOUT resolving its references (prevent infinite loops)
1549
1710
  const doc = await this.read(location.collection, location.slug, {
1550
1711
  resolveReferences: false,
1712
+ allowUnavailableEntryType: true,
1551
1713
  });
1552
1714
  // `urlPath` is what makes a resolved reference linkable without a second listing pass to
1553
1715
  // build a contentId -> url table. Deliberately `computeEntryUrl` (utils/entry-url.ts), the
@@ -1585,8 +1747,9 @@ export class ContentStore {
1585
1747
  // rename/delete this store hasn't observed yet.
1586
1748
  if (isNodeError(error) && error.code === 'ENOENT')
1587
1749
  return STALE_LOOKUP;
1588
- console.error(`Failed to resolve reference ${id}:`, error);
1589
- return null;
1750
+ // Still the id, so a save of the referring entry keeps it.
1751
+ canopyLogError(`Failed to resolve reference ${id}:`, error);
1752
+ return buildMissingReference(id);
1590
1753
  }
1591
1754
  }
1592
1755
  }
@@ -6,7 +6,7 @@
6
6
  * metadata); display concerns like labels are left to the adopter's `extract` callback.
7
7
  */
8
8
  import { isIndexSlug } from './utils/entry-url.js';
9
- import { createReferenceResolver, listCollectionEntries, resolveCollectionItemReferences, sortByOrder, } from './content-listing.js';
9
+ import { createReferenceResolver, listCollectionEntries, withoutUnavailableEntries, resolveCollectionItemReferences, sortByOrder, } from './content-listing.js';
10
10
  /** Group flat schema items by parentPath for O(1) child lookup. */
11
11
  const groupByParent = (flat) => {
12
12
  const map = new Map();
@@ -73,7 +73,7 @@ export async function buildContentTree(branchRoot, flatSchema, contentRootName,
73
73
  ? createReferenceResolver(branchRoot, flatSchema, contentRootName, visibility)
74
74
  : null;
75
75
  const listVisibleEntries = async (collection) => {
76
- const entries = await listCollectionEntries(branchRoot, collection);
76
+ const entries = withoutUnavailableEntries(await listCollectionEntries(branchRoot, collection), collection);
77
77
  const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
78
78
  // Resolved here rather than at the two node-building sites so BOTH inherit it — the same
79
79
  // reason the ACL filter lives here. A denied entry is filtered above and never resolved.
package/dist/context.js CHANGED
@@ -7,6 +7,8 @@ import { loadBranchContext, loadOrCreateBranchContext } from './branch-workspace
7
7
  import { buildContentTree as buildContentTreeImpl, } from './content-tree.js';
8
8
  import { listEntries as listEntriesImpl, } from './content-listing.js';
9
9
  import { createDebugLogger } from './utils/debug.js';
10
+ import { baseBranchOf } from './utils/base-branch.js';
11
+ import { CONTEXT_USER_ID } from './system-users.js';
10
12
  const log = createDebugLogger({ prefix: 'Context' });
11
13
  /** True when a ContentStoreError indicates a path/entry wasn't found (expected during candidate probing). */
12
14
  function isLookupFailure(err) {
@@ -69,10 +71,11 @@ export function createCanopyContext(options) {
69
71
  // defaultActiveBranch is explicit). Same contract as the HTTP API handler —
70
72
  // switching branches mid-session updates what getCanopy() serves.
71
73
  await services.refreshActiveBranch();
74
+ await services.resolvePendingBaseBranch();
72
75
  const user = await getUser();
73
76
  // Read once, so every read and listing in this request agrees on it even if
74
77
  // a concurrent request's refreshActiveBranch() replaces services.config.
75
- const activeBranch = services.config.defaultActiveBranch ?? services.config.defaultBaseBranch ?? 'main';
78
+ const activeBranch = services.config.defaultActiveBranch ?? baseBranchOf(services.config);
76
79
  // No ACL enforcement at build time, on static deployments, or for the
77
80
  // synthetic admin. For the last this is load-bearing, not an optimization:
78
81
  // that user has unconditional access anyway (path checks bypass entirely for
@@ -112,7 +115,7 @@ export function createCanopyContext(options) {
112
115
  config: services.config,
113
116
  branchName: activeBranch,
114
117
  mode: services.config.mode,
115
- createdBy: 'canopycms-context',
118
+ createdBy: CONTEXT_USER_ID,
116
119
  remoteUrl: services.config.defaultRemoteUrl,
117
120
  });
118
121
  }