canopycms 0.0.67-int.90 → 0.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (539) hide show
  1. package/README.md +30 -3
  2. package/dist/ai/generate.js +1 -27
  3. package/dist/ai/handler.d.ts +0 -4
  4. package/dist/ai/handler.js +0 -11
  5. package/dist/ai/index.d.ts +1 -8
  6. package/dist/ai/index.js +1 -8
  7. package/dist/ai/json-to-markdown.js +15 -58
  8. package/dist/ai/resolve-branch.d.ts +2 -7
  9. package/dist/ai/resolve-branch.js +2 -8
  10. package/dist/ai/strip-mdx.d.ts +5 -13
  11. package/dist/ai/strip-mdx.js +5 -17
  12. package/dist/ai/to-plain-text.d.ts +10 -28
  13. package/dist/ai/to-plain-text.js +10 -28
  14. package/dist/ai/transform-components.d.ts +2 -4
  15. package/dist/ai/transform-components.js +8 -19
  16. package/dist/ai/types.d.ts +5 -10
  17. package/dist/api/admin-branch-health.d.ts +4 -12
  18. package/dist/api/admin-branch-health.js +26 -66
  19. package/dist/api/admin.d.ts +4 -12
  20. package/dist/api/admin.js +10 -26
  21. package/dist/api/assets.d.ts +0 -25
  22. package/dist/api/assets.js +50 -91
  23. package/dist/api/branch-merge.d.ts +1 -1
  24. package/dist/api/branch-merge.js +0 -2
  25. package/dist/api/branch-review.d.ts +0 -8
  26. package/dist/api/branch-review.js +0 -13
  27. package/dist/api/branch-status.d.ts +0 -3
  28. package/dist/api/branch-status.js +8 -33
  29. package/dist/api/branch-withdraw.d.ts +0 -1
  30. package/dist/api/branch-withdraw.js +6 -9
  31. package/dist/api/branch.d.ts +7 -12
  32. package/dist/api/branch.js +7 -44
  33. package/dist/api/client.d.ts +71 -224
  34. package/dist/api/client.js +82 -233
  35. package/dist/api/comments.d.ts +2 -4
  36. package/dist/api/comments.js +0 -17
  37. package/dist/api/content.d.ts +0 -6
  38. package/dist/api/content.js +66 -142
  39. package/dist/api/entries-constants.d.ts +5 -9
  40. package/dist/api/entries-constants.js +5 -9
  41. package/dist/api/entries.d.ts +8 -13
  42. package/dist/api/entries.js +20 -51
  43. package/dist/api/github-sync.d.ts +2 -4
  44. package/dist/api/github-sync.js +4 -8
  45. package/dist/api/groups.d.ts +3 -0
  46. package/dist/api/groups.js +8 -21
  47. package/dist/api/guards.d.ts +4 -16
  48. package/dist/api/guards.js +3 -22
  49. package/dist/api/index.d.ts +4 -4
  50. package/dist/api/permissions.d.ts +2 -0
  51. package/dist/api/permissions.js +18 -40
  52. package/dist/api/reference-options.d.ts +0 -1
  53. package/dist/api/reference-options.js +3 -11
  54. package/dist/api/request-body-hash.d.ts +12 -23
  55. package/dist/api/request-body-hash.js +12 -23
  56. package/dist/api/resolve-references.d.ts +0 -1
  57. package/dist/api/resolve-references.js +13 -28
  58. package/dist/api/route-builder.d.ts +3 -7
  59. package/dist/api/route-builder.js +1 -1
  60. package/dist/api/routes.d.ts +12 -0
  61. package/dist/api/routes.js +47 -0
  62. package/dist/api/schema.d.ts +23 -33
  63. package/dist/api/schema.js +20 -74
  64. package/dist/api/settings-helpers.js +4 -5
  65. package/dist/api/types.d.ts +0 -5
  66. package/dist/api/user.js +0 -5
  67. package/dist/api/validators.d.ts +14 -52
  68. package/dist/api/validators.js +14 -52
  69. package/dist/assets/asset-prefixes.d.ts +4 -8
  70. package/dist/assets/asset-prefixes.js +4 -8
  71. package/dist/assets/asset-src.d.ts +5 -22
  72. package/dist/assets/asset-src.js +5 -21
  73. package/dist/assets/factory.d.ts +1 -2
  74. package/dist/assets/factory.js +1 -2
  75. package/dist/assets/finalize.d.ts +3 -1
  76. package/dist/assets/finalize.js +3 -1
  77. package/dist/assets/index.d.ts +3 -3
  78. package/dist/assets/index.js +1 -1
  79. package/dist/assets/keys.d.ts +5 -3
  80. package/dist/assets/keys.js +6 -3
  81. package/dist/assets/pipeline.d.ts +3 -2
  82. package/dist/assets/pipeline.js +22 -56
  83. package/dist/assets/sharp-loader.d.ts +2 -10
  84. package/dist/assets/sharp-loader.js +2 -10
  85. package/dist/assets/store-local.d.ts +1 -2
  86. package/dist/assets/store-local.js +1 -2
  87. package/dist/assets/store-s3.d.ts +3 -4
  88. package/dist/assets/svg-sanitizer.d.ts +6 -14
  89. package/dist/assets/svg-sanitizer.js +6 -14
  90. package/dist/assets/transform-directives.d.ts +3 -4
  91. package/dist/assets/transform-directives.js +1 -1
  92. package/dist/assets/transform.d.ts +19 -30
  93. package/dist/assets/transform.js +18 -31
  94. package/dist/auth/cache.d.ts +2 -3
  95. package/dist/auth/cache.js +2 -3
  96. package/dist/auth/caching-auth-plugin.d.ts +11 -22
  97. package/dist/auth/caching-auth-plugin.js +12 -14
  98. package/dist/auth/context-helpers.d.ts +6 -20
  99. package/dist/auth/context-helpers.js +5 -17
  100. package/dist/auth/file-based-auth-cache.d.ts +10 -21
  101. package/dist/auth/file-based-auth-cache.js +23 -51
  102. package/dist/auth/plugin.d.ts +29 -61
  103. package/dist/auth/plugin.js +9 -13
  104. package/dist/auth/types.d.ts +11 -20
  105. package/dist/authorization/branch.d.ts +30 -49
  106. package/dist/authorization/branch.js +16 -42
  107. package/dist/authorization/content.d.ts +11 -40
  108. package/dist/authorization/content.js +13 -45
  109. package/dist/authorization/groups/index.d.ts +0 -3
  110. package/dist/authorization/groups/index.js +0 -3
  111. package/dist/authorization/groups/loader.d.ts +14 -28
  112. package/dist/authorization/groups/loader.js +14 -38
  113. package/dist/authorization/groups/schema.d.ts +0 -6
  114. package/dist/authorization/groups/schema.js +4 -7
  115. package/dist/authorization/helpers.d.ts +9 -28
  116. package/dist/authorization/helpers.js +9 -28
  117. package/dist/authorization/index.d.ts +3 -25
  118. package/dist/authorization/index.js +3 -35
  119. package/dist/authorization/path.d.ts +6 -13
  120. package/dist/authorization/path.js +8 -25
  121. package/dist/authorization/permissions/index.d.ts +0 -3
  122. package/dist/authorization/permissions/index.js +0 -3
  123. package/dist/authorization/permissions/loader.d.ts +14 -27
  124. package/dist/authorization/permissions/loader.js +14 -32
  125. package/dist/authorization/permissions/schema.d.ts +1 -6
  126. package/dist/authorization/permissions/schema.js +6 -13
  127. package/dist/authorization/protected-branch.d.ts +36 -65
  128. package/dist/authorization/protected-branch.js +29 -41
  129. package/dist/authorization/settings-file-store.d.ts +38 -81
  130. package/dist/authorization/settings-file-store.js +38 -79
  131. package/dist/authorization/types.d.ts +4 -26
  132. package/dist/authorization/types.js +0 -6
  133. package/dist/authorization/validation.d.ts +1 -23
  134. package/dist/authorization/validation.js +1 -24
  135. package/dist/branch-health.d.ts +39 -67
  136. package/dist/branch-health.js +27 -43
  137. package/dist/branch-metadata-file.d.ts +16 -32
  138. package/dist/branch-metadata-file.js +13 -26
  139. package/dist/branch-metadata.d.ts +49 -80
  140. package/dist/branch-metadata.js +57 -87
  141. package/dist/branch-registry.d.ts +26 -64
  142. package/dist/branch-registry.js +44 -93
  143. package/dist/branch-schema-cache.d.ts +34 -124
  144. package/dist/branch-schema-cache.js +49 -155
  145. package/dist/branch-workspace.d.ts +4 -4
  146. package/dist/branch-workspace.js +15 -19
  147. package/dist/build/generate-ai-content.d.ts +1 -0
  148. package/dist/build/generate-ai-content.js +12 -25
  149. package/dist/build-canopy.d.ts +17 -27
  150. package/dist/build-canopy.js +16 -23
  151. package/dist/build-mode.d.ts +25 -27
  152. package/dist/build-mode.js +25 -29
  153. package/dist/cli/cli.d.ts +15 -26
  154. package/dist/cli/cli.js +601 -978
  155. package/dist/cli/generate-ai-content.js +351 -628
  156. package/dist/cli/github-app-manifest.d.ts +84 -0
  157. package/dist/cli/github-app-manifest.js +161 -0
  158. package/dist/cli/init-github-app.d.ts +110 -444
  159. package/dist/cli/init-github-app.js +236 -773
  160. package/dist/cli/init.js +39 -80
  161. package/dist/cli/migrate.d.ts +13 -10
  162. package/dist/cli/migrate.js +22 -30
  163. package/dist/cli/project-detect.d.ts +17 -31
  164. package/dist/cli/project-detect.js +31 -42
  165. package/dist/cli/project-root.d.ts +4 -11
  166. package/dist/cli/project-root.js +4 -11
  167. package/dist/cli/prompt.d.ts +10 -0
  168. package/dist/cli/prompt.js +67 -0
  169. package/dist/cli/sync.d.ts +0 -8
  170. package/dist/cli/sync.js +8 -28
  171. package/dist/cli/template-files/middleware.ts.template +15 -29
  172. package/dist/cli/templates.d.ts +2 -12
  173. package/dist/cli/templates.js +4 -4
  174. package/dist/client.d.ts +1 -1
  175. package/dist/comment-store.d.ts +32 -53
  176. package/dist/comment-store.js +32 -52
  177. package/dist/config/flatten.d.ts +0 -3
  178. package/dist/config/flatten.js +2 -12
  179. package/dist/config/helpers.js +2 -2
  180. package/dist/config/index.d.ts +2 -2
  181. package/dist/config/index.js +1 -4
  182. package/dist/config/schemas/collection.d.ts +0 -8
  183. package/dist/config/schemas/collection.js +0 -10
  184. package/dist/config/schemas/config.d.ts +6 -78
  185. package/dist/config/schemas/config.js +38 -54
  186. package/dist/config/schemas/field.d.ts +2 -242
  187. package/dist/config/schemas/field.js +9 -20
  188. package/dist/config/schemas/media.js +1 -1
  189. package/dist/config/schemas/permissions.js +0 -2
  190. package/dist/config/schemas/url.d.ts +5 -6
  191. package/dist/config/schemas/url.js +5 -6
  192. package/dist/config/types.d.ts +45 -33
  193. package/dist/config/types.js +0 -1
  194. package/dist/config.d.ts +1 -8
  195. package/dist/config.js +1 -9
  196. package/dist/content-id-index.d.ts +76 -188
  197. package/dist/content-id-index.js +79 -230
  198. package/dist/content-index-generation.d.ts +27 -48
  199. package/dist/content-index-generation.js +35 -84
  200. package/dist/content-index-registry.d.ts +15 -24
  201. package/dist/content-index-registry.js +5 -9
  202. package/dist/content-listing.d.ts +64 -107
  203. package/dist/content-listing.js +68 -118
  204. package/dist/content-reader.d.ts +20 -29
  205. package/dist/content-reader.js +10 -18
  206. package/dist/content-store.d.ts +210 -336
  207. package/dist/content-store.js +413 -657
  208. package/dist/content-tree.d.ts +44 -86
  209. package/dist/content-tree.js +26 -54
  210. package/dist/context.d.ts +67 -94
  211. package/dist/context.js +53 -72
  212. package/dist/dev-content-watcher.d.ts +18 -18
  213. package/dist/dev-content-watcher.js +41 -44
  214. package/dist/editor/BranchManager.d.ts +3 -6
  215. package/dist/editor/BranchManager.js +24 -44
  216. package/dist/editor/CanopyEditor.js +5 -2
  217. package/dist/editor/CommentsPanel.js +0 -2
  218. package/dist/editor/Editor.js +19 -76
  219. package/dist/editor/EditorAuthGate.d.ts +27 -0
  220. package/dist/editor/EditorAuthGate.js +140 -0
  221. package/dist/editor/EntryNavigator.js +0 -20
  222. package/dist/editor/FormRenderer.d.ts +1 -2
  223. package/dist/editor/FormRenderer.js +8 -23
  224. package/dist/editor/admin/SystemHealthPanel.js +20 -37
  225. package/dist/editor/admin/useSystemHealth.d.ts +2 -3
  226. package/dist/editor/admin/useSystemHealth.js +10 -12
  227. package/dist/editor/client-reference-resolver.d.ts +2 -5
  228. package/dist/editor/client-reference-resolver.js +2 -19
  229. package/dist/editor/comments/BranchComments.js +0 -4
  230. package/dist/editor/comments/EntryComments.js +0 -4
  231. package/dist/editor/comments/FieldWrapper.js +0 -4
  232. package/dist/editor/comments/InlineCommentThread.js +0 -2
  233. package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
  234. package/dist/editor/comments/ThreadCarousel.js +0 -12
  235. package/dist/editor/components/EditorFooter.d.ts +0 -8
  236. package/dist/editor/components/EditorFooter.js +0 -5
  237. package/dist/editor/components/EditorHeader.d.ts +0 -31
  238. package/dist/editor/components/EditorHeader.js +21 -71
  239. package/dist/editor/components/EditorSidebar.d.ts +0 -19
  240. package/dist/editor/components/EditorSidebar.js +0 -16
  241. package/dist/editor/components/EntryCreateModal.d.ts +4 -4
  242. package/dist/editor/components/EntryCreateModal.js +7 -22
  243. package/dist/editor/components/RenameEntryModal.js +0 -4
  244. package/dist/editor/components/UserBadge.js +0 -10
  245. package/dist/editor/components/index.js +0 -2
  246. package/dist/editor/context/ApiClientContext.d.ts +8 -7
  247. package/dist/editor/context/ApiClientContext.js +39 -11
  248. package/dist/editor/context/AssetContext.d.ts +7 -17
  249. package/dist/editor/context/AssetContext.js +7 -17
  250. package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
  251. package/dist/editor/context/EditorIdentityContext.js +7 -0
  252. package/dist/editor/context/EditorStateContext.d.ts +10 -41
  253. package/dist/editor/context/EditorStateContext.js +7 -23
  254. package/dist/editor/context/SWRProvider.d.ts +3 -6
  255. package/dist/editor/context/index.d.ts +1 -20
  256. package/dist/editor/context/index.js +1 -20
  257. package/dist/editor/editor-utils.d.ts +8 -47
  258. package/dist/editor/editor-utils.js +19 -72
  259. package/dist/editor/fields/BlockField.d.ts +2 -2
  260. package/dist/editor/fields/BlockField.js +1 -2
  261. package/dist/editor/fields/CodeField.d.ts +0 -1
  262. package/dist/editor/fields/CodeField.js +0 -1
  263. package/dist/editor/fields/DateTimeField.d.ts +15 -22
  264. package/dist/editor/fields/DateTimeField.js +17 -26
  265. package/dist/editor/fields/ImageField.d.ts +2 -2
  266. package/dist/editor/fields/ImageField.js +8 -14
  267. package/dist/editor/fields/InlineGroupField.d.ts +0 -1
  268. package/dist/editor/fields/InlineGroupField.js +0 -1
  269. package/dist/editor/fields/MarkdownField.d.ts +0 -1
  270. package/dist/editor/fields/MarkdownField.js +6 -11
  271. package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
  272. package/dist/editor/fields/MdxImageDialog.js +0 -1
  273. package/dist/editor/fields/NumberField.d.ts +9 -19
  274. package/dist/editor/fields/NumberField.js +11 -22
  275. package/dist/editor/fields/NumberListField.d.ts +7 -10
  276. package/dist/editor/fields/NumberListField.js +7 -10
  277. package/dist/editor/fields/ObjectField.d.ts +5 -8
  278. package/dist/editor/fields/ObjectField.js +0 -1
  279. package/dist/editor/fields/ReferenceField.d.ts +1 -1
  280. package/dist/editor/fields/ReferenceField.js +11 -16
  281. package/dist/editor/fields/SelectField.d.ts +2 -2
  282. package/dist/editor/fields/SelectField.js +0 -1
  283. package/dist/editor/fields/StringListField.d.ts +0 -4
  284. package/dist/editor/fields/StringListField.js +4 -9
  285. package/dist/editor/fields/TextField.d.ts +0 -1
  286. package/dist/editor/fields/TextField.js +0 -1
  287. package/dist/editor/fields/ToggleField.d.ts +0 -1
  288. package/dist/editor/fields/ToggleField.js +0 -1
  289. package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
  290. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
  291. package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
  292. package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
  293. package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
  294. package/dist/editor/group-manager/GroupCard.d.ts +0 -3
  295. package/dist/editor/group-manager/GroupForm.d.ts +0 -3
  296. package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
  297. package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
  298. package/dist/editor/group-manager/MemberList.d.ts +0 -3
  299. package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
  300. package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
  301. package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
  302. package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
  303. package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
  304. package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
  305. package/dist/editor/group-manager/index.d.ts +0 -6
  306. package/dist/editor/group-manager/index.js +0 -10
  307. package/dist/editor/group-manager/types.d.ts +2 -5
  308. package/dist/editor/group-manager/types.js +0 -3
  309. package/dist/editor/hooks/index.d.ts +6 -11
  310. package/dist/editor/hooks/index.js +6 -12
  311. package/dist/editor/hooks/useBranchActions.d.ts +0 -14
  312. package/dist/editor/hooks/useBranchActions.js +0 -22
  313. package/dist/editor/hooks/useBranchManager.d.ts +2 -48
  314. package/dist/editor/hooks/useBranchManager.js +4 -64
  315. package/dist/editor/hooks/useBranchesData.d.ts +7 -6
  316. package/dist/editor/hooks/useBranchesData.js +7 -6
  317. package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
  318. package/dist/editor/hooks/useCommentSystem.js +1 -36
  319. package/dist/editor/hooks/useDraftManager.d.ts +2 -35
  320. package/dist/editor/hooks/useDraftManager.js +23 -64
  321. package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
  322. package/dist/editor/hooks/useEditorLayout.js +0 -23
  323. package/dist/editor/hooks/useEntriesData.d.ts +1 -0
  324. package/dist/editor/hooks/useEntriesData.js +2 -3
  325. package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
  326. package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
  327. package/dist/editor/hooks/useEntryManager.d.ts +2 -27
  328. package/dist/editor/hooks/useEntryManager.js +29 -78
  329. package/dist/editor/hooks/useGroupManager.d.ts +0 -21
  330. package/dist/editor/hooks/useGroupManager.js +0 -21
  331. package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
  332. package/dist/editor/hooks/usePermissionManager.js +0 -20
  333. package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
  334. package/dist/editor/hooks/useReferenceResolution.js +1 -48
  335. package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
  336. package/dist/editor/hooks/useSchemaManager.js +0 -19
  337. package/dist/editor/hooks/useUserContext.d.ts +2 -9
  338. package/dist/editor/hooks/useUserContext.js +10 -10
  339. package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
  340. package/dist/editor/hooks/useUserMetadata.js +0 -3
  341. package/dist/editor/media/AssetCard.d.ts +0 -1
  342. package/dist/editor/media/AssetCard.js +1 -2
  343. package/dist/editor/media/CropStep.d.ts +0 -1
  344. package/dist/editor/media/CropStep.js +0 -1
  345. package/dist/editor/media/MediaLibrary.d.ts +1 -2
  346. package/dist/editor/media/MediaLibrary.js +1 -2
  347. package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
  348. package/dist/editor/media/MediaLibraryBody.js +0 -1
  349. package/dist/editor/media/crop-math.d.ts +1 -1
  350. package/dist/editor/media/crop-math.js +1 -1
  351. package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
  352. package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
  353. package/dist/editor/permission-manager/PermissionEditor.js +0 -1
  354. package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
  355. package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
  356. package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
  357. package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
  358. package/dist/editor/permission-manager/constants.d.ts +0 -3
  359. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
  360. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
  361. package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
  362. package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
  363. package/dist/editor/permission-manager/index.d.ts +0 -7
  364. package/dist/editor/permission-manager/index.js +0 -16
  365. package/dist/editor/permission-manager/types.d.ts +2 -12
  366. package/dist/editor/permission-manager/types.js +0 -3
  367. package/dist/editor/permission-manager/utils.d.ts +0 -28
  368. package/dist/editor/permission-manager/utils.js +9 -23
  369. package/dist/editor/preview-bridge.d.ts +5 -1
  370. package/dist/editor/preview-bridge.js +4 -4
  371. package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
  372. package/dist/editor/schema-editor/CollectionEditor.js +9 -31
  373. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
  374. package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
  375. package/dist/editor/schema-editor/index.d.ts +0 -7
  376. package/dist/editor/schema-editor/index.js +0 -7
  377. package/dist/editor/utils/env.d.ts +3 -18
  378. package/dist/editor/utils/env.js +6 -25
  379. package/dist/entry-link-resolver.d.ts +13 -35
  380. package/dist/entry-link-resolver.js +20 -53
  381. package/dist/entry-schema-registry.d.ts +20 -60
  382. package/dist/entry-schema-registry.js +20 -69
  383. package/dist/entry-schema.d.ts +118 -223
  384. package/dist/entry-schema.js +46 -91
  385. package/dist/git-manager.d.ts +101 -200
  386. package/dist/git-manager.js +211 -384
  387. package/dist/github-service.d.ts +34 -75
  388. package/dist/github-service.js +37 -91
  389. package/dist/http/handler.d.ts +6 -37
  390. package/dist/http/handler.js +65 -120
  391. package/dist/http/router.d.ts +15 -32
  392. package/dist/http/router.js +34 -99
  393. package/dist/http/types.d.ts +15 -40
  394. package/dist/http/types.js +3 -7
  395. package/dist/id.d.ts +4 -10
  396. package/dist/id.js +4 -10
  397. package/dist/index.js +6 -9
  398. package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
  399. package/dist/operating-mode/client-safe-strategy.js +8 -28
  400. package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
  401. package/dist/operating-mode/client-unsafe-strategy.js +9 -34
  402. package/dist/operating-mode/client.d.ts +3 -10
  403. package/dist/operating-mode/client.js +3 -8
  404. package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
  405. package/dist/operating-mode/deployment-name-fixtures.js +14 -18
  406. package/dist/operating-mode/deployment-name.d.ts +19 -29
  407. package/dist/operating-mode/deployment-name.js +29 -47
  408. package/dist/operating-mode/index.d.ts +4 -25
  409. package/dist/operating-mode/index.js +9 -31
  410. package/dist/operating-mode/mode-env.d.ts +32 -54
  411. package/dist/operating-mode/mode-env.js +36 -58
  412. package/dist/operating-mode/types.d.ts +24 -72
  413. package/dist/operating-mode/types.js +1 -7
  414. package/dist/paths/branch-name.d.ts +30 -40
  415. package/dist/paths/branch-name.js +30 -40
  416. package/dist/paths/branch.d.ts +4 -18
  417. package/dist/paths/branch.js +6 -22
  418. package/dist/paths/index.d.ts +2 -13
  419. package/dist/paths/index.js +3 -23
  420. package/dist/paths/normalize-server.d.ts +3 -15
  421. package/dist/paths/normalize-server.js +3 -15
  422. package/dist/paths/normalize.d.ts +13 -33
  423. package/dist/paths/normalize.js +13 -33
  424. package/dist/paths/resolve.d.ts +4 -17
  425. package/dist/paths/resolve.js +4 -22
  426. package/dist/paths/types.d.ts +12 -34
  427. package/dist/paths/types.js +5 -7
  428. package/dist/paths/validation.d.ts +22 -119
  429. package/dist/paths/validation.js +30 -148
  430. package/dist/reference-resolver.d.ts +0 -13
  431. package/dist/reference-resolver.js +0 -18
  432. package/dist/resolve-canopy-user.d.ts +16 -18
  433. package/dist/resolve-canopy-user.js +17 -26
  434. package/dist/resource-generation.d.ts +16 -25
  435. package/dist/resource-generation.js +34 -97
  436. package/dist/schema/meta-loader.d.ts +2 -14
  437. package/dist/schema/meta-loader.js +15 -64
  438. package/dist/schema/resolver.d.ts +1 -4
  439. package/dist/schema/resolver.js +1 -6
  440. package/dist/schema/schema-store-types.d.ts +0 -12
  441. package/dist/schema/schema-store.d.ts +61 -138
  442. package/dist/schema/schema-store.js +85 -282
  443. package/dist/schema/types.d.ts +0 -4
  444. package/dist/server.d.ts +62 -93
  445. package/dist/server.js +63 -94
  446. package/dist/services.d.ts +23 -47
  447. package/dist/services.js +55 -98
  448. package/dist/settings-workspace.d.ts +17 -30
  449. package/dist/settings-workspace.js +51 -81
  450. package/dist/static/index.d.ts +26 -52
  451. package/dist/static/index.js +33 -67
  452. package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
  453. package/dist/task-queue/cms-task-queue.js +13 -0
  454. package/dist/task-queue/task-queue-config.d.ts +3 -0
  455. package/dist/{worker → task-queue}/task-queue-config.js +1 -6
  456. package/dist/task-queue/task-queue.d.ts +16 -22
  457. package/dist/task-queue/task-queue.js +20 -44
  458. package/dist/task-queue/types.d.ts +0 -1
  459. package/dist/task-queue/worker-status.d.ts +35 -0
  460. package/dist/task-queue/worker-status.js +40 -0
  461. package/dist/types.d.ts +40 -51
  462. package/dist/url-collision.d.ts +26 -40
  463. package/dist/url-collision.js +47 -63
  464. package/dist/url-exclusivity-fixtures.d.ts +15 -20
  465. package/dist/url-exclusivity-fixtures.js +14 -19
  466. package/dist/url-path-resolver.d.ts +8 -16
  467. package/dist/url-path-resolver.js +19 -29
  468. package/dist/user.d.ts +8 -21
  469. package/dist/user.js +10 -19
  470. package/dist/utils/async-mutex.js +4 -6
  471. package/dist/utils/atomic-write.d.ts +2 -11
  472. package/dist/utils/atomic-write.js +2 -11
  473. package/dist/utils/body-field.d.ts +8 -15
  474. package/dist/utils/body-field.js +8 -15
  475. package/dist/utils/content-serialize.d.ts +18 -24
  476. package/dist/utils/content-serialize.js +109 -160
  477. package/dist/utils/content-write-lock.d.ts +51 -106
  478. package/dist/utils/content-write-lock.js +66 -111
  479. package/dist/utils/debug.d.ts +7 -23
  480. package/dist/utils/debug.js +7 -23
  481. package/dist/utils/entry-url.d.ts +11 -23
  482. package/dist/utils/entry-url.js +11 -26
  483. package/dist/utils/error.d.ts +25 -90
  484. package/dist/utils/error.js +60 -147
  485. package/dist/utils/flatten-group-fields.d.ts +5 -7
  486. package/dist/utils/flatten-group-fields.js +5 -7
  487. package/dist/utils/format.d.ts +1 -5
  488. package/dist/utils/format.js +1 -5
  489. package/dist/utils/fs.d.ts +3 -4
  490. package/dist/utils/fs.js +3 -4
  491. package/dist/utils/git.d.ts +52 -83
  492. package/dist/utils/git.js +79 -125
  493. package/dist/utils/logger.d.ts +35 -50
  494. package/dist/utils/logger.js +35 -52
  495. package/dist/utils/provisioning-lock.d.ts +23 -35
  496. package/dist/utils/provisioning-lock.js +49 -69
  497. package/dist/utils/sanitize-href.d.ts +5 -10
  498. package/dist/utils/sanitize-href.js +8 -18
  499. package/dist/utils/title-field.d.ts +17 -20
  500. package/dist/utils/title-field.js +18 -27
  501. package/dist/utils/typed-filename.d.ts +20 -48
  502. package/dist/utils/typed-filename.js +25 -61
  503. package/dist/utils/url-prefix.d.ts +3 -3
  504. package/dist/utils/url-prefix.js +3 -3
  505. package/dist/validation/block-structural-keys.d.ts +16 -31
  506. package/dist/validation/block-structural-keys.js +19 -29
  507. package/dist/validation/deletion-checker.d.ts +0 -34
  508. package/dist/validation/deletion-checker.js +1 -43
  509. package/dist/validation/entry-link-validator.d.ts +6 -13
  510. package/dist/validation/entry-link-validator.js +4 -13
  511. package/dist/validation/entry-type-reference-validator.d.ts +13 -18
  512. package/dist/validation/entry-type-reference-validator.js +13 -18
  513. package/dist/validation/entry-validator.d.ts +13 -25
  514. package/dist/validation/entry-validator.js +15 -32
  515. package/dist/validation/field-traversal.d.ts +3 -30
  516. package/dist/validation/field-traversal.js +1 -32
  517. package/dist/validation/reference-validator.d.ts +5 -24
  518. package/dist/validation/reference-validator.js +5 -30
  519. package/dist/worker/cms-worker.d.ts +172 -232
  520. package/dist/worker/cms-worker.js +241 -333
  521. package/dist/worker/git-sync.d.ts +32 -133
  522. package/dist/worker/git-sync.js +129 -191
  523. package/dist/worker/github-auth.d.ts +13 -19
  524. package/dist/worker/github-auth.js +13 -19
  525. package/dist/worker/history-rewrite.d.ts +53 -66
  526. package/dist/worker/history-rewrite.js +54 -60
  527. package/dist/worker/log.d.ts +24 -41
  528. package/dist/worker/log.js +25 -42
  529. package/dist/worker/rebase.d.ts +20 -110
  530. package/dist/worker/rebase.js +279 -354
  531. package/dist/worker/task-runner.d.ts +32 -66
  532. package/dist/worker/task-runner.js +126 -162
  533. package/dist/worker/worker-context.d.ts +41 -81
  534. package/package.json +4 -4
  535. package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
  536. package/dist/worker/task-queue-config.d.ts +0 -8
  537. package/dist/worker/task-queue.js +0 -19
  538. package/dist/worker/worker-status.d.ts +0 -47
  539. package/dist/worker/worker-status.js +0 -52
@@ -478,18 +478,18 @@ var init_config = __esm({
478
478
  onAccountClick: z6.function().returns(z6.void()).optional(),
479
479
  onLogoutClick: z6.function().returns(z6.void()).optional(),
480
480
  // Optional: custom account component (e.g., Clerk's UserButton)
481
- AccountComponent: z6.custom().optional()
481
+ AccountComponent: z6.custom().optional(),
482
+ // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
483
+ SignInComponent: z6.custom().optional()
482
484
  });
483
485
  CanopyConfigSchema = z6.object({
484
486
  media: mediaSchema.optional(),
485
- // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
486
- // have .default('deny'), so the field is optional on input but always resolves to
487
- // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
488
- // before the inner default runs, defeating the fail-closed default (SCH-M1).
489
- // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
490
- // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
491
- // still resolves to 'deny' and an omitted level inside the object form stays undefined
492
- // (resolved to 'deny' at read time by resolveDefaultPathAccess).
487
+ // No outer .optional(): both schemas already .default('deny'), so the field is optional on
488
+ // input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
489
+ // .optional() would short-circuit before the inner default runs, defeating the fail-closed
490
+ // default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
491
+ // inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
492
+ // undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
493
493
  defaultBranchAccess: defaultBranchAccessSchema,
494
494
  defaultPathAccess: defaultPathAccessSchema,
495
495
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -513,16 +513,11 @@ var init_config = __esm({
513
513
  allowNetworkRemoteInProd: z6.boolean().optional(),
514
514
  settingsBranch: z6.string().optional(),
515
515
  autoCreateSettingsPR: z6.boolean().optional(),
516
- // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
517
- // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
518
- // the literal 'prod' instead of staying `undefined` — collapsing the
519
- // env > config > modeDefault precedence chain that resolveDeploymentName
520
- // (operating-mode/deployment-name.ts) implements: config would then always
521
- // "win" over modeDefault (masking dev's real default of 'local'), and the env
522
- // var would just be racing config's baked-in 'prod' instead of a true absence.
523
- // `.optional()` here is what keeps an omitted deploymentName reaching
524
- // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
525
- // Do not "fix" this by removing `.optional()`.
516
+ // `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
517
+ // `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
518
+ // env > config > modeDefault precedence chain `resolveDeploymentName`
519
+ // (operating-mode/deployment-name.ts) implements — config would always "win" over
520
+ // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
526
521
  deploymentName: deploymentNameSchema.optional(),
527
522
  contentRoot: contentRootSchema.default("content"),
528
523
  sourceRoot: sourceRootSchema.optional(),
@@ -632,7 +627,6 @@ var init_client_safe_strategy = __esm({
632
627
  constructor() {
633
628
  this.mode = "prod";
634
629
  }
635
- // UI Feature Flags
636
630
  supportsBranching() {
637
631
  return true;
638
632
  }
@@ -645,7 +639,6 @@ var init_client_safe_strategy = __esm({
645
639
  supportsPullRequests() {
646
640
  return true;
647
641
  }
648
- // Simple Data
649
642
  getPermissionsFileName() {
650
643
  return "permissions.json";
651
644
  }
@@ -663,7 +656,6 @@ var init_client_safe_strategy = __esm({
663
656
  constructor() {
664
657
  this.mode = "dev";
665
658
  }
666
- // UI Feature Flags
667
659
  supportsBranching() {
668
660
  return true;
669
661
  }
@@ -676,7 +668,6 @@ var init_client_safe_strategy = __esm({
676
668
  supportsPullRequests() {
677
669
  return false;
678
670
  }
679
- // Simple Data
680
671
  getPermissionsFileName() {
681
672
  return "permissions.json";
682
673
  }
@@ -796,11 +787,6 @@ var init_client_unsafe_strategy = __esm({
796
787
  init_deployment_name();
797
788
  init_config2();
798
789
  ProdStrategy = class extends ProdClientSafeStrategy {
799
- // All client-safe methods inherited automatically from ProdClientSafeStrategy:
800
- // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
801
- // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
802
- // - shouldCommit(), shouldPush()
803
- // Add client-unsafe methods (use Node.js APIs)
804
790
  getWorkspaceRoot(_sourceRoot) {
805
791
  return path9.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
806
792
  }
@@ -854,7 +840,6 @@ var init_client_unsafe_strategy = __esm({
854
840
  }
855
841
  };
856
842
  DevStrategy = class extends DevClientSafeStrategy {
857
- // Inherits client-safe methods from DevClientSafeStrategy
858
843
  getWorkspaceRoot(sourceRoot) {
859
844
  return path9.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
860
845
  }
@@ -910,8 +895,6 @@ var init_client_unsafe_strategy = __esm({
910
895
  // dist/operating-mode/index.js
911
896
  var operating_mode_exports = {};
912
897
  __export(operating_mode_exports, {
913
- BROWSER_MODE_ENV_VAR: () => BROWSER_MODE_ENV_VAR,
914
- SERVER_MODE_ENV_VAR: () => SERVER_MODE_ENV_VAR,
915
898
  clearClientStrategyCache: () => clearClientStrategyCache,
916
899
  clearStrategyCache: () => clearStrategyCache,
917
900
  clientOperatingStrategy: () => clientOperatingStrategy,
@@ -1216,12 +1199,11 @@ function provisioningLockOptions(lockPath, retries, onCompromised) {
1216
1199
  realpath: false,
1217
1200
  retries,
1218
1201
  stale: 3e4,
1219
- // proper-lockfile invokes this from inside its refresh timer, so ANY throw
1220
- // escaping here is an uncaught exception that kills the process -- the very
1221
- // failure the handler exists to prevent. Call sites are told not to throw
1222
- // (see OnLockCompromised); this makes it structural rather than a
1223
- // convention, and also covers the LOGGER throwing: under `CI=true`,
1224
- // vitest's `onConsoleLog` turns any console write into a throw.
1202
+ // proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
1203
+ // an uncaught exception that kills the process. Call sites are told not to throw (see
1204
+ // OnLockCompromised); this makes it structural rather than a convention, and also covers the
1205
+ // LOGGER throwing -- under `CI=true`, vitest's `onConsoleLog` turns a console write into a
1206
+ // throw.
1225
1207
  onCompromised: (err) => {
1226
1208
  try {
1227
1209
  if (onCompromised) {
@@ -1513,9 +1495,9 @@ var ContentIdIndex = class {
1513
1495
  this.root = path4.resolve(root);
1514
1496
  }
1515
1497
  /**
1516
- * Build index by scanning filenames recursively. Duplicate embedded IDs
1517
- * are quarantined, not thrown on -- see the class doc comment's
1518
- * "Duplicate-ID quarantine" section and {@link getDuplicateIds}.
1498
+ * Build the index by scanning filenames recursively. Duplicate embedded IDs
1499
+ * are quarantined, not thrown on — see the class doc and
1500
+ * {@link getDuplicateIds}.
1519
1501
  */
1520
1502
  async buildFromFilenames(startPath = "") {
1521
1503
  await this.scanDirectory(startPath);
@@ -1531,7 +1513,7 @@ var ContentIdIndex = class {
1531
1513
  this.byCollection.get(location.collection).add(id);
1532
1514
  }
1533
1515
  }
1534
- /** Remove a location from all three indexes -- used when a lexicographically-earlier duplicate displaces it. */
1516
+ /** Remove a location from all three indexes, when an earlier duplicate displaces it. */
1535
1517
  evictLocation(location) {
1536
1518
  this.idToLocation.delete(location.id);
1537
1519
  this.pathToId.delete(location.relativePath);
@@ -1544,7 +1526,7 @@ var ContentIdIndex = class {
1544
1526
  }
1545
1527
  }
1546
1528
  }
1547
- /** Record a quarantined duplicate path for admin-facing health reporting (see getDuplicateIds). */
1529
+ /** Record a quarantined duplicate path for health reporting (see getDuplicateIds). */
1548
1530
  recordDuplicate(id, droppedPath) {
1549
1531
  let dropped = this.duplicateIds.get(id);
1550
1532
  if (!dropped) {
@@ -1606,11 +1588,9 @@ var ContentIdIndex = class {
1606
1588
  }
1607
1589
  }
1608
1590
  /**
1609
- * IDs discovered on more than one file during the last buildFromFilenames
1610
- * scan. Empty unless the content tree currently has a duplicate-ID pair
1611
- * (see the class doc comment). Consumed by branch-health.ts's admin scan
1612
- * and by the repair-content-duplicates admin action
1613
- * (api/admin-branch-health.ts).
1591
+ * IDs the last buildFromFilenames scan found on more than one file; empty
1592
+ * otherwise. Consumed by branch-health.ts's admin scan and by the
1593
+ * repair-content-duplicates action (api/admin-branch-health.ts).
1614
1594
  */
1615
1595
  getDuplicateIds() {
1616
1596
  const result = [];
@@ -1622,11 +1602,10 @@ var ContentIdIndex = class {
1622
1602
  return result;
1623
1603
  }
1624
1604
  /**
1625
- * The quarantine record for ONE id, or null when that id is not
1626
- * duplicated. The O(1) counterpart to {@link getDuplicateIds} for the
1627
- * write path, which has to ask this question on every save whose resolved
1628
- * path disagrees with the index (ContentStore.write()'s duplicate-ID
1629
- * guard) and must not pay for materializing the whole list.
1605
+ * The quarantine record for ONE id, or null when that id is not duplicated.
1606
+ * The O(1) counterpart to {@link getDuplicateIds}, for ContentStore.write()'s
1607
+ * duplicate-ID guard: it asks on every save whose resolved path disagrees
1608
+ * with the index, so it must not pay to materialize the whole list.
1630
1609
  */
1631
1610
  getDuplicateFor(id) {
1632
1611
  const dropped = this.duplicateIds.get(id);
@@ -1641,33 +1620,19 @@ var ContentIdIndex = class {
1641
1620
  droppedPaths: Array.from(dropped).sort()
1642
1621
  };
1643
1622
  }
1644
- /**
1645
- * Forward lookup: ID → location (O(1))
1646
- */
1623
+ /** Forward lookup: ID → location (O(1)). */
1647
1624
  findById(id) {
1648
1625
  return this.idToLocation.get(id) || null;
1649
1626
  }
1650
- /**
1651
- * Reverse lookup: path → ID (O(1))
1652
- */
1627
+ /** Reverse lookup: path → ID (O(1)). */
1653
1628
  findByPath(relativePath) {
1654
1629
  return this.pathToId.get(relativePath) || null;
1655
1630
  }
1656
- /**
1657
- * Get all ID locations in the index.
1658
- * Useful for validation and checking references.
1659
- */
1631
+ /** Every ID location in the index, for validation and reference checking. */
1660
1632
  getAllLocations() {
1661
1633
  return Array.from(this.idToLocation.values());
1662
1634
  }
1663
- /**
1664
- * Get all entries in a collection by collection path.
1665
- *
1666
- * Performance: O(1) + O(m) where m is the number of entries in the collection.
1667
- *
1668
- * @param collectionPath - The collection path (e.g., "content/posts")
1669
- * @returns Array of IdLocation objects for entries in the collection
1670
- */
1635
+ /** Entries in one collection: O(1) + O(entries in it). */
1671
1636
  getEntriesInCollection(collectionPath) {
1672
1637
  const idSet = this.byCollection.get(collectionPath);
1673
1638
  if (!idSet) {
@@ -1683,14 +1648,9 @@ var ContentIdIndex = class {
1683
1648
  return locations;
1684
1649
  }
1685
1650
  /**
1686
- * Get all entries in a collection and its subcollections (tree traversal).
1687
- * For example, getEntriesInCollectionTree("content/data-catalog") returns entries
1688
- * in "content/data-catalog", "content/data-catalog/openstax", etc.
1689
- *
1690
- * Performance: O(k * m) where k is number of matching collections and m is avg entries per collection.
1691
- *
1692
- * @param collectionPath - The root collection path to search
1693
- * @returns Array of IdLocation objects for entries in the collection tree
1651
+ * Entries in a collection and every subcollection: "content/docs" returns
1652
+ * "content/docs", "content/docs/api", and so on. O(matching collections ×
1653
+ * entries each).
1694
1654
  */
1695
1655
  getEntriesInCollectionTree(collectionPath) {
1696
1656
  const locations = [];
@@ -1713,7 +1673,6 @@ var ContentIdIndex = class {
1713
1673
  *
1714
1674
  * Performance: O(n) where n is total number of entries.
1715
1675
  *
1716
- * @returns Array of all IdLocation objects that are entries
1717
1676
  */
1718
1677
  getAllEntryLocations() {
1719
1678
  const locations = [];
@@ -1728,9 +1687,8 @@ var ContentIdIndex = class {
1728
1687
  return locations;
1729
1688
  }
1730
1689
  /**
1731
- * Add a new entry or collection to the index.
1732
- * Note: This only updates the in-memory index. The file with embedded ID
1733
- * must already exist on disk (created by ContentStore).
1690
+ * Add an entry or collection. In-memory only: the file with the embedded ID
1691
+ * must already exist on disk, created by ContentStore. Throws on collision.
1734
1692
  */
1735
1693
  add(location) {
1736
1694
  const id = extractIdFromFilename(path4.basename(location.relativePath));
@@ -1758,8 +1716,8 @@ var ContentIdIndex = class {
1758
1716
  }
1759
1717
  }
1760
1718
  /**
1761
- * Remove an entry or collection from the index by ID.
1762
- * Note: This only updates the in-memory index. The file must be deleted separately.
1719
+ * Remove an entry or collection by ID. In-memory only: the caller deletes the
1720
+ * file separately.
1763
1721
  */
1764
1722
  remove(id) {
1765
1723
  const location = this.idToLocation.get(id);
@@ -1778,8 +1736,8 @@ var ContentIdIndex = class {
1778
1736
  this.pathToId.delete(location.relativePath);
1779
1737
  }
1780
1738
  /**
1781
- * Update the path for an existing ID (e.g., after file rename/move).
1782
- * This is used to keep the index in sync when files are renamed.
1739
+ * Repoint an existing ID at a new path; keeps the index in step with a
1740
+ * rename or move. Throws when the ID is unknown.
1783
1741
  */
1784
1742
  updatePath(id, newRelativePath) {
1785
1743
  const location = this.idToLocation.get(id);
@@ -2053,15 +2011,11 @@ var DebugLogger = class {
2053
2011
  return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
2054
2012
  }
2055
2013
  /**
2056
- * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
2057
- * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
2058
- * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
2059
- * `multi_line_start_pattern` matches the timestamp at the START of a line
2060
- * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
2061
- * leading `[` meant every debug line was silently folded into the previous
2062
- * event instead of starting its own. The bracketed `[prefix:category]` and
2063
- * `[level]` fields still follow, so the human-readable shape is unchanged
2064
- * apart from those two characters.
2014
+ * The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
2015
+ * be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
2016
+ * the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
2017
+ * line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
2018
+ * character folds the line into the previous event instead of starting its own.
2065
2019
  */
2066
2020
  formatMessage(level, category, message) {
2067
2021
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -2094,15 +2048,9 @@ var DebugLogger = class {
2094
2048
  throw new Error(errorMsg);
2095
2049
  }
2096
2050
  }
2097
- /**
2098
- * Start timing an operation
2099
- */
2100
2051
  time(label) {
2101
2052
  this.timers.set(label, Date.now());
2102
2053
  }
2103
- /**
2104
- * End timing an operation and log the duration
2105
- */
2106
2054
  timeEnd(category, label) {
2107
2055
  const start = this.timers.get(label);
2108
2056
  if (start === void 0) {
@@ -2114,9 +2062,6 @@ var DebugLogger = class {
2114
2062
  this.debug(category, `${label} completed`, { durationMs: duration });
2115
2063
  return duration;
2116
2064
  }
2117
- /**
2118
- * Wrap an async function with automatic timing
2119
- */
2120
2065
  async timed(category, label, fn) {
2121
2066
  this.time(label);
2122
2067
  try {
@@ -2232,7 +2177,6 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
2232
2177
  collections.push({
2233
2178
  ...meta,
2234
2179
  path: folderPath,
2235
- // Path derived from folder name
2236
2180
  contentId: collectionContentId
2237
2181
  });
2238
2182
  const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
@@ -2482,38 +2426,26 @@ var BranchSchemaCache = class {
2482
2426
  /**
2483
2427
  * Whether to skip the on-disk cache for this branchRoot.
2484
2428
  *
2485
- * branchRoot equals the project root (process.cwd()) only in the synthetic
2486
- * contexts used by static deployments and build phases — real branch roots are
2487
- * always nested under the workspace (.canopy-dev/content-branches/<name> in dev,
2488
- * <workspaceRoot>/content-branches/<name> in prod). Never write .canopy-meta/ at
2489
- * the project root, regardless of which entrypoint produced the cwd branchRoot.
2429
+ * Never write `.canopy-meta/` at the project root, whichever entrypoint
2430
+ * produced the cwd branchRoot. branchRoot equals process.cwd() only in the
2431
+ * synthetic contexts static deployments and build phases use; a real branch
2432
+ * root is always nested under the workspace.
2490
2433
  */
2491
2434
  skipDiskCache(branchRoot) {
2492
2435
  return isBuildMode() || path8.resolve(branchRoot) === path8.resolve(process.cwd());
2493
2436
  }
2494
- /**
2495
- * Get schema for a branch (loads from cache or resolves fresh).
2496
- *
2497
- * @param branchRoot - Root directory of the branch (e.g., .canopy-dev/content-branches/main)
2498
- * @param entrySchemaRegistry - Map of schema names to field definitions
2499
- * @param contentRootName - Name of content directory (e.g., "content") from config
2500
- * @returns Resolved schema tree and flattened schema
2501
- */
2437
+ /** Get schema for a branch, from the cache when fresh, else resolved fresh. */
2502
2438
  async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
2503
2439
  return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
2504
2440
  }
2505
2441
  /**
2506
- * Resolve the schema from disk. Wrapped in a protected method (rather than
2507
- * calling the imported resolveSchema directly) so tests can subclass and
2508
- * override with a deferred/blocking implementation to simulate cross-process
2442
+ * Resolve the schema from disk. Protected rather than a direct resolveSchema
2443
+ * call so tests can subclass and block it to simulate cross-process
2509
2444
  * interleavings — mirrors BranchRegistry's scanBranchDirectories() hook.
2510
2445
  */
2511
2446
  async resolveFresh(contentRoot, entrySchemaRegistry) {
2512
2447
  return resolveSchema(contentRoot, entrySchemaRegistry);
2513
2448
  }
2514
- /**
2515
- * Load schema from cache or resolve fresh if cache is missing or stale.
2516
- */
2517
2449
  async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
2518
2450
  const contentRoot = path8.join(branchRoot, contentRootName);
2519
2451
  const skipDiskCache = this.skipDiskCache(branchRoot);
@@ -2599,23 +2531,13 @@ var BranchSchemaCache = class {
2599
2531
  return { schema: result.schema, flatSchema };
2600
2532
  }
2601
2533
  /**
2602
- * Resolve the schema fresh from disk and persist it, SKIPPING the cache
2603
- * read entirely -- unlike {@link getSchema}, which can short-circuit
2604
- * through a cache HIT. That distinction matters for eager re-resolve after
2605
- * invalidation (see {@link invalidate}'s doc comment and
2606
- * `SchemaOps.invalidateSchemaCache()` in schema/schema-store.ts, the sole
2607
- * intended caller): the whole point of an eager re-resolve is to guarantee
2608
- * ONE scan that is coherent with the mutation this host just made. A plain
2609
- * `getSchema()` call right after `invalidate()` is NOT that guarantee --
2610
- * `getSchema()`'s cache-read fast path (`loadFromCacheOrResolve`) can still
2611
- * return a snapshot written by a DIFFERENT, concurrent host: if that
2612
- * foreign host's own eager re-resolve raced this one and embedded the
2613
- * (now-current) marker token over ITS OWN stale-NFS-cache scan (the
2614
- * window-E case this class's doc comment describes), this host's
2615
- * `getSchema()` would happily accept that foreign snapshot as "current"
2616
- * and skip the one scan this call was specifically trying to guarantee.
2617
- * `resolveAndPersist()` never reads the cache file at all, so it cannot be
2618
- * short-circuited that way.
2534
+ * Resolve fresh from disk and persist, SKIPPING the cache read entirely.
2535
+ * That is what makes it, and not {@link getSchema}, the right call for the
2536
+ * eager re-resolve in `SchemaOps.invalidateSchemaCache()` (its sole intended
2537
+ * caller): an eager re-resolve exists to guarantee ONE scan coherent with the
2538
+ * mutation this host just made, and getSchema()'s cache-read fast path can
2539
+ * instead return a snapshot a concurrent host wrote — possibly one embedding
2540
+ * the now-current token over ITS own stale-NFS scan — skipping that scan.
2619
2541
  */
2620
2542
  async resolveAndPersist(branchRoot, entrySchemaRegistry, contentRootName = "content") {
2621
2543
  const skipDiskCache = this.skipDiskCache(branchRoot);
@@ -2624,20 +2546,12 @@ var BranchSchemaCache = class {
2624
2546
  });
2625
2547
  }
2626
2548
  /**
2627
- * Invalidate cache for a branch by bumping the cross-process generation
2628
- * marker (resource-generation.ts). Every process sharing this branchRoot
2629
- * will re-resolve at its next read.
2549
+ * Invalidate a branch's cache by bumping the marker; every process sharing
2550
+ * this branchRoot re-resolves at its next read. The bump must succeed —
2551
+ * swallowing that failure leaves the schema cache stale indefinitely, and
2552
+ * unlike BranchRegistry there is no get-miss backstop for a resolved schema.
2630
2553
  *
2631
- * The bump must succeed: a swallowed failure here would leave the schema
2632
- * cache stale indefinitely with no bounding backstop (unlike
2633
- * BranchRegistry, there is no get-miss backstop for a resolved schema).
2634
- *
2635
- * No eager regeneration here (unlike BranchRegistry.invalidate()): this
2636
- * method isn't given the entrySchemaRegistry/contentRootName resolveSchema
2637
- * needs. See the class doc comment for why the mutating request's own
2638
- * follow-up schema read closes the same window-E gap instead.
2639
- *
2640
- * @param branchRoot - Root directory of the branch
2554
+ * No eager re-resolve here; it lives in SchemaOps (see the class doc).
2641
2555
  */
2642
2556
  async invalidate(branchRoot) {
2643
2557
  if (this.skipDiskCache(branchRoot))
@@ -2790,13 +2704,10 @@ var DuplicateContentIdError = class extends ContentConflictError {
2790
2704
  constructor(contentId, paths) {
2791
2705
  const sorted = Array.from(new Set(paths)).sort();
2792
2706
  super(
2793
- // Names the STATE, not an action. The repair-content-duplicates
2794
- // endpoint exists but nothing in the editor renders it, so telling an
2795
- // editor "an admin can run X" sent them to an admin who could neither
2796
- // run X nor see that the branch was affected. Say what is true; the
2797
- // admin panel's read-only duplicate list (SystemHealthPanel) is the
2798
- // diagnosis half, and the repair UI is tracked in
2799
- // .claude/future-tasks/duplicate-content-id-repair-ui.md.
2707
+ // Names the STATE, not an action: the repair-content-duplicates endpoint
2708
+ // exists but nothing in the editor renders it, and the admin panel's
2709
+ // duplicate list (SystemHealthPanel) is the diagnosis half only. The repair
2710
+ // UI is tracked in .claude/future-tasks/duplicate-content-id-repair-ui.md.
2800
2711
  `Content ID ${contentId} is on more than one file (${sorted.map((p) => `"${p}"`).join(" and ")}), so this save was refused rather than risk overwriting or deleting the wrong one. An administrator needs to resolve the duplicate on the server before this entry can be saved.`
2801
2712
  );
2802
2713
  this.name = "DuplicateContentIdError";
@@ -2846,20 +2757,16 @@ var ContentStore = class {
2846
2757
  }
2847
2758
  /**
2848
2759
  * [SYNC-C1] Run a working-tree mutation under the branch's cross-host
2849
- * content-write lock (utils/content-write-lock.ts), on top of the
2850
- * in-process locks the callee takes for itself.
2760
+ * content-write lock (utils/content-write-lock.ts), on top of the in-process
2761
+ * locks the callee takes for itself. The in-process mutex serializes writers
2762
+ * inside ONE process and says nothing about the EC2 worker rebasing this same
2763
+ * tree on shared EFS, which destroys an in-flight save.
2851
2764
  *
2852
- * The in-process mutex serializes writers inside ONE process; it says
2853
- * nothing about the EC2 worker rebasing this same tree on shared EFS, which
2854
- * destroys an in-flight save (`checkout --theirs` overwrites it and the
2855
- * rebase then reports success; `rebase --abort` hard-resets it). This is the
2856
- * layer that actually excludes the two.
2765
+ * Reads deliberately do NOT take it: an extra EFS round-trip per read is not an
2766
+ * acceptable cost, and a rebase cannot destroy a read.
2857
2767
  *
2858
- * Reads deliberately do NOT take it -- an extra EFS round-trip per read is
2859
- * not an acceptable cost, and reads cannot be destroyed by a rebase.
2860
- *
2861
- * Acquisition order is always content lock -> `withLock`, never the reverse,
2862
- * so the two cannot deadlock.
2768
+ * Acquisition order is always content lock -> `withLock`, never the reverse, so
2769
+ * the two cannot deadlock.
2863
2770
  */
2864
2771
  async withContentWriteExclusion(fn) {
2865
2772
  try {
@@ -2875,20 +2782,18 @@ var ContentStore = class {
2875
2782
  *
2876
2783
  * Called (via content-index-registry) after in-process operations that change the
2877
2784
  * working tree underneath this store — git checkout/merge/rebase, the worker's
2878
- * rebase loop, and sync-core's content replacement. Without this, ID→path lookups
2879
- * keep resolving to pre-mutation paths and saves can target moved/deleted files.
2880
- *
2881
- * Cheap: only bumps a generation counter; the rebuild happens lazily.
2785
+ * rebase loop, sync-core's content replacement. Without it, ID→path lookups keep
2786
+ * resolving to pre-mutation paths and saves can target moved/deleted files.
2787
+ * Cheap: bumps a generation counter, the rebuild is lazy.
2882
2788
  */
2883
2789
  invalidateIndex() {
2884
2790
  this.indexGeneration++;
2885
2791
  }
2886
2792
  /**
2887
- * Get the ID index, ensuring it's loaded and current first.
2888
- * Loads lazily on first access and rebuilds after invalidateIndex(); repeated
2889
- * accesses with no intervening invalidation reuse the already-built index,
2890
- * except for a throttled probe of the on-disk generation marker that detects
2891
- * mutations made by OTHER processes sharing this root (e.g. on EFS).
2793
+ * The ID index, rebuilt first if stale. Loads lazily, rebuilds after
2794
+ * invalidateIndex(), and otherwise reuses the built index apart from a throttled
2795
+ * probe of the on-disk generation marker, which detects mutations by OTHER
2796
+ * processes sharing this root (e.g. on EFS).
2892
2797
  */
2893
2798
  async idIndex() {
2894
2799
  if (this.loadedIndexGeneration === this.indexGeneration && this.shouldProbeDiskGeneration()) {
@@ -2936,14 +2841,12 @@ var ContentStore = class {
2936
2841
  return true;
2937
2842
  }
2938
2843
  /**
2939
- * After one of this store's own successful mutations: publish the change to
2940
- * other processes (bump the on-disk marker) and adopt the written token,
2941
- * since the in-memory index was already updated incrementally — avoiding a
2942
- * pointless self-rescan on the next probe. Adoption is skipped unless the
2943
- * index is quiescent and `updatedIndex` is still the live instance: if a
2944
- * rebuild raced with the mutation, its scan may predate our file change, so
2945
- * we leave the recorded token older and let the next probe observe our bump
2946
- * and trigger the healing rebuild.
2844
+ * After one of this store's own successful mutations: publish it to other
2845
+ * processes (bump the on-disk marker) and adopt the written token, since the
2846
+ * in-memory index was already updated incrementally. Adoption is skipped unless
2847
+ * the index is quiescent and `updatedIndex` is still the live instance: a rebuild
2848
+ * that raced the mutation may have scanned before our file change, so leaving the
2849
+ * recorded token older lets the next probe observe our bump and heal.
2947
2850
  */
2948
2851
  async recordOwnMutation(updatedIndex) {
2949
2852
  const token = await bumpContentIndexGeneration(this.root);
@@ -2952,16 +2855,14 @@ var ContentStore = class {
2952
2855
  }
2953
2856
  }
2954
2857
  /**
2955
- * Backstop for the residual staleness windows of the cross-process marker
2956
- * (NFS attribute caching, probe throttle, self-adoption — see
2957
- * content-index-generation.ts): force one rebuild in response to a
2958
- * suspicious lookup (an ID miss, or an index hit whose file is gone), but
2959
- * throttle how often a caller can force one. Time-boxed so genuinely
2960
- * dangling IDs cost at most one rescan per window. Returns true if a
2961
- * refresh was performed by THIS call; callers that lose the throttle race
2962
- * still benefit — idIndex() dedupes concurrent builds, so a caller that won
2963
- * the race rebuilds the index for everyone, and callers should still retry
2964
- * their lookup against the live index regardless of this return value.
2858
+ * Backstop for the residual staleness windows of the cross-process marker (NFS
2859
+ * attribute caching, probe throttle, self-adoption — see
2860
+ * content-index-generation.ts): force one rebuild in response to a suspicious
2861
+ * lookup (an ID miss, or an index hit whose file is gone), throttled so a
2862
+ * genuinely dangling ID costs at most one rescan per window. Returns true only if
2863
+ * THIS call refreshed; a caller that loses the throttle race must still retry its
2864
+ * lookup, since idIndex() dedupes concurrent builds and the winner rebuilds for
2865
+ * everyone.
2965
2866
  */
2966
2867
  async refreshIndexForSuspiciousLookup() {
2967
2868
  const now = Date.now();
@@ -2973,13 +2874,12 @@ var ContentStore = class {
2973
2874
  return true;
2974
2875
  }
2975
2876
  /**
2976
- * Every file in `dir` whose filename embeds `id`, as root-relative paths
2977
- * (empty when there is no such file, or no such dir).
2877
+ * Every file in `dir` whose filename embeds `id`, as root-relative paths (empty
2878
+ * when there is no such file, or no such dir).
2978
2879
  *
2979
- * [F1] Returns ALL matches, not the first: more than one match is a
2980
- * duplicate-ID pair, and callers must be able to tell that apart from a
2981
- * clean single hit rather than silently picking whichever one `readdir()`
2982
- * happened to yield first.
2880
+ * [F1] Returns ALL matches, not the first: more than one match is a duplicate-ID
2881
+ * pair, and callers must be able to tell that apart from a clean single hit rather
2882
+ * than silently picking whichever one `readdir()` happened to yield first.
2983
2883
  */
2984
2884
  async findEntryPathsById(dir, id) {
2985
2885
  let entries;
@@ -3000,10 +2900,6 @@ var ContentStore = class {
3000
2900
  }
3001
2901
  return matches.sort();
3002
2902
  }
3003
- /**
3004
- * Get all schema items for iteration.
3005
- * Used internally by ReferenceResolver for path matching.
3006
- */
3007
2903
  getSchemaItems() {
3008
2904
  return this.schemaIndex.values();
3009
2905
  }
@@ -3019,17 +2915,16 @@ var ContentStore = class {
3019
2915
  * Is this path a COLLECTION schema item?
3020
2916
  *
3021
2917
  * The non-throwing form of `assertCollection`, reading the same `schemaIndex` -- which is the
3022
- * point. A caller that gates on this cannot disagree with what `buildPaths` will then do: the
3023
- * Map is last-wins, so where a subcollection's path collides with a parent's entry-type name
3024
- * both this and `buildPaths` see the collection. A `find` over the flat schema LIST is
3025
- * first-wins and would not.
2918
+ * point: a caller that gates on this cannot disagree with what `buildPaths` will then do,
2919
+ * because the Map is last-wins where a subcollection's path collides with a parent's
2920
+ * entry-type name. A `find` over the flat schema LIST is first-wins and would not.
3026
2921
  *
3027
- * Type-only, deliberately -- `resolvePath` additionally requires `entries`, but a collection
3028
- * with subcollections and no entries of its own is legal, and mirroring that stricter test here
3029
- * would reject something `buildPaths` accepts.
2922
+ * Type-only, deliberately: `resolvePath` additionally requires `entries`, but a collection
2923
+ * with subcollections and no entries of its own is legal, and testing for that here would
2924
+ * reject something `buildPaths` accepts.
3030
2925
  *
3031
2926
  * Exists for `readByUrlPath`'s URL-addressability gate; see `ReadContentInput`'s
3032
- * `urlAddressableOnly` and the note on `buildPaths`' entry-type branch below.
2927
+ * `urlAddressableOnly`.
3033
2928
  */
3034
2929
  isCollectionPath(collectionPath) {
3035
2930
  return this.schemaIndex.get(normalizeFilesystemPath(collectionPath))?.type === "collection";
@@ -3037,23 +2932,20 @@ var ContentStore = class {
3037
2932
  /**
3038
2933
  * Does this collection declare `entryTypeName` in its `entries` config?
3039
2934
  *
3040
- * Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries` decides
3041
- * whether a file on disk is one of the collection's entries at all. `buildPaths`' own directory
3042
- * scan deliberately does NOT check this -- it matches on slug alone, so that an entry whose type
3043
- * was renamed out of the schema stays findable and therefore still editable, renameable and
3044
- * deletable. Only URL resolution consults this, so what enumeration hides is not served.
2935
+ * Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries`
2936
+ * decides whether a file on disk is one of the collection's entries at all. `buildPaths`'
2937
+ * own directory scan deliberately does NOT check this -- it matches on slug alone, so an
2938
+ * entry whose type was renamed out of the schema stays findable and therefore editable,
2939
+ * renameable and deletable. Only URL resolution consults this, so what enumeration hides is
2940
+ * not served.
3045
2941
  *
3046
- * Returns FALSE when the collection declares no `entries` at all, which is stricter than
3047
- * `parseTypedFilename`'s own `if (entryTypes && ...)` guard and deliberately so: the enumerating
3048
- * surface is not `parseTypedFilename`, it is `listCollectionEntries`, and that returns `[]`
3049
- * outright for a collection with no `entries`. A collections-only container therefore publishes
3050
- * nothing, so a URL read that resolved a file sitting in one would be answering where nothing is
3051
- * advertised -- the exact disagreement this predicate exists to close. Such a file cannot have
3052
- * been created by the CMS (there is no entry type to create it as); it arrived by hand, by merge
3053
- * or by retrofit, and it is invisible to the sitemap and to static params either way.
2942
+ * Returns FALSE when the collection declares no `entries` at all -- stricter than
2943
+ * `parseTypedFilename`'s own guard, and deliberately so: the enumerating surface is
2944
+ * `listCollectionEntries`, which returns `[]` outright for such a collection, so a URL read
2945
+ * resolving a file sitting in one would answer where nothing is advertised.
3054
2946
  *
3055
- * Returns true for a path that is not a collection at all, because that is rule 1's question,
3056
- * not this one's -- and under `urlAddressableOnly` rule 1 has already rejected it.
2947
+ * Returns true for a path that is not a collection at all, because that is rule 1's
2948
+ * question, and under `urlAddressableOnly` rule 1 has already rejected it.
3057
2949
  */
3058
2950
  declaresEntryType(collectionPath, entryTypeName) {
3059
2951
  const item = this.schemaIndex.get(normalizeFilesystemPath(collectionPath));
@@ -3071,25 +2963,20 @@ var ContentStore = class {
3071
2963
  return item;
3072
2964
  }
3073
2965
  /**
3074
- * Lock key for an existing entry, addressed by its permanent content ID
3075
- * (stable across renames -- the whole point of this locking scheme; see
3076
- * .claude/future-tasks/resolved/content-store-lock-key.md). Namespaced by store
3077
- * root: the same content ID exists in every clone of a branch, and one
3078
- * process can hold ContentStore instances on several clones (dev
3079
- * workspace roots, prod branch clones) at once, so the root prefix keeps
3080
- * the keyspace scoped per-store. It also keeps this ID keyspace disjoint
3081
- * from other modules' raw-path-keyed locks sharing the same withLock()
3082
- * map (comment-store, branch-metadata, etc).
2966
+ * Lock key for an existing entry, addressed by its permanent content ID --
2967
+ * stable across renames, which is the whole point of this locking scheme.
2968
+ * Namespaced by store root: the same content ID exists in every clone of a
2969
+ * branch and one process can hold stores on several clones at once, so the
2970
+ * prefix scopes the keyspace per store and keeps it disjoint from other
2971
+ * modules' raw-path-keyed locks sharing the same withLock() map.
3083
2972
  */
3084
2973
  idLockKey(id) {
3085
2974
  return `${this.root}:id:${id}`;
3086
2975
  }
3087
2976
  /**
3088
- * Stable collection+slug identifier for lock-key namespacing. Mirrors the
3089
- * schemaItem resolution buildPaths() performs for entry-type delegation:
3090
- * an entry-type item delegates to its parent collection with the slug
3091
- * defaulting to the entry type's own name (see buildPaths()'s
3092
- * `schemaItem.type === 'entry-type'` branch).
2977
+ * Stable collection+slug identifier for lock-key namespacing. Mirrors
2978
+ * buildPaths()' entry-type delegation: an entry-type item delegates to its
2979
+ * parent collection with the slug defaulting to the entry type's own name.
3093
2980
  */
3094
2981
  collectionSlugKey(schemaItem, slug) {
3095
2982
  if (schemaItem.type === "entry-type") {
@@ -3099,27 +2986,24 @@ var ContentStore = class {
3099
2986
  }
3100
2987
  /**
3101
2988
  * Lock key for a not-yet-existing entry (a create). Serializes concurrent
3102
- * same-slug creates WITHIN this process: the second call's in-lock
3103
- * buildPaths() re-resolution will find the first call's just-written file
3104
- * and fold in as an edit (see write()). A concurrent create racing from a
3105
- * DIFFERENT process is NOT covered by this in-process mutex -- accepted
3106
- * per the epic's design review: each writer mints its own fresh ID and
3107
- * writes its own distinct filename, so both writes succeed and the result
3108
- * is two same-slug files with different IDs (a slug-uniqueness violation
3109
- * surfaced on the next listing/lookup), never a duplicate-ID collision
3110
- * that would poison index rebuilds.
2989
+ * same-slug creates WITHIN this process: the second call's in-lock buildPaths()
2990
+ * re-resolution finds the first call's just-written file and folds in as an edit.
2991
+ *
2992
+ * A create racing from a DIFFERENT process is NOT covered by this in-process
2993
+ * mutex, and that is accepted: each writer mints its own fresh ID and filename, so
2994
+ * both writes succeed and the result is two same-slug files with different IDs (a
2995
+ * slug-uniqueness violation surfaced on the next listing/lookup), never a
2996
+ * duplicate-ID collision that would poison index rebuilds.
3111
2997
  */
3112
2998
  createLockKey(schemaItem, slug) {
3113
2999
  return `${this.root}:create:${this.collectionSlugKey(schemaItem, slug)}`;
3114
3000
  }
3115
3001
  /**
3116
- * Lock key for a write()/delete() pre-pass classification. Existing
3117
- * entries lock on their content ID (idLockKey()). Legacy entries with no
3118
- * embedded ID (pre-ID-era filenames -- renameEntry() already refuses to
3119
- * touch these, via its four-part filename check, so there is no rename
3120
- * race to guard against for them) fall back to the physical path, matching
3121
- * this store's original locking behavior for that narrow case. Not-yet-
3122
- * existing entries lock on a per-slug create-key (createLockKey()).
3002
+ * Lock key for a write()/delete() pre-pass classification: an existing entry
3003
+ * locks on its content ID (idLockKey()), a not-yet-existing one on a per-slug
3004
+ * create-key (createLockKey()). An entry with no embedded ID falls back to its
3005
+ * physical path -- renameEntry() refuses to touch those (its four-part filename
3006
+ * check), so they have no rename race to guard against.
3123
3007
  */
3124
3008
  entryLockKey(schemaItem, slug, prePass) {
3125
3009
  if (!prePass.existed)
@@ -3130,9 +3014,6 @@ var ContentStore = class {
3130
3014
  * Refuse a create/rename that would give a second entry a `urlPath` another entry already
3131
3015
  * holds. See url-collision.ts for the two shapes that count, and the legitimate
3132
3016
  * landing-page-beside-a-collection shape that deliberately does not.
3133
- *
3134
- * @param collectionDir Absolute physical directory the entry will live in.
3135
- * @param slug The entry's slug, as it will be written.
3136
3017
  */
3137
3018
  async assertUrlPathAvailable(collectionDir, slug) {
3138
3019
  const claimant = await findUrlPathClaimant({
@@ -3147,19 +3028,15 @@ var ContentStore = class {
3147
3028
  throw new UrlPathConflictError(message, claimant.physicalPath);
3148
3029
  }
3149
3030
  /**
3150
- * Build absolute and relative paths with security validation.
3151
- * All entries use the unified filename pattern: {type}.{slug}.{id}.{ext}
3031
+ * Build absolute and relative paths for an entry, filename pattern
3032
+ * `{type}.{slug}.{id}.{ext}`. `options.existingId` addresses a known entry; without it a
3033
+ * new ID is generated. `options.entryTypeName` picks among a collection's entry types,
3034
+ * defaulting to its default one.
3152
3035
  *
3153
- * SECURITY BOUNDARY: This method prevents path traversal attacks by:
3154
- * 1. Validating that resolved paths stay within the content root
3155
- * 2. Checking slugs for malicious patterns (via validateSlug)
3156
- * 3. Using path.resolve to normalize paths before validation
3157
- *
3158
- * This validation is performed BEFORE file I/O in resolveDocumentPath(),
3159
- * ensuring permission checks happen before any file system access.
3160
- *
3161
- * @param options.existingId - Optional ID to use (for edits). If not provided, generates new ID.
3162
- * @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
3036
+ * SECURITY BOUNDARY against path traversal: resolved paths must stay inside the content
3037
+ * root, slugs go through `validateSlug`, and everything is `path.resolve`d before those
3038
+ * checks. All of it runs BEFORE any file I/O in resolveDocumentPath(), so permission checks
3039
+ * happen before filesystem access.
3163
3040
  */
3164
3041
  async buildPaths(schemaItem, slug, options = {}) {
3165
3042
  const rootWithSep = this.root.endsWith(path11.sep) ? this.root : `${this.root}${path11.sep}`;
@@ -3246,10 +3123,7 @@ var ContentStore = class {
3246
3123
  }
3247
3124
  throw new ContentStoreError("Invalid schema item type", "VALIDATION");
3248
3125
  }
3249
- /**
3250
- * Path resolution: resolves a URL path to a schema item
3251
- * - Try as collection + slug (last segment = slug)
3252
- */
3126
+ /** Resolve path segments to a collection schema item plus the trailing slug. */
3253
3127
  resolvePath(pathSegments) {
3254
3128
  if (pathSegments.length === 0) {
3255
3129
  throw new ContentStoreError("Empty path", "VALIDATION");
@@ -3319,15 +3193,11 @@ var ContentStore = class {
3319
3193
  collection: schemaItem.logicalPath,
3320
3194
  collectionName: schemaItem.name,
3321
3195
  format,
3322
- // Copy, never the object gray-matter hands back: its cache is
3323
- // process-global and keyed by file content, so every caller parsing the
3324
- // same bytes gets the SAME `data` instance (the hazard content-listing.ts
3325
- // documents at length). Assigning it straight into `doc.data` was safe
3326
- // only by accident -- `resolveReferencesInData` spreads its input, so the
3327
- // shared object got replaced a few lines down. But that only happens when
3328
- // resolution runs: with `resolveReferences: false` the caller was handed
3329
- // the shared instance itself, and one mutation would poison every later
3330
- // parse of those bytes.
3196
+ // Copy, never the object gray-matter hands back: its cache is process-global and
3197
+ // keyed by file content, so every caller parsing the same bytes gets the SAME `data`
3198
+ // instance (the hazard content-listing.ts documents at length). With
3199
+ // `resolveReferences: false` the caller would otherwise be handed that shared
3200
+ // instance, and one mutation would poison every later parse of those bytes.
3331
3201
  data: { ...parsed.data ?? {} },
3332
3202
  body: parsed.content,
3333
3203
  bodyFieldName: findBodyFieldName(fields),
@@ -3532,7 +3402,6 @@ var ContentStore = class {
3532
3402
  }
3533
3403
  }
3534
3404
  /**
3535
- * Get the ID for an entry given its collection and slug.
3536
3405
  * Returns null if no ID exists yet.
3537
3406
  */
3538
3407
  async getIdForEntry(collectionPath, slug) {
@@ -3558,20 +3427,16 @@ var ContentStore = class {
3558
3427
  }
3559
3428
  }
3560
3429
  /**
3561
- * Resolve the actual on-disk entry type of an existing collection entry, by
3562
- * inspecting its filename directly. Entry filenames embed their type
3563
- * (`{type}.{slug}.{id}.{ext}`) and it is immutable after creation — see the
3564
- * existing-file lookup in buildPaths(), which this mirrors. Returns
3565
- * undefined if no entry exists yet at this slug.
3430
+ * The on-disk entry type of an existing collection entry, read from its filename
3431
+ * (`{type}.{slug}.{id}.{ext}`), which is immutable after creation -- mirroring the
3432
+ * existing-file lookup in buildPaths(). Undefined if no entry exists yet at this slug.
3566
3433
  *
3567
- * Used at the API write boundary (api/content.ts) to validate an existing
3568
- * entry's payload against ITS real schema rather than a caller-supplied (or
3569
- * omitted/spoofed) `entryType` param — ContentStore.write() preserves the
3570
- * on-disk type regardless of what's requested, so validation must agree
3571
- * with what will actually be written (post-review M2).
3434
+ * The API write boundary (api/content.ts) validates an existing entry's payload against ITS
3435
+ * real schema rather than a caller-supplied (or omitted, or spoofed) `entryType` param:
3436
+ * ContentStore.write() preserves the on-disk type regardless of what is requested, so
3437
+ * validation must agree with what will actually be written.
3572
3438
  *
3573
- * Cheap: one buildPaths() resolution plus a stat, no file content is read —
3574
- * same cost class as documentExists().
3439
+ * Cheap: one buildPaths() resolution plus a stat, no file content is read.
3575
3440
  */
3576
3441
  async getExistingEntryType(collectionPath, slug = "") {
3577
3442
  const schemaItem = this.assertCollection(collectionPath);
@@ -3579,9 +3444,9 @@ var ContentStore = class {
3579
3444
  return await filePathExists(absolutePath) ? entryTypeName : void 0;
3580
3445
  }
3581
3446
  /**
3582
- * Count existing entries of a given entry type in a collection, by filename
3583
- * (entry filenames embed their type: `{type}.{slug}.{id}.{ext}`). Used to
3584
- * enforce `EntryTypeConfig.maxItems` server-side at the create boundary.
3447
+ * Count existing entries of a given entry type in a collection, by filename (entry
3448
+ * filenames embed their type: `{type}.{slug}.{id}.{ext}`). Enforces
3449
+ * `EntryTypeConfig.maxItems` server-side at the create boundary.
3585
3450
  */
3586
3451
  async countEntriesOfType(collectionPath, entryTypeName) {
3587
3452
  this.assertCollection(collectionPath);
@@ -3592,21 +3457,15 @@ var ContentStore = class {
3592
3457
  return entries.filter((entry) => !entry.isDirectory() && extractEntryTypeFromFilename(entry.name) === entryTypeName).length;
3593
3458
  }
3594
3459
  /**
3595
- * Test-only seam: awaited right after the pre-pass buildPaths() resolves
3596
- * in delete() and renameEntry(), before the lock key is used to acquire
3597
- * anything. No-op in production. A test subclass can override this to
3598
- * inject a controlled pause in that exact window, letting a test
3599
- * deterministically run OTHER mutations (which don't hold this call's
3600
- * not-yet-acquired lock) to completion before this call proceeds to
3601
- * acquire its (possibly now-stale) lock key -- see the "Deterministic
3602
- * interleavings" testing pattern in docs/concurrency.md and
3603
- * branch-registry.test.ts's `BlockingRegistry` for the same idiom.
3460
+ * Test-only seam: awaited right after the pre-pass buildPaths() resolves in delete() and
3461
+ * renameEntry(), while neither has yet acquired a lock. No-op in production. A test subclass
3462
+ * overrides it to pause in that exact window and run OTHER mutations to completion, making
3463
+ * the stale-lock-key path deterministic -- see the "Deterministic interleavings" testing
3464
+ * pattern in docs/concurrency.md and branch-registry.test.ts's `BlockingRegistry`.
3604
3465
  */
3605
3466
  async afterPrePassForTesting() {
3606
3467
  }
3607
- /**
3608
- * Delete an entry and remove it from the index.
3609
- */
3468
+ /** Delete an entry and remove it from the index. */
3610
3469
  async delete(collectionPath, slug) {
3611
3470
  await this.idIndex();
3612
3471
  const collection = this.assertCollection(collectionPath);
@@ -3646,12 +3505,6 @@ var ContentStore = class {
3646
3505
  /**
3647
3506
  * Rename an entry by changing its slug (middle segment of filename).
3648
3507
  * Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
3649
- *
3650
- * @param collectionPath - Logical path to the collection
3651
- * @param currentSlug - Current slug of the entry
3652
- * @param newSlug - New slug (must be unique within collection)
3653
- * @returns Object with new logical path
3654
- * @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
3655
3508
  */
3656
3509
  async renameEntry(collectionPath, currentSlug, newSlug) {
3657
3510
  await this.idIndex();
@@ -3744,27 +3597,18 @@ var ContentStore = class {
3744
3597
  throw new ContentConflictError();
3745
3598
  });
3746
3599
  }
3747
- /**
3748
- * List all entries in a collection tree (including subcollections).
3749
- * For example, passing 'content/data-catalog' returns entries from
3750
- * 'content/data-catalog', 'content/data-catalog/partner-a', etc.
3751
- * Returns array of entry metadata (relativePath, collection, slug).
3752
- * Returns empty array if the collection doesn't exist.
3753
- */
3754
3600
  /**
3755
3601
  * Resolve a schema collection referenced by name or logical path.
3756
3602
  *
3757
- * Accepts either a full logical path ("content/authors") or a bare
3758
- * collection name ("authors") — the contract reference fields use for
3759
- * `collections: [...]` (see README). This is the single normalization
3760
- * point shared by reference-option loading and reference validation so
3761
- * the dropdown and the write boundary can never disagree.
3603
+ * Accepts a full logical path ("content/authors") or a bare collection name ("authors") --
3604
+ * the contract reference fields use for `collections: [...]` (see README). The single
3605
+ * normalization point shared by reference-option loading and reference validation, so the
3606
+ * dropdown and the write boundary can never disagree.
3762
3607
  *
3763
- * CAVEAT: the bare-name fallback matches on the LAST path segment and
3764
- * returns the first hit in schema order — with two collections sharing a
3765
- * leaf name (e.g. content/blog/posts and content/news/posts), a bare
3766
- * 'posts' is ambiguous. Use the full logical path in schemas that nest
3767
- * same-named collections.
3608
+ * CAVEAT: the bare-name fallback matches on the LAST path segment and returns the first hit
3609
+ * in schema order, so with two collections sharing a leaf name (content/blog/posts,
3610
+ * content/news/posts) a bare 'posts' is ambiguous. Use the full logical path in schemas that
3611
+ * nest same-named collections.
3768
3612
  */
3769
3613
  resolveCollectionItem(collectionPath) {
3770
3614
  const normalized = normalizeFilesystemPath(collectionPath);
@@ -3782,6 +3626,11 @@ var ContentStore = class {
3782
3626
  }
3783
3627
  return item && item.type === "collection" ? item : void 0;
3784
3628
  }
3629
+ /**
3630
+ * Every entry in a collection tree, including subcollections: 'content/data-catalog'
3631
+ * returns entries from it and from 'content/data-catalog/partner-a' and so on. Empty when
3632
+ * the collection does not exist.
3633
+ */
3785
3634
  async getCollectionEntryPaths(collectionPath) {
3786
3635
  const idIndex = await this.idIndex();
3787
3636
  const collection = this.resolveCollectionItem(collectionPath);
@@ -3804,29 +3653,21 @@ var ContentStore = class {
3804
3653
  /**
3805
3654
  * Resolve every `reference` field in `data` against `fields`, returning a copy.
3806
3655
  *
3807
- * This is what `read()` applies automatically (unless `resolveReferences: false`), exposed
3808
- * so BATCH surfaces can opt into the same resolution without duplicating the walk: the
3809
- * listing primitives in content-listing.ts and content-tree.ts read entry files straight
3810
- * off disk and never touch this store, so before this existed a `reference` field reached
3811
- * `listEntries()`/`buildContentTree()` callers as a bare id string or `null`.
3656
+ * What `read()` applies automatically (unless `resolveReferences: false`), exposed so BATCH
3657
+ * surfaces can opt into the same resolution without duplicating the walk: the listing
3658
+ * primitives in content-listing.ts and content-tree.ts read entry files straight off disk
3659
+ * and never touch this store. Pass one {@link ReferenceResolveCache} across a whole batch so
3660
+ * a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
3661
+ * unmemoized behavior.
3812
3662
  *
3813
- * Pass one {@link ReferenceResolveCache} across a whole batch so a target referenced by
3814
- * many entries is read once rather than once per referencing entry. Omit `cache` and the
3815
- * behavior is exactly what `read()` has always done, unmemoized.
3816
- *
3817
- * **Path ACLs are not consulted for the targets.** Resolution goes through this store's
3818
- * own `read()`, below the per-entry permission check in content-reader.ts — so a resolved
3819
- * target may be an entry the caller could not `read()` directly. That is pre-existing
3820
- * `read()` behavior, matched here on purpose so one rule covers both; see the
3821
- * `resolveReferences` option in content-listing.ts for the note adopters see.
3663
+ * **Path ACLs are not consulted for the targets.** Resolution goes through this store's own
3664
+ * `read()`, below the per-entry permission check in content-reader.ts, so a resolved target
3665
+ * may be an entry the caller could not `read()` directly -- pre-existing `read()` behavior,
3666
+ * matched here on purpose so one rule covers both.
3822
3667
  */
3823
3668
  async resolveReferences(data, fields, cache) {
3824
3669
  return this.resolveReferencesInData(data, fields, cache);
3825
3670
  }
3826
- /**
3827
- * Recursively resolve reference fields in data.
3828
- * This traverses objects, arrays, and blocks to find and resolve all reference fields.
3829
- */
3830
3671
  async resolveReferencesInData(data, fields, cache) {
3831
3672
  const resolved = { ...data };
3832
3673
  const idIndex = await this.idIndex();
@@ -3872,43 +3713,28 @@ var ContentStore = class {
3872
3713
  return resolved;
3873
3714
  }
3874
3715
  /**
3875
- * Resolve a single reference ID to full entry data, memoized when a batch cache is
3876
- * supplied. Without a cache this is a straight pass-through to the uncached path, so
3877
- * `read()` behaves exactly as it always has.
3716
+ * Resolve a single reference ID to full entry data, memoized when a batch cache is supplied.
3717
+ * Without a cache this is a straight pass-through, so `read()` behaves as it always has.
3878
3718
  *
3879
- * Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo
3880
- * would hand one shared object to all 40 entries referencing the same block — a mutation
3881
- * in one caller's `extract` (truncating a body for a search index, deleting a field) would
3882
- * silently rewrite it for every sibling, and both `list: true` elements of `[id, id]` would
3883
- * be the same instance. The copy is what keeps the cache a pure performance optimization
3884
- * instead of a semantic change.
3719
+ * Every occurrence gets its OWN deep copy, even on a cache hit. Without that, the memo would
3720
+ * hand one shared object to all 40 entries referencing the same block -- a mutation in one
3721
+ * caller's `extract` would silently rewrite it for every sibling, and both `list: true`
3722
+ * elements of `[id, id]` would be the same instance. The copy is what keeps the cache a pure
3723
+ * performance optimization instead of a semantic change.
3885
3724
  *
3886
- * Note the uncached path is NOT the clean baseline it looks like. For a json/yaml target it
3887
- * genuinely reparses fresh per occurrence, but for md/mdx gray-matter serves `data` from a
3888
- * process-global content-keyed cache, so `resolveSingleReferenceOnce`'s top-level spread
3889
- * severs exactly one level and NESTED frontmatter objects alias across occurrences, calls and
3890
- * requests. That is pre-existing `read()` behavior and out of scope here — deliberately, since
3891
- * touching it would change `read()` — but it means the cached path is the safer of the two,
3892
- * not a relaxation of a guarantee the uncached one provides. See
3893
- * `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
3725
+ * The uncached path is NOT the clean baseline it looks like: for md/mdx, gray-matter serves
3726
+ * `data` from a process-global content-keyed cache, so `resolveSingleReferenceOnce`'s
3727
+ * top-level spread severs exactly one level and NESTED frontmatter objects alias across
3728
+ * occurrences, calls and requests. Pre-existing `read()` behavior, deliberately out of scope
3729
+ * here; see `.claude/future-tasks/graymatter-cache-shared-frontmatter.md`.
3894
3730
  *
3895
- * What the copy costs, measured rather than assumed (2000 occurrences of one target, local
3896
- * disk, so every read hits the page cache — the friendliest possible case for NOT caching):
3897
- * a snippet-sized target is ~63x cheaper to clone than to re-read-and-parse, while a 265KB
3898
- * JSON target is ~0.8x, i.e. cloning is marginally SLOWER than reparsing it. So the win is
3899
- * large in the case this exists for and roughly a wash at the pathological end, never a
3900
- * blow-up. Two things keep the bad end narrow: an md/mdx target resolves to its FRONTMATTER
3901
- * only *unless the field sets `includeBody`* (`read()` puts the body on `doc.body`, which
3902
- * `resolveSingleReferenceOnce` spreads in only for an embedding field), so by default body
3903
- * size is irrelevant no matter how long the document and only a genuinely huge JSON/YAML
3904
- * target reaches the wash. **`includeBody: true` is the case that CAN reach it on markdown**:
3905
- * the body then sits inside the memoized object and is cloned once per referencing entry, so
3906
- * a long document embedded by many pages pays that repeatedly — the reason `includeBody`
3907
- * exists as an opt-in per field rather than as resolution's default. And in the deployment this
3908
- * targets, content lives on EFS/NFS where the syscall the memo removes dominates parse and
3909
- * clone alike, which the local-disk numbers above understate badly. Correctness is the
3910
- * reason for the copy regardless; the numbers are here so nobody has to re-derive them
3911
- * before touching this.
3731
+ * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
3732
+ * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
3733
+ * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
3734
+ * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
3735
+ * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
3736
+ * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
3737
+ * cloned once per referencing entry.
3912
3738
  */
3913
3739
  resolveSingleReference(id, idIndex, includeBody, cache) {
3914
3740
  if (!cache)
@@ -3922,24 +3748,17 @@ var ContentStore = class {
3922
3748
  return pending.then((resolved) => resolved === null ? null : structuredClone(resolved));
3923
3749
  }
3924
3750
  /**
3925
- * Returns null if the reference is invalid or missing.
3926
- * Includes id, slug, and collection fields for debugging.
3751
+ * Returns null if the reference is invalid or missing. Includes id, slug and collection.
3927
3752
  *
3928
- * Suspicious results (ID missing from the index, or an index hit whose file
3929
- * is gone) trigger one forced index refresh and a retry — self-healing for
3930
- * mutations by other processes inside the marker's residual windows. The
3931
- * forced refresh itself is throttled (see refreshIndexForSuspiciousLookup),
3932
- * but the retry against the live index always runs regardless of whether
3933
- * this call won the throttle race: when a `list: true` reference array is
3934
- * resolved via Promise.all, every miss shares the same stale snapshot, and
3935
- * idIndex() dedupes concurrent builds — so a sibling call that wins the
3936
- * throttle and rebuilds heals every other miss in the same batch, not just
3937
- * the first.
3753
+ * A suspicious result (ID missing from the index, or an index hit whose file is gone)
3754
+ * triggers one forced index refresh and a retry — self-healing for mutations by other
3755
+ * processes inside the marker's residual windows. The forced refresh is itself throttled
3756
+ * (see refreshIndexForSuspiciousLookup) but the retry against the live index always runs:
3757
+ * every miss in a `list: true` Promise.all shares one stale snapshot, and idIndex() dedupes
3758
+ * concurrent builds, so a sibling that wins the throttle heals the whole batch.
3938
3759
  *
3939
- * A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every
3940
- * distinct id still runs the full refresh-and-retry above. What a cache changes
3941
- * is only that repeats of the SAME id in one batch share that one attempt's
3942
- * outcome instead of each getting an independent throw of the dice.
3760
+ * A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
3761
+ * still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
3943
3762
  */
3944
3763
  async resolveSingleReferenceUncached(id, idIndex, includeBody) {
3945
3764
  const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
@@ -4987,15 +4806,12 @@ var BranchRegistry = class {
4987
4806
  return await this.regenerate();
4988
4807
  }
4989
4808
  /**
4990
- * Returns a single branch by name. Uses cache if available.
4809
+ * Returns a single branch by name, from the cached snapshot when it is fresh.
4991
4810
  *
4992
- * Suspicious-miss backstop: if `name` isn't in the (possibly cached) list,
4993
- * that's a signal the snapshot may be stale in the specific direction where
4994
- * a branch that now exists is missing from it - force one fresh
4995
- * regeneration and re-search before giving up, throttled per instance so a
4996
- * genuinely-absent branch costs at most one extra scan per throttle window.
4997
- * This bounds durable window-E staleness for the "branch exists but
4998
- * snapshot predates it" direction; see the class doc comment.
4811
+ * Suspicious-miss backstop: a `name` missing from the list is the signal that
4812
+ * the snapshot may predate a branch that now exists, so force one fresh
4813
+ * regeneration and re-search before giving up. Throttled per instance, so a
4814
+ * genuinely-absent branch costs at most one extra scan per window.
4999
4815
  */
5000
4816
  async get(name) {
5001
4817
  const branches = await this.list();
@@ -5015,14 +4831,11 @@ var BranchRegistry = class {
5015
4831
  return true;
5016
4832
  }
5017
4833
  /**
5018
- * Marks the cache as stale for every process sharing this root by bumping
5019
- * the cross-process generation marker, then eager-regenerates on this host
5020
- * (see class doc comment for why). The bump must succeed: a swallowed
5021
- * failure here would leave the registry stale indefinitely with no
5022
- * bounding backstop, unlike a failed eager regen below, which only forgoes
5023
- * closing window (E) early - the bump alone already restored correctness
5024
- * for every future reader. A failed eager regen therefore must not fail the
5025
- * caller's save/delete.
4834
+ * Marks the cache stale for every process sharing this root by bumping the
4835
+ * marker, then eager-regenerates on this host. The bump must succeed —
4836
+ * swallowing that failure leaves the registry stale indefinitely with no
4837
+ * backstop. A failed eager regen must NOT fail the caller's save/delete: the
4838
+ * bump alone already restored correctness for every future reader.
5026
4839
  */
5027
4840
  async invalidate() {
5028
4841
  await bumpResourceGeneration(this.root, RESOURCE2, { mustSucceed: true });
@@ -5040,9 +4853,8 @@ var BranchRegistry = class {
5040
4853
  /**
5041
4854
  * Scans branch directories and rebuilds the cache, deduping concurrent
5042
4855
  * callers on this instance to a single underlying scan. Never loops waiting
5043
- * for the snapshot's embedded token to match the marker - under a bump
5044
- * storm that could livelock; a caller that wants the very latest state
5045
- * after a concurrent bump should call list() again after this resolves.
4856
+ * for the embedded token to match the marker — that livelocks under a bump
4857
+ * storm; a caller wanting the very latest state calls list() again.
5046
4858
  */
5047
4859
  async regenerate() {
5048
4860
  if (this.regenInFlight)
@@ -5261,16 +5073,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5261
5073
  this.baseRoot = baseRoot;
5262
5074
  this.settleMs = options?.settleMs;
5263
5075
  }
5264
- /**
5265
- * Load branch metadata without requiring baseRoot.
5266
- * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
5267
- */
5076
+ /** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
5268
5077
  static async loadOnly(branchRoot) {
5269
5078
  return readBranchMetadataFile(branchRoot);
5270
5079
  }
5271
5080
  /**
5272
- * Get a BranchMetadataFileManager instance configured for registry invalidation.
5273
- * Use this in API handlers to ensure registry cache is invalidated on updates.
5081
+ * An instance wired for registry invalidation. API handlers use this, so the
5082
+ * registry cache is invalidated on update.
5274
5083
  */
5275
5084
  static get(branchRoot, baseRoot, options) {
5276
5085
  return new _BranchMetadataFileManager(branchRoot, baseRoot, options);
@@ -5289,18 +5098,16 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5289
5098
  }
5290
5099
  }
5291
5100
  /**
5292
- * Write branch.json via the shared OCC helper, with the schemaVersion
5293
- * default applied here (payload shaping stays branch-metadata's concern).
5101
+ * Write branch.json via the shared OCC helper, applying the schemaVersion
5102
+ * default here since payload shaping is branch-metadata's concern.
5294
5103
  *
5295
- * Throws the helper's raw {@link OccWriteConflictError} so the surrounding
5296
- * {@link withOccRetry} in save() recognizes and retries it; translation to
5297
- * the public `BranchMetadataConflictError` contract happens at the save()
5298
- * boundary, after retries are exhausted (translating earlier would make
5299
- * withOccRetry's default predicate miss it, since it only recognizes the
5300
- * raw error type).
5104
+ * Throws the helper's raw {@link OccWriteConflictError}, which is what the
5105
+ * surrounding {@link withOccRetry} in save() recognizes and retries;
5106
+ * translating to the public `BranchMetadataConflictError` earlier than the
5107
+ * save() boundary would make withOccRetry's predicate miss it.
5301
5108
  *
5302
- * branch-metadata historically writes WITH a trailing newline, unlike
5303
- * comment-store; `trailingNewline: true` preserves that.
5109
+ * branch.json is written WITH a trailing newline (`trailingNewline: true`),
5110
+ * unlike comments.json.
5304
5111
  */
5305
5112
  async write(meta, expectedVersion) {
5306
5113
  const payload = {
@@ -5314,28 +5121,22 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5314
5121
  });
5315
5122
  }
5316
5123
  /**
5317
- * Run a save cycle under the full lock + OCC-retry stack described in the
5318
- * class doc comment. A conflict that survives every retry surfaces as the
5319
- * public `BranchMetadataConflictError`.
5124
+ * Run a save cycle under the full lock + OCC-retry stack (class doc). A
5125
+ * conflict surviving every retry surfaces as `BranchMetadataConflictError`.
5320
5126
  *
5321
- * Guards against a phantom-resurrection race with branch deletion: a
5322
- * caller's branchContext can be resolved BEFORE a concurrent
5323
- * deleteBranchHandler removes the branch directory, but this save() call
5324
- * only reaches here (and its own mkdir({recursive:true}) inside write())
5325
- * AFTER the removal. Without this check, that mkdir would silently
5326
- * recreate `.canopy-meta/` (and this save would recreate branch.json from
5327
- * defaults) inside a directory tree that no longer exists anywhere else --
5328
- * a registry entry with no clone behind it. Checking BEFORE the lock stack
5329
- * (rather than after) fails fast without paying for a lock acquisition on
5330
- * a doomed save.
5127
+ * The stat guards a phantom-resurrection race with branch deletion: a
5128
+ * caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
5129
+ * removes the branch directory, while the save reaches here after it, and
5130
+ * write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
5131
+ * branch.json from defaults inside a tree nothing else refers to — a registry
5132
+ * entry with no clone behind it. It runs BEFORE the lock stack so a doomed
5133
+ * save fails fast instead of paying for a lock.
5331
5134
  *
5332
- * Residual window (accepted): a save that passes this check can still
5333
- * race a `rm` that starts moments later and is still mid-flight when this
5334
- * save's write lands, resurrecting the tree. Closing that fully would
5335
- * need a tombstone OUTSIDE the tree being removed -- the lockfile
5336
- * (`withOccFileLock`) this save takes next lives INSIDE `branchRoot`, so
5337
- * it cannot itself provide a wider guarantee than "the directory existed
5338
- * a moment ago."
5135
+ * Accepted residual window: a save that passes the check can still race a
5136
+ * `rm` that starts moments later and is mid-flight when the write lands.
5137
+ * Closing that needs a tombstone OUTSIDE the tree being removed, and the
5138
+ * lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
5139
+ * than "the directory existed a moment ago".
5339
5140
  */
5340
5141
  async save(incoming) {
5341
5142
  try {
@@ -5375,9 +5176,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5375
5176
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
5376
5177
  // Fork point is recorded once at creation; later saves must not move it
5377
5178
  baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
5378
- // Always stamped fresh — the spreads above would otherwise let
5379
- // the existing (creation-time) value win forever, freezing the
5380
- // timestamp the editor's Branches panel sorts and displays by
5179
+ // Always stamped fresh; the spreads above would otherwise let
5180
+ // the creation-time value win forever, freezing the timestamp
5181
+ // the editor's Branches panel sorts and displays by
5381
5182
  updatedAt: now
5382
5183
  }
5383
5184
  };
@@ -5395,9 +5196,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
5395
5196
  await this.invalidateRegistry();
5396
5197
  return saved;
5397
5198
  }
5398
- /**
5399
- * Invalidates the registry cache so next list() call regenerates from branch.json files.
5400
- */
5199
+ /** Invalidate the registry cache so the next list() regenerates. */
5401
5200
  async invalidateRegistry() {
5402
5201
  const registry2 = new BranchRegistry(this.baseRoot);
5403
5202
  await registry2.invalidate();
@@ -5543,17 +5342,11 @@ var GitManager = class _GitManager {
5543
5342
  log5.debug("git", "Clone complete");
5544
5343
  }
5545
5344
  /**
5546
- * Initializes a local bare git repository to simulate a remote for dev mode.
5547
- *
5548
- * This is idempotent - if the remote already exists, it will not be recreated.
5549
- *
5550
- * The remote is seeded with the current state of the baseBranch (e.g., 'main').
5551
- * When the remote already exists but is missing the requested baseBranch
5552
- * (dev-mode branch auto-detect makes this routine: any git branch created
5553
- * after the remote was first seeded), that branch is pushed from the source
5554
- * repo on demand. Branches that already exist in the remote are never
5555
- * updated here — the CMS pushes editor state into this remote, and a
5556
- * refresh from the source repo would clobber it.
5345
+ * Initializes a local bare git repository to simulate a remote for dev mode,
5346
+ * seeded with the current state of baseBranch. Idempotent: an existing remote
5347
+ * is never recreated, and a branch already in it is never refreshed from the
5348
+ * source repo — the CMS pushes editor state into this remote, so a refresh
5349
+ * would clobber it. A baseBranch the remote lacks is pushed on demand.
5557
5350
  *
5558
5351
  * @throws Error if not a git repo, no commits, or baseBranch doesn't exist
5559
5352
  */
@@ -5654,39 +5447,28 @@ var GitManager = class _GitManager {
5654
5447
  * EITHER the local-heads namespace (`refs/heads/<branch>`) or the GitHub
5655
5448
  * tracking namespace (`GITHUB_TRACKING_REF_PREFIX<branch>`).
5656
5449
  *
5657
- * Originally checked `refs/heads/*` only, for dev-mode's
5658
- * `ensureLocalSimulatedRemote` (a simulated remote never gets a tracking
5659
- * namespace, so that caller only ever needed the local-heads check).
5660
- * Generalized/promoted to public for api/branch.ts's create-time collision
5661
- * guard, which also needs the tracking namespace: `syncGit()` fetches
5662
- * GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
5663
- * directly (see that constant's doc comment above), and
5664
- * `reconcileTrackedBranches()` (worker/git-sync.ts) only
5665
- * non-destructively brings `refs/heads/*` toward what's tracked there — so
5666
- * a branch that another CanopyCMS deployment sharing this repo (or a
5667
- * direct push to GitHub) just created can sit in the tracking namespace
5668
- * for a while before, or without ever, gaining a local head here. Checking
5669
- * `refs/heads/*` alone would miss exactly the two-deployments-one-repo
5670
- * collision that guard exists to catch.
5450
+ * Both namespaces matter to api/branch.ts's create-time collision guard: a
5451
+ * branch another CanopyCMS deployment sharing this repo (or a direct push to
5452
+ * GitHub) just created sits in the tracking namespace before, or without
5453
+ * ever, gaining a local head here (see GITHUB_TRACKING_REF_PREFIX above), so
5454
+ * checking `refs/heads/*` alone would miss exactly the
5455
+ * two-deployments-one-repo collision that guard exists to catch.
5671
5456
  *
5672
5457
  * Runs git with an explicit `--git-dir` instead of a cwd inside the repo:
5673
5458
  * environments with `safe.bareRepository=explicit` (sandboxed/CI git setups)
5674
5459
  * refuse cwd-based discovery of bare repos but expressly allow `--git-dir`.
5675
- * A single `for-each-ref` call checks both candidate refs at once (no
5676
- * exception-based control flow — `for-each-ref` exits 0 whether or not
5677
- * either ref exists, same reason the old implementation used `branch
5678
- * --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
5679
- * task on stderr output, and `--quiet` suppresses exactly that).
5680
- * `--end-of-options` guards the ref-name positionals the same way
5681
- * `push` below does, since `branch` here can be a sanitized but otherwise
5682
- * caller-influenced string.
5460
+ * A single `for-each-ref` call checks both candidate refs at once, without
5461
+ * exception-based control flow — it exits 0 whether or not either ref
5462
+ * exists, whereas `rev-parse --verify --quiet` suppresses the stderr output
5463
+ * that is the only thing simple-git fails a task on. `--end-of-options`
5464
+ * guards the ref-name positionals the same way `push` below does, since
5465
+ * `branch` here is sanitized but otherwise caller-influenced.
5683
5466
  *
5684
5467
  * A failure here therefore means the remote itself is unreadable and is
5685
5468
  * surfaced, NOT treated as "branch absent" — that would route
5686
5469
  * `ensureLocalSimulatedRemote` to the push path against a repo it couldn't
5687
5470
  * even read, and would silently skip api/branch.ts's collision check
5688
- * instead of letting that caller distinguish "unreadable" from "absent"
5689
- * and decide what to do.
5471
+ * instead of letting that caller distinguish "unreadable" from "absent".
5690
5472
  */
5691
5473
  static async bareRemoteHasBranch(remotePath, branch, options = {}) {
5692
5474
  let output;
@@ -5713,36 +5495,30 @@ var GitManager = class _GitManager {
5713
5495
  * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
5714
5496
  * no-op (not an error) when the ref doesn't exist.
5715
5497
  *
5716
- * This is the "explicit path" for removing a deleted branch's local head
5717
- * that the sync loop deliberately is not (see GITHUB_TRACKING_REF_PREFIX's
5718
- * doc comment: reconcileTrackedBranches never deletes a head). Called by
5719
- * api/branch.ts's deleteBranchHandler: without it, a deleted branch's head
5720
- * lives in `remote.git` forever, and the ordinary create -> publish ->
5721
- * squash-merge -> delete -> reuse-the-name cycle then has the REUSED
5722
- * branch's first publish rejected non-fast-forward against the stale head
5723
- * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip
5724
- * is not an ancestor of the new branch) -- a permanent, misleading 409.
5725
- * Worse, a retried submit skips the local push (clean tree) and enqueues
5726
- * the worker push of the STALE head, resurrecting the deleted branch's
5727
- * content on GitHub as an apparent success.
5498
+ * The explicit path for removing a deleted branch's local head, which the
5499
+ * sync loop deliberately is not (reconcileTrackedBranches never deletes a
5500
+ * head — see GITHUB_TRACKING_REF_PREFIX). Called by api/branch.ts's
5501
+ * deleteBranchHandler: a head left in `remote.git` forever makes the
5502
+ * create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
5503
+ * the reused branch's first publish non-fast-forward against the stale head
5504
+ * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip is
5505
+ * not an ancestor of the new branch), and a retried submit then skips the
5506
+ * local push on a clean tree and enqueues the worker push of the STALE head,
5507
+ * resurrecting the deleted branch's content on GitHub as an apparent success.
5728
5508
  *
5729
- * Deliberately leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`)
5730
- * alone: that namespace mirrors GitHub's view, and if the branch still
5731
- * exists on GitHub, the create-time collision check SHOULD keep reporting
5732
- * it until the remote side is actually gone.
5509
+ * Leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`) alone: that
5510
+ * namespace mirrors GitHub's view, so while the branch still exists there the
5511
+ * create-time collision check SHOULD keep reporting it.
5733
5512
  *
5734
- * Same `--git-dir` invocation style as bareRemoteHasBranch above (works
5735
- * under `safe.bareRepository=explicit`). The existence pre-check makes
5736
- * "absent" deterministic instead of parsing update-ref's locale-dependent
5737
- * failure text, and its captured SHA is passed to `update-ref -d` as the
5738
- * expected old value -- same pattern as reconcileTrackedBranches'
5739
- * guarded updates (worker/git-sync.ts): a concurrent Lambda push
5740
- * re-creating/moving this branch between the read and the delete makes
5741
- * update-ref throw (surfaced as the caller's best-effort warning) instead
5742
- * of silently deleting a commit that was just pushed. No
5743
- * `--end-of-options` on update-ref (older gits don't accept it there);
5744
- * the ref argument always begins with the literal `refs/heads/` prefix,
5745
- * so it can never parse as an option.
5513
+ * Same `--git-dir` invocation style as bareRemoteHasBranch above (works under
5514
+ * `safe.bareRepository=explicit`). The existence pre-check makes "absent"
5515
+ * deterministic instead of parsing update-ref's locale-dependent failure
5516
+ * text, and its captured SHA is passed to `update-ref -d` as the expected old
5517
+ * value, so a concurrent push re-creating or moving this branch between the
5518
+ * read and the delete makes update-ref throw instead of silently deleting a
5519
+ * just-pushed commit. No `--end-of-options` on update-ref (older gits reject
5520
+ * it there); the ref argument always begins with the literal `refs/heads/`
5521
+ * prefix, so it can never parse as an option.
5746
5522
  */
5747
5523
  static async deleteBareRemoteHead(remotePath, branch) {
5748
5524
  const ref = `refs/heads/${branch}`;
@@ -5808,7 +5584,6 @@ var GitManager = class _GitManager {
5808
5584
  }
5809
5585
  }
5810
5586
  /**
5811
- * Find the git root directory
5812
5587
  * @returns Path to git root, or cwd if not in a git repo
5813
5588
  */
5814
5589
  static async findGitRoot() {
@@ -5822,8 +5597,6 @@ var GitManager = class _GitManager {
5822
5597
  return gitRoot;
5823
5598
  }
5824
5599
  /**
5825
- * Validate that a git repository exists at the given path
5826
- * @param repoPath - Path to check for .git directory
5827
5600
  * @throws Error if git repo doesn't exist
5828
5601
  */
5829
5602
  static async validateGitRepoExists(repoPath) {
@@ -5841,22 +5614,14 @@ var GitManager = class _GitManager {
5841
5614
  }
5842
5615
  /**
5843
5616
  * Guards prod mode against pointing git operations at a NETWORK remote
5844
- * (http(s)://, ssh://, git://, or scp-like `user@host:path`).
5617
+ * (http(s)://, ssh://, git://, or scp-like `user@host:path`): the prod CMS
5618
+ * Lambda has no internet access and would hang until timeout trying to
5619
+ * clone/fetch/push one. Fires only for `mode === 'prod'`, and only for the
5620
+ * three resolvable sources (explicit param, config, env var) — `file://`
5621
+ * URLs, plain filesystem paths and `resolveRemoteUrl`'s auto-detect step
5622
+ * (local by construction) are always allowed.
5845
5623
  *
5846
- * In the intended prod architecture the CMS Lambda has no internet access:
5847
- * all git network I/O happens on the EC2 worker against the EFS-local bare
5848
- * repo `{workspace}/remote.git`, which the Lambda reaches via auto-detect
5849
- * (see `resolveRemoteUrl`'s auto-detect step, which always yields a local
5850
- * path by construction — never checked here). A network URL supplied via
5851
- * any of the three resolvable sources (explicit param, config, env var) is
5852
- * almost always a misconfiguration: the internet-less Lambda would try to
5853
- * clone/fetch/push it directly and hang until timeout.
5854
- *
5855
- * Dev mode is never restricted here — this only fires for `mode === 'prod'`.
5856
- * `file://` URLs and plain filesystem paths are LOCAL and always allowed.
5857
- *
5858
- * @param source - Human-readable description of where `url` came from, used
5859
- * only in the thrown error message (e.g. "config.defaultRemoteUrl").
5624
+ * @param source - Where `url` came from, for the thrown error message only.
5860
5625
  */
5861
5626
  static assertRemoteUrlAllowedInMode(mode, url, source, allowNetworkRemoteInProd) {
5862
5627
  if (mode !== "prod")
@@ -5874,17 +5639,12 @@ var GitManager = class _GitManager {
5874
5639
  * 3. Environment variable (mode-specific)
5875
5640
  * 4. Auto-initialized local remote (for dev mode)
5876
5641
  *
5877
- * Uses strategy flags to determine behavior, GitManager executes the logic.
5878
- *
5879
- * In prod mode, a resolved network URL from any of the first three sources
5880
- * is rejected unless `options.allowNetworkRemoteInProd` is set — see
5881
- * `assertRemoteUrlAllowedInMode`. Auto-detect/auto-init (source 4) are never
5882
- * checked: they always yield a local filesystem path by construction.
5883
- *
5884
- * @param options.sourceRoot - Optional source directory for monorepos. When provided,
5885
- * this directory (relative to git root) is used as the source for the simulated remote.
5886
- * Defaults to process.cwd().
5642
+ * In prod mode a resolved network URL from any of the first three sources is
5643
+ * rejected unless `options.allowNetworkRemoteInProd` is set — see
5644
+ * `assertRemoteUrlAllowedInMode`.
5887
5645
  *
5646
+ * @param options.sourceRoot - Source directory for monorepos, relative to the
5647
+ * git root; the source for the simulated remote. Defaults to process.cwd().
5888
5648
  * @returns Remote URL or undefined if no remote is needed
5889
5649
  */
5890
5650
  static async resolveRemoteUrl(options) {
@@ -5936,14 +5696,12 @@ var GitManager = class _GitManager {
5936
5696
  *
5937
5697
  * Uses `rev-parse --git-dir` with `GIT_CEILING_DIRECTORIES` pinned to the
5938
5698
  * parent directory so a corrupt/missing `.git` can't make git silently
5939
- * traverse upward and report a false positive from an ancestor repo (the
5940
- * same protection `initializeWorkspace` relies on below).
5699
+ * traverse upward and report a false positive from an ancestor repo.
5941
5700
  *
5942
5701
  * Shared by `initializeWorkspace` (clone-vs-reuse decision) and
5943
5702
  * `SettingsWorkspaceManager`'s rename guard (settings-workspace.ts), which
5944
5703
  * must know whether a settings workspace ALREADY exists before touching it —
5945
- * touching an existing orphan settings branch under a different name wipes
5946
- * permissions.json/groups.json (see that guard's doc comment).
5704
+ * re-initializing one under a different name wipes permissions.json/groups.json.
5947
5705
  */
5948
5706
  static async repoExistsAt(workspacePath) {
5949
5707
  try {
@@ -5959,12 +5717,7 @@ var GitManager = class _GitManager {
5959
5717
  * Ensures a git workspace is initialized and ready for use.
5960
5718
  * Handles cloning, remote configuration, and branch checkout/creation.
5961
5719
  *
5962
- * This centralizes the common initialization sequence used by both BranchWorkspaceManager
5963
- * and SettingsWorkspaceManager.
5964
- *
5965
5720
  * Note: Does NOT configure git author - that should be done before commits, not during init.
5966
- *
5967
- * @returns Configured GitManager instance for the workspace
5968
5721
  */
5969
5722
  static async initializeWorkspace(options) {
5970
5723
  const baseBranch = await resolveBaseBranch({
@@ -6062,20 +5815,16 @@ var GitManager = class _GitManager {
6062
5815
  }
6063
5816
  /**
6064
5817
  * Mark ContentStore ID indexes AND the resolved-schema cache rooted at (or
6065
- * under) this repo as stale. Called after operations that mutate the
6066
- * working tree (checkout/merge/rebase) so ID→path lookups don't keep
6067
- * resolving to pre-mutation paths, and so a rebase/checkout that pulled in
6068
- * upstream `.collection.json` changes doesn't leave the schema cache
6069
- * pinned to the pre-mutation schema. Invoked in `finally` blocks because
6070
- * even failed merges/rebases may have touched the tree before aborting;
6071
- * over-invalidating is safe.
5818
+ * under) this repo as stale, so ID→path lookups and `.collection.json`
5819
+ * schemas don't stay pinned to the pre-mutation tree. Called in `finally`
5820
+ * blocks around every working-tree mutation (checkout/merge/rebase) because
5821
+ * even a failed merge may have touched the tree; over-invalidating is safe.
6072
5822
  *
6073
- * Covers both scopes: in-process stores/caches via their registries, and
6074
- * consumers in OTHER processes sharing the filesystem (worker vs Lambda on
6075
- * EFS) via the on-disk generation markers — unless this manager targets a
6076
- * settings workspace (skipIndexMarker), where only the free in-process
6077
- * content-index invalidation runs and NEITHER marker is bumped (settings
6078
- * workspaces have no schema cache of their own either).
5823
+ * Covers in-process stores/caches via their registries and other processes
5824
+ * sharing the filesystem (worker vs Lambda on EFS) via the on-disk generation
5825
+ * markers — except on a settings workspace (skipIndexMarker), where only the
5826
+ * free in-process content-index invalidation runs and NEITHER marker is
5827
+ * bumped (such workspaces have no schema cache of their own either).
6079
5828
  */
6080
5829
  async invalidateContentIndexes() {
6081
5830
  if (this.skipIndexMarker) {
@@ -6224,27 +5973,21 @@ var GitManager = class _GitManager {
6224
5973
  ]);
6225
5974
  }
6226
5975
  /**
6227
- * Check whether the local branch has commits the remote mirror doesn't
6228
- * have -- i.e. whether push() would actually move the remote ref forward.
5976
+ * Whether the local branch has commits the remote mirror doesn't -- i.e.
5977
+ * whether push() would move the remote ref forward.
6229
5978
  *
6230
- * Exists so callers (submitBranch) can gate pushing on "is there anything
6231
- * new to send" rather than on "is the working tree dirty": committing
6232
- * cleans the tree, so a dirty-tree gate around commit+push skips the push
6233
- * entirely on a retry after a failed push, even though the just-created
6234
- * commit never reached the remote (see services.ts submitBranch).
5979
+ * Callers (services.ts submitBranch) gate pushing on this rather than on "is
5980
+ * the working tree dirty": committing cleans the tree, so a dirty-tree gate
5981
+ * around commit+push skips the push entirely when retried after a failed
5982
+ * push, even though the commit never reached the remote.
6235
5983
  *
6236
- * Workspaces are cloned `--single-branch`, so the remote-tracking ref for
6237
- * any branch other than the one cloned never exists locally -- same
6238
- * constraint pullBaseInner works around. This fetches the specific branch
6239
- * directly (bypassing the configured single-branch refspec, exactly like
6240
- * pullBaseInner) and pins the result to FETCH_HEAD's SHA immediately after
6241
- * the fetch that populated it, rather than trusting `<remote>/<branch>`:
6242
- * FETCH_HEAD is a shared mutable file that any other fetch in this clone
6243
- * can repoint before it's read.
5984
+ * Fetches the specific branch directly and pins FETCH_HEAD's SHA immediately
5985
+ * after, rather than trusting `<remote>/<branch>` -- the pullBaseInner
5986
+ * constraint: single-branch clones have no remote-tracking ref for any other
5987
+ * branch, and FETCH_HEAD is shared mutable state.
6244
5988
  *
6245
- * A branch that has never been pushed has no ref on the remote at all --
6246
- * `git fetch` then fails ("couldn't find remote ref"), which this treats
6247
- * as "ahead" (needs pushing) rather than an error.
5989
+ * A branch never pushed has no ref on the remote, so `git fetch` fails
5990
+ * ("couldn't find remote ref"); that counts as "ahead", not an error.
6248
5991
  */
6249
5992
  async hasUnpushedCommits(branch) {
6250
5993
  const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
@@ -6293,51 +6036,31 @@ var GitManager = class _GitManager {
6293
6036
  await this.git.remote(["set-url", this.remote, remoteUrl]);
6294
6037
  }
6295
6038
  }
6296
- /**
6297
- * Check if working directory has uncommitted changes
6298
- */
6299
6039
  async hasUncommittedChanges() {
6300
6040
  const status = await this.status();
6301
6041
  return status.files.length > 0;
6302
6042
  }
6303
- /**
6304
- * Get list of uncommitted file paths
6305
- */
6306
6043
  async getUncommittedFiles() {
6307
6044
  const status = await this.status();
6308
6045
  return status.files.map((f) => f.path);
6309
6046
  }
6310
- /**
6311
- * Get remote URL for current repo
6312
- */
6313
6047
  async getRemoteUrl() {
6314
6048
  const remotes = await this.git.getRemotes(true);
6315
6049
  const remote = remotes.find((r) => r.name === this.remote);
6316
6050
  return remote?.refs.push || remote?.refs.fetch;
6317
6051
  }
6318
6052
  /**
6319
- * Add a pattern to .git/info/exclude to prevent it from being committed/pushed.
6320
- * This is used to exclude .canopy-meta/ from content branch workspaces.
6321
- *
6322
- * .git/info/exclude is a per-repository gitignore that never gets committed.
6323
- * Perfect for runtime metadata that should never leave the workspace.
6324
- *
6325
- * This is idempotent - if the pattern already exists, it won't be added again.
6053
+ * Excludes `pattern` (e.g. `.canopy-meta/`) from this workspace's git, so it
6054
+ * can never be committed or pushed. See {@link ensureGitExcludePattern}.
6326
6055
  */
6327
6056
  async ensureGitExclude(pattern) {
6328
6057
  await ensureGitExcludePattern(this.repoPath, pattern);
6329
6058
  }
6330
6059
  /**
6331
- * Create an orphan branch for settings (permissions/groups).
6332
- *
6333
- * Orphan branches have no shared history with other branches - they start fresh.
6334
- * This is perfect for deployment-specific settings that shouldn't pollute content history.
6335
- *
6336
- * The branch contains only settings files committed by explicit path
6337
- * (e.g. permissions.json, groups.json at the workspace root).
6338
- *
6339
- * @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
6340
- * @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
6060
+ * Create an orphan branch (no shared history) for settings, so
6061
+ * deployment-specific settings never pollute content history. It holds only
6062
+ * settings files committed by explicit path (permissions.json, groups.json at
6063
+ * the workspace root).
6341
6064
  */
6342
6065
  async createOrphanSettingsBranch(branchName, initialFiles) {
6343
6066
  try {