@elytracms/studio 0.0.91 → 0.0.93

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 (353) hide show
  1. package/convex/asset-alt-migration.test.ts +127 -0
  2. package/convex/asset-replace.test.ts +168 -0
  3. package/convex/asset-revalidate.test.ts +6 -6
  4. package/convex/assets.ts +41 -5
  5. package/convex/cms.ts +537 -177
  6. package/convex/contract-parity.test.ts +173 -19
  7. package/convex/delivery.ts +26 -13
  8. package/convex/document-revalidate.test.ts +17 -16
  9. package/convex/orphan-hydration.test.ts +116 -0
  10. package/convex/presence-cost.test.ts +89 -0
  11. package/convex/rail-write-parity.test.ts +17 -14
  12. package/convex/referenceMaintenance.ts +15 -15
  13. package/convex/schema.ts +70 -5
  14. package/convex/triggers.ts +21 -8
  15. package/convex/write-limits.ts +1 -1
  16. package/dist/components/ai/ai-chat-panel.js +33 -22
  17. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  18. package/dist/components/ai/proposal-card.js +2 -1
  19. package/dist/components/ai/proposal-card.js.map +1 -1
  20. package/dist/components/ai/translate-document-action.d.ts +8 -2
  21. package/dist/components/ai/translate-document-action.js.map +1 -1
  22. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  23. package/dist/components/ai/translate-document-dialog.js +14 -8
  24. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  25. package/dist/components/ai/translate-skipped.d.ts +19 -0
  26. package/dist/components/ai/translate-skipped.js +46 -0
  27. package/dist/components/ai/translate-skipped.js.map +1 -0
  28. package/dist/components/app-views/default-modules.d.ts +23 -6
  29. package/dist/components/app-views/default-modules.js +33 -14
  30. package/dist/components/app-views/default-modules.js.map +1 -1
  31. package/dist/components/assets/asset-detail.js +18 -8
  32. package/dist/components/assets/asset-detail.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +15 -4
  34. package/dist/components/assets/asset-metadata.js +15 -2
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.js +4 -1
  37. package/dist/components/assets/asset-search.js.map +1 -1
  38. package/dist/components/assets/asset-selection.d.ts +0 -8
  39. package/dist/components/assets/asset-selection.js +4 -23
  40. package/dist/components/assets/asset-selection.js.map +1 -1
  41. package/dist/components/assets/asset-thumb.js +3 -2
  42. package/dist/components/assets/asset-thumb.js.map +1 -1
  43. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  44. package/dist/components/assets/assets-collection-view-source.js +9 -5
  45. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  46. package/dist/components/assets/assets-store.d.ts +14 -0
  47. package/dist/components/assets/assets-store.js +23 -1
  48. package/dist/components/assets/assets-store.js.map +1 -1
  49. package/dist/components/assets/replace-file.d.ts +22 -0
  50. package/dist/components/assets/replace-file.js +59 -0
  51. package/dist/components/assets/replace-file.js.map +1 -0
  52. package/dist/components/builder/blocks-model.d.ts +14 -26
  53. package/dist/components/builder/blocks-model.js +35 -41
  54. package/dist/components/builder/blocks-model.js.map +1 -1
  55. package/dist/components/builder/blocks-panel.d.ts +7 -3
  56. package/dist/components/builder/blocks-panel.js +6 -5
  57. package/dist/components/builder/blocks-panel.js.map +1 -1
  58. package/dist/components/builder/builder-store.d.ts +126 -30
  59. package/dist/components/builder/builder-store.js +169 -67
  60. package/dist/components/builder/builder-store.js.map +1 -1
  61. package/dist/components/builder/canvas-body.d.ts +30 -0
  62. package/dist/components/builder/canvas-body.js +38 -0
  63. package/dist/components/builder/canvas-body.js.map +1 -0
  64. package/dist/components/builder/canvas-chrome-css.js +7 -0
  65. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  66. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  67. package/dist/components/builder/canvas-dnd.js +5 -5
  68. package/dist/components/builder/canvas-dnd.js.map +1 -1
  69. package/dist/components/builder/canvas-render-context.d.ts +30 -22
  70. package/dist/components/builder/canvas-render-context.js +44 -44
  71. package/dist/components/builder/canvas-render-context.js.map +1 -1
  72. package/dist/components/builder/canvas-runs.d.ts +45 -0
  73. package/dist/components/builder/canvas-runs.js +51 -0
  74. package/dist/components/builder/canvas-runs.js.map +1 -0
  75. package/dist/components/builder/canvas.d.ts +5 -4
  76. package/dist/components/builder/canvas.js +34 -6
  77. package/dist/components/builder/canvas.js.map +1 -1
  78. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  79. package/dist/components/builder/clipboard-actions.js +50 -13
  80. package/dist/components/builder/clipboard-actions.js.map +1 -1
  81. package/dist/components/builder/composition-field-editor.d.ts +10 -16
  82. package/dist/components/builder/composition-field-editor.js +30 -40
  83. package/dist/components/builder/composition-field-editor.js.map +1 -1
  84. package/dist/components/builder/composition-patches.d.ts +7 -0
  85. package/dist/components/builder/composition-patches.js +256 -0
  86. package/dist/components/builder/composition-patches.js.map +1 -0
  87. package/dist/components/builder/delete-key.d.ts +5 -0
  88. package/dist/components/builder/delete-key.js +4 -0
  89. package/dist/components/builder/delete-key.js.map +1 -1
  90. package/dist/components/builder/insert-context.d.ts +17 -25
  91. package/dist/components/builder/insert-context.js +45 -92
  92. package/dist/components/builder/insert-context.js.map +1 -1
  93. package/dist/components/builder/page-tree.d.ts +3 -3
  94. package/dist/components/builder/page-tree.js +3 -3
  95. package/dist/components/builder/page-tree.js.map +1 -1
  96. package/dist/components/builder/placement-refusal.d.ts +18 -0
  97. package/dist/components/builder/placement-refusal.js +46 -0
  98. package/dist/components/builder/placement-refusal.js.map +1 -0
  99. package/dist/components/builder/property-inspector.js +4 -0
  100. package/dist/components/builder/property-inspector.js.map +1 -1
  101. package/dist/components/builder/reuse-context.js +65 -26
  102. package/dist/components/builder/reuse-context.js.map +1 -1
  103. package/dist/components/builder/reuse.d.ts +79 -8
  104. package/dist/components/builder/reuse.js +71 -23
  105. package/dist/components/builder/reuse.js.map +1 -1
  106. package/dist/components/builder/run-inspector.d.ts +11 -0
  107. package/dist/components/builder/run-inspector.js +33 -0
  108. package/dist/components/builder/run-inspector.js.map +1 -0
  109. package/dist/components/cms/asset-field.d.ts +38 -1
  110. package/dist/components/cms/asset-field.js +182 -58
  111. package/dist/components/cms/asset-field.js.map +1 -1
  112. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  113. package/dist/components/cms/asset-picker-core.js +20 -0
  114. package/dist/components/cms/asset-picker-core.js.map +1 -1
  115. package/dist/components/cms/asset-picker.js +12 -18
  116. package/dist/components/cms/asset-picker.js.map +1 -1
  117. package/dist/components/cms/bulk-actions.d.ts +4 -20
  118. package/dist/components/cms/bulk-actions.js +3 -24
  119. package/dist/components/cms/bulk-actions.js.map +1 -1
  120. package/dist/components/cms/cell-edit.d.ts +5 -5
  121. package/dist/components/cms/cell-edit.js.map +1 -1
  122. package/dist/components/cms/cell.js.map +1 -1
  123. package/dist/components/cms/cms-store.d.ts +87 -45
  124. package/dist/components/cms/cms-store.js +347 -203
  125. package/dist/components/cms/cms-store.js.map +1 -1
  126. package/dist/components/cms/collection-table.js +7 -2
  127. package/dist/components/cms/collection-table.js.map +1 -1
  128. package/dist/components/cms/commit-policy.d.ts +24 -0
  129. package/dist/components/cms/commit-policy.js +37 -0
  130. package/dist/components/cms/commit-policy.js.map +1 -0
  131. package/dist/components/cms/detail-editor.js +83 -137
  132. package/dist/components/cms/detail-editor.js.map +1 -1
  133. package/dist/components/cms/document-diff-view.d.ts +3 -1
  134. package/dist/components/cms/document-diff-view.js +12 -7
  135. package/dist/components/cms/document-diff-view.js.map +1 -1
  136. package/dist/components/cms/draft-commit.d.ts +70 -0
  137. package/dist/components/cms/draft-commit.js +82 -0
  138. package/dist/components/cms/draft-commit.js.map +1 -0
  139. package/dist/components/cms/duplicate-document.d.ts +11 -13
  140. package/dist/components/cms/duplicate-document.js +26 -34
  141. package/dist/components/cms/duplicate-document.js.map +1 -1
  142. package/dist/components/cms/field-anchors.d.ts +7 -4
  143. package/dist/components/cms/field-anchors.js +9 -7
  144. package/dist/components/cms/field-anchors.js.map +1 -1
  145. package/dist/components/cms/field-collaboration.d.ts +47 -0
  146. package/dist/components/cms/field-collaboration.js +87 -0
  147. package/dist/components/cms/field-collaboration.js.map +1 -0
  148. package/dist/components/cms/field-control-lib.d.ts +22 -1
  149. package/dist/components/cms/field-control-lib.js +33 -4
  150. package/dist/components/cms/field-control-lib.js.map +1 -1
  151. package/dist/components/cms/field-control.js +17 -14
  152. package/dist/components/cms/field-control.js.map +1 -1
  153. package/dist/components/cms/gate-validation.d.ts +21 -30
  154. package/dist/components/cms/gate-validation.js +11 -22
  155. package/dist/components/cms/gate-validation.js.map +1 -1
  156. package/dist/components/cms/object-array-field.js +4 -4
  157. package/dist/components/cms/object-array-field.js.map +1 -1
  158. package/dist/components/cms/prop-fields.d.ts +7 -2
  159. package/dist/components/cms/prop-fields.js +13 -6
  160. package/dist/components/cms/prop-fields.js.map +1 -1
  161. package/dist/components/cms/reference-confirm.js +10 -4
  162. package/dist/components/cms/reference-confirm.js.map +1 -1
  163. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  164. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  165. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  166. package/dist/components/cms/rich-text/insert.js +25 -17
  167. package/dist/components/cms/rich-text/insert.js.map +1 -1
  168. package/dist/components/cms/use-document-draft.d.ts +96 -0
  169. package/dist/components/cms/use-document-draft.js +283 -0
  170. package/dist/components/cms/use-document-draft.js.map +1 -0
  171. package/dist/components/cms/use-document-versions.js +2 -2
  172. package/dist/components/cms/use-document-versions.js.map +1 -1
  173. package/dist/components/cms/use-live-url.d.ts +6 -12
  174. package/dist/components/cms/use-live-url.js +16 -15
  175. package/dist/components/cms/use-live-url.js.map +1 -1
  176. package/dist/components/cms/use-publish-controller.d.ts +13 -18
  177. package/dist/components/cms/use-publish-controller.js +24 -41
  178. package/dist/components/cms/use-publish-controller.js.map +1 -1
  179. package/dist/components/cms/use-published-perspective.d.ts +2 -3
  180. package/dist/components/cms/use-published-perspective.js +4 -3
  181. package/dist/components/cms/use-published-perspective.js.map +1 -1
  182. package/dist/components/cms/versions-pane.js +8 -1
  183. package/dist/components/cms/versions-pane.js.map +1 -1
  184. package/dist/components/collection-view/cms-collection-view-source.js +24 -7
  185. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  186. package/dist/components/components/transfer-dialog.js +28 -23
  187. package/dist/components/components/transfer-dialog.js.map +1 -1
  188. package/dist/components/components/transfer.d.ts +31 -21
  189. package/dist/components/components/transfer.js +49 -35
  190. package/dist/components/components/transfer.js.map +1 -1
  191. package/dist/components/project-list.js.map +1 -1
  192. package/dist/components/project-session.d.ts +6 -3
  193. package/dist/components/project-session.js +14 -8
  194. package/dist/components/project-session.js.map +1 -1
  195. package/dist/components/project-switcher.js.map +1 -1
  196. package/dist/components/rail-view-source.js +2 -1
  197. package/dist/components/rail-view-source.js.map +1 -1
  198. package/dist/components/section-nav.js +1 -1
  199. package/dist/components/section-nav.js.map +1 -1
  200. package/dist/lib/ai/apply-content.js +56 -116
  201. package/dist/lib/ai/apply-content.js.map +1 -1
  202. package/dist/lib/ai/chat-context.d.ts +8 -0
  203. package/dist/lib/ai/chat-context.js +37 -6
  204. package/dist/lib/ai/chat-context.js.map +1 -1
  205. package/dist/lib/ai/chat-state.d.ts +14 -1
  206. package/dist/lib/ai/chat-state.js.map +1 -1
  207. package/dist/lib/ai/constraints.d.ts +20 -5
  208. package/dist/lib/ai/constraints.js +125 -33
  209. package/dist/lib/ai/constraints.js.map +1 -1
  210. package/dist/lib/ai/fake-provider.d.ts +16 -3
  211. package/dist/lib/ai/fake-provider.js +22 -6
  212. package/dist/lib/ai/fake-provider.js.map +1 -1
  213. package/dist/lib/ai/index.d.ts +2 -0
  214. package/dist/lib/ai/index.js +2 -0
  215. package/dist/lib/ai/index.js.map +1 -1
  216. package/dist/lib/ai/leash.d.ts +34 -0
  217. package/dist/lib/ai/leash.js +28 -0
  218. package/dist/lib/ai/leash.js.map +1 -0
  219. package/dist/lib/ai/provenance.d.ts +1 -1
  220. package/dist/lib/ai/provenance.js.map +1 -1
  221. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  222. package/dist/lib/ai/rich-text-input.js +2 -1
  223. package/dist/lib/ai/rich-text-input.js.map +1 -1
  224. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  225. package/dist/lib/ai/rich-text-translate.js +28 -10
  226. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  227. package/dist/lib/ai/shape.d.ts +13 -13
  228. package/dist/lib/ai/shape.js +69 -93
  229. package/dist/lib/ai/shape.js.map +1 -1
  230. package/dist/lib/ai/tools.d.ts +36 -41
  231. package/dist/lib/ai/tools.js +85 -87
  232. package/dist/lib/ai/tools.js.map +1 -1
  233. package/dist/lib/ai/translate-document.d.ts +39 -5
  234. package/dist/lib/ai/translate-document.js +160 -51
  235. package/dist/lib/ai/translate-document.js.map +1 -1
  236. package/dist/lib/ai/types.d.ts +34 -23
  237. package/dist/lib/ai/types.js.map +1 -1
  238. package/dist/lib/ai/update-fields.d.ts +21 -12
  239. package/dist/lib/ai/update-fields.js +118 -177
  240. package/dist/lib/ai/update-fields.js.map +1 -1
  241. package/dist/lib/ai/writer-form.d.ts +116 -0
  242. package/dist/lib/ai/writer-form.js +429 -0
  243. package/dist/lib/ai/writer-form.js.map +1 -0
  244. package/dist/lib/ai-current-document.d.ts +11 -11
  245. package/dist/lib/ai-current-document.js +4 -4
  246. package/dist/lib/ai-current-document.js.map +1 -1
  247. package/dist/lib/assets/upload.d.ts +31 -2
  248. package/dist/lib/assets/upload.js +61 -6
  249. package/dist/lib/assets/upload.js.map +1 -1
  250. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  251. package/dist/lib/clipboard/os-clipboard.js +8 -4
  252. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  253. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  254. package/dist/lib/clipboard/paste-target.js +8 -7
  255. package/dist/lib/clipboard/paste-target.js.map +1 -1
  256. package/dist/lib/cms/field-patch.d.ts +15 -0
  257. package/dist/lib/cms/field-patch.js +25 -0
  258. package/dist/lib/cms/field-patch.js.map +1 -0
  259. package/dist/lib/cms/published-documents.d.ts +1 -1
  260. package/dist/lib/cms/published-documents.js +1 -1
  261. package/dist/lib/cms/published-documents.js.map +1 -1
  262. package/dist/lib/cms-views/view-model.d.ts +3 -3
  263. package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
  264. package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
  265. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
  266. package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
  267. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  268. package/dist/lib/data/collection-content.d.ts +4 -13
  269. package/dist/lib/data/collection-content.js +11 -5
  270. package/dist/lib/data/collection-content.js.map +1 -1
  271. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  272. package/dist/lib/dnd/drop-rules.js +53 -59
  273. package/dist/lib/dnd/drop-rules.js.map +1 -1
  274. package/dist/lib/engine-version.d.ts +1 -1
  275. package/dist/lib/engine-version.js +1 -1
  276. package/dist/lib/engine-version.js.map +1 -1
  277. package/dist/lib/i18n/messages.de.js +72 -15
  278. package/dist/lib/i18n/messages.de.js.map +1 -1
  279. package/dist/lib/i18n/messages.en.d.ts +61 -12
  280. package/dist/lib/i18n/messages.en.js +76 -16
  281. package/dist/lib/i18n/messages.en.js.map +1 -1
  282. package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
  283. package/dist/lib/operations/__fixtures__/write-document.js +16 -0
  284. package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
  285. package/dist/lib/operations/cms-persistence.d.ts +124 -31
  286. package/dist/lib/operations/cms-persistence.js +447 -68
  287. package/dist/lib/operations/cms-persistence.js.map +1 -1
  288. package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
  289. package/dist/lib/operations/config-manifest-lookup.js +56 -0
  290. package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
  291. package/dist/lib/operations/project-runtime-config.d.ts +39 -0
  292. package/dist/lib/operations/project-runtime-config.js +54 -0
  293. package/dist/lib/operations/project-runtime-config.js.map +1 -0
  294. package/dist/lib/operations/publish-with-revision.d.ts +64 -0
  295. package/dist/lib/operations/publish-with-revision.js +66 -0
  296. package/dist/lib/operations/publish-with-revision.js.map +1 -0
  297. package/dist/lib/operations/runtime-validator.d.ts +32 -0
  298. package/dist/lib/operations/runtime-validator.js +18 -0
  299. package/dist/lib/operations/runtime-validator.js.map +1 -0
  300. package/dist/lib/operations/session-hydration.d.ts +31 -14
  301. package/dist/lib/operations/session-hydration.js +12 -16
  302. package/dist/lib/operations/session-hydration.js.map +1 -1
  303. package/dist/lib/operations/studio-operations.d.ts +59 -2
  304. package/dist/lib/operations/studio-operations.js +76 -14
  305. package/dist/lib/operations/studio-operations.js.map +1 -1
  306. package/dist/lib/persistence/convex-adapter.d.ts +13 -5
  307. package/dist/lib/persistence/convex-adapter.js +189 -74
  308. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  309. package/dist/lib/persistence/convex-client.d.ts +65 -5
  310. package/dist/lib/persistence/convex-client.js +106 -3
  311. package/dist/lib/persistence/convex-client.js.map +1 -1
  312. package/dist/lib/persistence/index.js +17 -18
  313. package/dist/lib/persistence/index.js.map +1 -1
  314. package/dist/lib/persona/labels.js +4 -1
  315. package/dist/lib/persona/labels.js.map +1 -1
  316. package/dist/lib/query/operations-query.d.ts +1 -1
  317. package/dist/lib/query/operations-query.js +1 -1
  318. package/dist/lib/query/operations-query.js.map +1 -1
  319. package/dist/lib/references/impact.d.ts +21 -15
  320. package/dist/lib/references/impact.js +13 -26
  321. package/dist/lib/references/impact.js.map +1 -1
  322. package/dist/lib/references/references-store.d.ts +9 -0
  323. package/dist/lib/references/references-store.js +24 -4
  324. package/dist/lib/references/references-store.js.map +1 -1
  325. package/dist/lib/routes/route-issues.d.ts +6 -6
  326. package/dist/lib/routes/route-issues.js +26 -80
  327. package/dist/lib/routes/route-issues.js.map +1 -1
  328. package/dist/lib/workspace/config-issues.js +2 -1
  329. package/dist/lib/workspace/config-issues.js.map +1 -1
  330. package/dist/lib/workspace/index.d.ts +31 -1
  331. package/dist/lib/workspace/index.js +64 -17
  332. package/dist/lib/workspace/index.js.map +1 -1
  333. package/dist/lib/workspace/switcher-source.d.ts +3 -3
  334. package/dist/lib/workspace/switcher-source.js.map +1 -1
  335. package/dist/lib/workspace/workspace-source.d.ts +2 -2
  336. package/dist/lib/workspace/workspace-source.js +2 -1
  337. package/dist/lib/workspace/workspace-source.js.map +1 -1
  338. package/package.json +3 -3
  339. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  340. package/dist/components/cms/cms-composition-validators.js +0 -133
  341. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  342. package/dist/components/cms/publish-clean.d.ts +0 -28
  343. package/dist/components/cms/publish-clean.js +0 -38
  344. package/dist/components/cms/publish-clean.js.map +0 -1
  345. package/dist/components/cms/use-save-mode.d.ts +0 -35
  346. package/dist/components/cms/use-save-mode.js +0 -54
  347. package/dist/components/cms/use-save-mode.js.map +0 -1
  348. package/dist/lib/assets/asset-placements.d.ts +0 -28
  349. package/dist/lib/assets/asset-placements.js +0 -83
  350. package/dist/lib/assets/asset-placements.js.map +0 -1
  351. package/dist/lib/cms/document-gate.d.ts +0 -47
  352. package/dist/lib/cms/document-gate.js +0 -61
  353. package/dist/lib/cms/document-gate.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { SEED_PROJECT_ID, createSeededPersistenceAdapter } from '@elytracms/core/persistence';
2
2
  import { resolveDeploymentCollections, resolveDeploymentProjectId, resolveDeploymentUrl, } from '../workspace/index.js';
3
3
  import { createBrowserBlobStorage } from '../assets/browser-blob-storage.js';
4
- import { getConvexClient } from './convex-client.js';
4
+ import { getConvexClient, getReactiveConvexClient } from './convex-client.js';
5
5
  import { ConvexPersistenceAdapter } from './convex-adapter.js';
6
6
  import { resolveBackendConfig } from './backend-mode.js';
7
7
  /**
@@ -18,11 +18,14 @@ import { resolveBackendConfig } from './backend-mode.js';
18
18
  export async function selectPersistenceAdapter() {
19
19
  const config = resolveBackendConfig();
20
20
  if (config.kind === 'convex') {
21
- return new ConvexPersistenceAdapter(getConvexClient(resolveDeploymentUrl(config.url)), resolveDeploymentProjectId('aurora-marketing'),
22
- // CMS collections are config-owned (EC-183, AD-11): inject the active
23
- // project's collections so the adapter serves the schema from config (the
24
- // backend has no `cmsSchemas` table).
25
- resolveDeploymentCollections());
21
+ // EC-583: no collections here — the store holds no schema; the operations
22
+ // context carries the config's collections (`createStudioOperations`).
23
+ const url = resolveDeploymentUrl(config.url);
24
+ return new ConvexPersistenceAdapter(getConvexClient(url), resolveDeploymentProjectId('aurora-marketing'), {
25
+ // EC-595: the document subscription's reactive client — built lazily on the first
26
+ // subscribe, authenticated from the same URL's client (the token it mirrors).
27
+ reactive: () => getReactiveConvexClient(url),
28
+ });
26
29
  }
27
30
  if (config.kind === 'playground') {
28
31
  // EC-207: routes/redirects are config-sourced, never seeded to the backend —
@@ -32,18 +35,14 @@ export async function selectPersistenceAdapter() {
32
35
  // sit under the same id the studio operates on. Otherwise authorization denies
33
36
  // every read ("not a member of project 'aurora'") and content resolves empty.
34
37
  const projectId = resolveDeploymentProjectId(SEED_PROJECT_ID);
35
- // EC-361 (AD-11 parity with the Convex adapter above): when a config project
36
- // declares collections, the playground serves the CONFIG-OWNED schema — not the
37
- // engine's seeded sample schema — so every schema reader (the content-session
38
- // hydration, the persister baseline, query validation's `filterable` set) sees
39
- // the same declarations connected mode serves. The resolved config already
40
- // carries the EC-257 default asset collection. Without a config (engine
41
- // fixtures/tests) the seeded sample schema stands, byte-identical to before.
42
- // EC-424: passing `collections` straight into the seed (not a follow-up
43
- // `saveSchema`) also reconciles the seeded DOCUMENTS against them — a field a
44
- // config declares localized but the shared fixture stores plain (e.g. this
45
- // repo's demo `post.slug`, EC-315) gets promoted into `localized`, not just
46
- // the schema — see `reconcileSeedDocumentLocalization` in `@elytracms/core/persistence`.
38
+ // EC-361/EC-424: when a config project declares collections, the seed is
39
+ // reconciled against them (a field the config declares localized but the shared
40
+ // fixture stores plain — this repo's demo `post.slug`, EC-315 — is promoted into
41
+ // `localized`), and only their documents are written (EC-606). EC-583: the
42
+ // collections are the seed's LENS only — the store holds no schema; every
43
+ // schema reader (hydration, the CMS surfaces, the operations context) reads the
44
+ // config itself. Without a config (engine fixtures/tests) the sample schema is
45
+ // the lens.
47
46
  const collections = resolveDeploymentCollections();
48
47
  // EC-459: seed with a studio-owned blob adapter that mints real browser-
49
48
  // displayable URLs (object URLs) instead of the engine default's opaque
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/persistence/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { SEED_PROJECT_ID, createSeededPersistenceAdapter } from '@elytracms/core/persistence'\nimport type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport {\n resolveDeploymentCollections,\n resolveDeploymentProjectId,\n resolveDeploymentUrl,\n} from '../workspace'\nimport { createBrowserBlobStorage } from '../assets/browser-blob-storage'\nimport { getConvexClient } from './convex-client'\nimport { ConvexPersistenceAdapter } from './convex-adapter'\nimport { resolveBackendConfig } from './backend-mode'\nimport type { StudioBackendMode } from './backend-mode'\n\n/**\n * Select the persistence backend (EC-153, vision AD-1). **Convex is the\n * default**: a configured `VITE_CONVEX_URL` is required unless the explicit\n * playground flag (`VITE_ELYTRA_MODE=playground`) opts into the deterministic,\n * seeded in-memory adapter. There is **no automatic in-memory fallback** —\n * missing configuration is a hard, descriptive error (the `DeploymentSetupGate`\n * renders it as a boot-error screen before any adapter is requested).\n *\n * EC-150: the Convex path uses the *shared* client so the auth adapter's\n * session JWT (set via `client.setAuth`) authenticates every persistence call.\n */\nexport async function selectPersistenceAdapter(): Promise<PersistenceAdapter> {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') {\n return new ConvexPersistenceAdapter(\n getConvexClient(resolveDeploymentUrl(config.url)),\n resolveDeploymentProjectId('aurora-marketing'),\n // CMS collections are config-owned (EC-183, AD-11): inject the active\n // project's collections so the adapter serves the schema from config (the\n // backend has no `cmsSchemas` table).\n resolveDeploymentCollections(),\n )\n }\n if (config.kind === 'playground') {\n // EC-207: routes/redirects are config-sourced, never seeded to the backend —\n // the playground adapter no longer seeds route documents.\n // EC-310: key the seed under the ACTIVE project id (the config slug, e.g.\n // `aurora`) — not the engine's default `prj_aurora` — so content + memberships\n // sit under the same id the studio operates on. Otherwise authorization denies\n // every read (\"not a member of project 'aurora'\") and content resolves empty.\n const projectId = resolveDeploymentProjectId(SEED_PROJECT_ID)\n // EC-361 (AD-11 parity with the Convex adapter above): when a config project\n // declares collections, the playground serves the CONFIG-OWNED schema — not the\n // engine's seeded sample schema — so every schema reader (the content-session\n // hydration, the persister baseline, query validation's `filterable` set) sees\n // the same declarations connected mode serves. The resolved config already\n // carries the EC-257 default asset collection. Without a config (engine\n // fixtures/tests) the seeded sample schema stands, byte-identical to before.\n // EC-424: passing `collections` straight into the seed (not a follow-up\n // `saveSchema`) also reconciles the seeded DOCUMENTS against them — a field a\n // config declares localized but the shared fixture stores plain (e.g. this\n // repo's demo `post.slug`, EC-315) gets promoted into `localized`, not just\n // the schema — see `reconcileSeedDocumentLocalization` in `@elytracms/core/persistence`.\n const collections = resolveDeploymentCollections()\n // EC-459: seed with a studio-owned blob adapter that mints real browser-\n // displayable URLs (object URLs) instead of the engine default's opaque\n // `memory://` locator — see `browser-blob-storage.ts` for the full\n // rationale. Playground-only; the Convex branch above is untouched.\n const adapter = await createSeededPersistenceAdapter(\n projectId,\n collections.length > 0 ? collections : undefined,\n { blobs: createBrowserBlobStorage() },\n )\n return adapter\n }\n throw new Error(config.detail)\n}\n\n/**\n * A stable key identifying the active backend (EC-179): the resolved deployment\n * URL for the active project under Convex, or `playground`. The operations\n * bootstrap caches one bundle per key, so switching to a project on a different\n * deployment transparently selects (or builds) that backend's operations client.\n */\nexport function activeDeploymentKey(): string {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') return resolveDeploymentUrl(config.url)\n if (config.kind === 'playground') return 'playground'\n throw new Error(config.detail)\n}\n\n/**\n * The active backend mode. Throws the same descriptive error as\n * {@link selectPersistenceAdapter} when the studio is misconfigured — UI that\n * only needs \"is this the playground?\" should use `isPlaygroundMode()`.\n */\nexport function persistenceMode(): StudioBackendMode {\n const config = resolveBackendConfig()\n if (config.kind === 'misconfigured') throw new Error(config.detail)\n return config.kind\n}\n\nexport { ConvexPersistenceAdapter } from './convex-adapter'\nexport {\n CONVEX_URL_ENV_VAR,\n DEV_LOGIN_ENV_VAR,\n MODE_ENV_VAR,\n PLAYGROUND_MODE,\n STUDIO_ENV_KEYS,\n detectStudioEnvAnomaly,\n isDevAutoLoginActive,\n isDevRuntime,\n isPlaygroundMode,\n provideStudioEnv,\n resetProvidedStudioEnvForTests,\n resolveBackendConfig,\n resolveDevAutoLogin,\n studioEnv,\n} from './backend-mode'\nexport type {\n DevAutoLoginCredentials,\n EnvRecord,\n StudioBackendConfig,\n StudioBackendMode,\n StudioEnvAnomaly,\n} from './backend-mode'\n// EC-258: the deployment-readiness facts mapper (the boot DeploymentSetupGate + a\n// future `elytra doctor` CLI compose the same `checkDeploymentReady` contract).\nexport { studioDeploymentReadiness, studioReadinessFacts } from './deployment-readiness'\nexport type { StudioReadinessInputs } from './deployment-readiness'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/persistence/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { SEED_PROJECT_ID, createSeededPersistenceAdapter } from '@elytracms/core/persistence'\nimport type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport {\n resolveDeploymentCollections,\n resolveDeploymentProjectId,\n resolveDeploymentUrl,\n} from '../workspace'\nimport { createBrowserBlobStorage } from '../assets/browser-blob-storage'\nimport { getConvexClient, getReactiveConvexClient } from './convex-client'\nimport { ConvexPersistenceAdapter } from './convex-adapter'\nimport { resolveBackendConfig } from './backend-mode'\nimport type { StudioBackendMode } from './backend-mode'\n\n/**\n * Select the persistence backend (EC-153, vision AD-1). **Convex is the\n * default**: a configured `VITE_CONVEX_URL` is required unless the explicit\n * playground flag (`VITE_ELYTRA_MODE=playground`) opts into the deterministic,\n * seeded in-memory adapter. There is **no automatic in-memory fallback** —\n * missing configuration is a hard, descriptive error (the `DeploymentSetupGate`\n * renders it as a boot-error screen before any adapter is requested).\n *\n * EC-150: the Convex path uses the *shared* client so the auth adapter's\n * session JWT (set via `client.setAuth`) authenticates every persistence call.\n */\nexport async function selectPersistenceAdapter(): Promise<PersistenceAdapter> {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') {\n // EC-583: no collections here — the store holds no schema; the operations\n // context carries the config's collections (`createStudioOperations`).\n const url = resolveDeploymentUrl(config.url)\n return new ConvexPersistenceAdapter(getConvexClient(url), resolveDeploymentProjectId('aurora-marketing'), {\n // EC-595: the document subscription's reactive client — built lazily on the first\n // subscribe, authenticated from the same URL's client (the token it mirrors).\n reactive: () => getReactiveConvexClient(url),\n })\n }\n if (config.kind === 'playground') {\n // EC-207: routes/redirects are config-sourced, never seeded to the backend —\n // the playground adapter no longer seeds route documents.\n // EC-310: key the seed under the ACTIVE project id (the config slug, e.g.\n // `aurora`) — not the engine's default `prj_aurora` — so content + memberships\n // sit under the same id the studio operates on. Otherwise authorization denies\n // every read (\"not a member of project 'aurora'\") and content resolves empty.\n const projectId = resolveDeploymentProjectId(SEED_PROJECT_ID)\n // EC-361/EC-424: when a config project declares collections, the seed is\n // reconciled against them (a field the config declares localized but the shared\n // fixture stores plain — this repo's demo `post.slug`, EC-315 — is promoted into\n // `localized`), and only their documents are written (EC-606). EC-583: the\n // collections are the seed's LENS only — the store holds no schema; every\n // schema reader (hydration, the CMS surfaces, the operations context) reads the\n // config itself. Without a config (engine fixtures/tests) the sample schema is\n // the lens.\n const collections = resolveDeploymentCollections()\n // EC-459: seed with a studio-owned blob adapter that mints real browser-\n // displayable URLs (object URLs) instead of the engine default's opaque\n // `memory://` locator — see `browser-blob-storage.ts` for the full\n // rationale. Playground-only; the Convex branch above is untouched.\n const adapter = await createSeededPersistenceAdapter(\n projectId,\n collections.length > 0 ? collections : undefined,\n { blobs: createBrowserBlobStorage() },\n )\n return adapter\n }\n throw new Error(config.detail)\n}\n\n/**\n * A stable key identifying the active backend (EC-179): the resolved deployment\n * URL for the active project under Convex, or `playground`. The operations\n * bootstrap caches one bundle per key, so switching to a project on a different\n * deployment transparently selects (or builds) that backend's operations client.\n */\nexport function activeDeploymentKey(): string {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') return resolveDeploymentUrl(config.url)\n if (config.kind === 'playground') return 'playground'\n throw new Error(config.detail)\n}\n\n/**\n * The active backend mode. Throws the same descriptive error as\n * {@link selectPersistenceAdapter} when the studio is misconfigured — UI that\n * only needs \"is this the playground?\" should use `isPlaygroundMode()`.\n */\nexport function persistenceMode(): StudioBackendMode {\n const config = resolveBackendConfig()\n if (config.kind === 'misconfigured') throw new Error(config.detail)\n return config.kind\n}\n\nexport { ConvexPersistenceAdapter } from './convex-adapter'\nexport {\n CONVEX_URL_ENV_VAR,\n DEV_LOGIN_ENV_VAR,\n MODE_ENV_VAR,\n PLAYGROUND_MODE,\n STUDIO_ENV_KEYS,\n detectStudioEnvAnomaly,\n isDevAutoLoginActive,\n isDevRuntime,\n isPlaygroundMode,\n provideStudioEnv,\n resetProvidedStudioEnvForTests,\n resolveBackendConfig,\n resolveDevAutoLogin,\n studioEnv,\n} from './backend-mode'\nexport type {\n DevAutoLoginCredentials,\n EnvRecord,\n StudioBackendConfig,\n StudioBackendMode,\n StudioEnvAnomaly,\n} from './backend-mode'\n// EC-258: the deployment-readiness facts mapper (the boot DeploymentSetupGate + a\n// future `elytra doctor` CLI compose the same `checkDeploymentReady` contract).\nexport { studioDeploymentReadiness, studioReadinessFacts } from './deployment-readiness'\nexport type { StudioReadinessInputs } from './deployment-readiness'\n"]}
@@ -30,7 +30,8 @@ import { humanizeName } from '../utils.js';
30
30
  * as the known-positive control that the search finds a render site when one exists.
31
31
  *
32
32
  * The graph table was unreachable structurally, not just unused: the ONE live path from
33
- * graph validation into the studio is `cms-composition-validators.ts`, which maps a
33
+ * graph validation into the studio was `cms-composition-validators.ts` (since EC-582
34
+ * `@elytracms/core/validation`'s `compositionValidators`), which maps a
34
35
  * graph issue to `CompositionIssue` — a type with `message`/`severity`/`nodePath` and
35
36
  * **no `code` field at all** (`cms-core/validate-document.ts:809`). The code an
36
37
  * exhaustiveness guard exists to force a label for cannot reach an editor surface.
@@ -75,6 +76,7 @@ const CMS_ISSUE_LABEL_KEYS = {
75
76
  'missing-localized-value': 'cms.issue.missing-localized-value',
76
77
  'route-conflict': 'cms.issue.route-conflict',
77
78
  'redirect-loop': 'cms.issue.redirect-loop',
79
+ 'invalid-redirect': 'cms.issue.invalid-redirect',
78
80
  'unknown-route-target': 'cms.issue.unknown-route-target',
79
81
  'hierarchy-cycle': 'cms.issue.hierarchy-cycle',
80
82
  'document-nesting-depth': 'cms.issue.document-nesting-depth',
@@ -86,6 +88,7 @@ const CMS_ISSUE_LABEL_KEYS = {
86
88
  'hidden-predicate-error': 'cms.issue.hidden-predicate-error',
87
89
  'unaddressable-url': 'cms.issue.unaddressable-url',
88
90
  'unknown-version': 'cms.issue.unknown-version',
91
+ 'invalid-stored-document': 'cms.issue.invalid-stored-document',
89
92
  };
90
93
  /**
91
94
  * EC-507 rider — the few codes whose TITLE overstates at `warning` severity.
@@ -1 +1 @@
1
- {"version":3,"file":"labels.js","sourceRoot":"","sources":["../../../src/lib/persona/labels.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CmsIssueCode, CmsIssueSeverity, CmsValidationIssue } from '@elytracms/core/cms-core'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { OperationError, OperationErrorCode, OperationIssue } from '@elytracms/core/operations'\nimport type { BuildFragmentError, FragmentParseError } from '@elytracms/core/clipboard'\nimport type { ContentQueryError } from '@elytracms/core/content'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '../utils'\n\n/**\n * Persona-aware labels (EC-192 item 2, Milestone B promise).\n *\n * A non-technical editor must never see a manifest/binding/path-token/validation\n * identifier on — or one click off — the editor path. The engine speaks in stable\n * machine codes (`cmsIssueCodeSchema`, `operationErrorCodeSchema`) and machine ids\n * (`Heading`, `r-blog-slug-en`); this module is the single translation layer the\n * editor-facing surfaces route through.\n *\n * The maps are exhaustive `Record<…Code, string>` over the Zod enums, so adding a\n * code in `@elytracms/core/cms-core` or `@elytracms/core/operations`\n * fails typecheck here until it is given an editor-facing label — the persona\n * boundary cannot silently regress. A table is only worth that guard while something\n * renders it: EC-509 deleted two that nothing did (see below). Engineers keep the raw\n * code: every surface that\n * shows a label also carries the code in a `title=`/`data-*` attribute (inspectable\n * via hover/devtools, never rendered as visible copy).\n */\n\n/**\n * EC-509 — two label tables were DELETED here rather than localized, because measuring\n * first showed nothing rendered them.\n *\n * `GRAPH_ISSUE_LABELS`/`graphIssueLabel` (16 entries, project-graph `IssueCode`) and\n * `CONDITION_OPERATOR_LABELS`/`conditionOperatorLabel` (8, `ConditionOperator`) had\n * **zero call sites** outside this file and its own test — measured by grep over\n * `packages/` + `apps/`, with `cmsIssueLabel` (4 sites) and `operationErrorLabel` (1)\n * as the known-positive control that the search finds a render site when one exists.\n *\n * The graph table was unreachable structurally, not just unused: the ONE live path from\n * graph validation into the studio is `cms-composition-validators.ts`, which maps a\n * graph issue to `CompositionIssue` — a type with `message`/`severity`/`nodePath` and\n * **no `code` field at all** (`cms-core/validate-document.ts:809`). The code an\n * exhaustiveness guard exists to force a label for cannot reach an editor surface.\n * `ConditionOperator` is live in the runtime renderer's condition gate\n * (`runtime-renderer/render.tsx:48`) but has no studio surface: v1 canvas is static\n * props, editor bindings parked, so no inspector ever shows an operator to name.\n *\n * **This overturns an EC-210 decision** (2026-06-17), which kept `graphIssueLabel` when\n * the validation panel was removed, on the rationale that it was \"the EC-192 boundary's\n * exhaustiveness guard, not panel code\". That was right then and its premise is false\n * now — the guard guards a boundary with no traffic, so it only forced editor copy for\n * strings no editor can see. Localizing them would have added 24 keys to the same\n * effect. If a surface ever renders these codes it needs labels written for it, and\n * will write them localized from the start.\n *\n * Neither symbol is referenced by either consumer repo (measured read-only) and neither\n * appears in docs.\n */\n\n/**\n * EC-443: the CMS issue titles live in the EC-241 message catalog\n * (`cms.issue.<code>`, EN source + shipped de), so the Issues pane / for-review\n * lane localize with the rest of the chrome. This map stays the exhaustive\n * `Record` over the Zod enum — a new code still fails typecheck here until it\n * gets an editor-facing catalog key (the persona boundary cannot silently\n * regress); {@link cmsIssueLabel} resolves it through `translate`.\n */\nconst CMS_ISSUE_LABEL_KEYS: Record<CmsIssueCode, string> = {\n 'duplicate-collection': 'cms.issue.duplicate-collection',\n 'duplicate-field': 'cms.issue.duplicate-field',\n 'unknown-collection': 'cms.issue.unknown-collection',\n 'invalid-collection-config': 'cms.issue.invalid-collection-config',\n 'invalid-field-config': 'cms.issue.invalid-field-config',\n 'reserved-field-name': 'cms.issue.reserved-field-name',\n 'missing-required-field': 'cms.issue.missing-required-field',\n 'invalid-field-value': 'cms.issue.invalid-field-value',\n 'unknown-relation-target': 'cms.issue.unknown-relation-target',\n 'unpublished-relation-target': 'cms.issue.unpublished-relation-target',\n 'cardinality-violation': 'cms.issue.cardinality-violation',\n 'unknown-asset': 'cms.issue.unknown-asset',\n 'duplicate-document': 'cms.issue.duplicate-document',\n 'unknown-locale': 'cms.issue.unknown-locale',\n 'missing-localized-value': 'cms.issue.missing-localized-value',\n 'route-conflict': 'cms.issue.route-conflict',\n 'redirect-loop': 'cms.issue.redirect-loop',\n 'unknown-route-target': 'cms.issue.unknown-route-target',\n 'hierarchy-cycle': 'cms.issue.hierarchy-cycle',\n 'document-nesting-depth': 'cms.issue.document-nesting-depth',\n 'singleton-document-drift': 'cms.issue.singleton-document-drift',\n 'duplicate-alias': 'cms.issue.duplicate-alias',\n 'unknown-alias': 'cms.issue.unknown-alias',\n 'unfilled-slot': 'cms.issue.unfilled-slot',\n 'custom-validation': 'cms.issue.custom-validation',\n 'hidden-predicate-error': 'cms.issue.hidden-predicate-error',\n 'unaddressable-url': 'cms.issue.unaddressable-url',\n 'unknown-version': 'cms.issue.unknown-version',\n}\n\n/**\n * EC-507 rider — the few codes whose TITLE overstates at `warning` severity.\n *\n * `invalid-field-value` reads \"Invalid value\", which is right when the value is\n * rejected (wrong type, not among its options) and wrong when it is not: EC-472\n * deliberately STORES `0202-09-18` as a legal ISO date and merely flags the year as\n * probably unintended. A title calling that invalid contradicts the value the editor\n * can plainly see was accepted, on all three surfaces at once.\n *\n * Keyed on SEVERITY rather than on the implausible-year case specifically, because\n * the overstatement belongs to the severity: any future warning under this code has\n * the same property (the value was kept), and the fix then needs no new entry. A\n * sparse `Partial` on purpose — a code with no entry keeps one title for both\n * severities, which is the correct default and keeps\n * {@link CMS_ISSUE_LABEL_KEYS} the single exhaustive map.\n */\nconst CMS_ISSUE_WARNING_LABEL_KEYS: Partial<Record<CmsIssueCode, string>> = {\n 'invalid-field-value': 'cms.issue.invalid-field-value.warning',\n}\n\n/**\n * EC-494: the badge titles live in the EC-241 catalog (`operations.errorLabel.<code>`),\n * exactly as EC-443 moved the CMS issue titles. **Measured before changing it:** this\n * map was a hard-coded English `Record`, so once the EC-494 bodies localized the banner\n * rendered a German sentence under an English badge — the inverse of the bug this\n * ticket exists for, and not what the ticket said was there. Still the exhaustive\n * `Record` over the Zod enum, so a new code fails typecheck until it has a key.\n */\nconst OPERATION_ERROR_LABEL_KEYS: Record<OperationErrorCode, string> = {\n validation: 'operations.errorLabel.validation',\n 'not-found': 'operations.errorLabel.not-found',\n conflict: 'operations.errorLabel.conflict',\n 'backend-unavailable': 'operations.errorLabel.backend-unavailable',\n unauthorized: 'operations.errorLabel.unauthorized',\n unknown: 'operations.errorLabel.unknown',\n}\n\n/**\n * Plain-language title for a CMS validation issue code — localized against the\n * active studio-UI locale (EC-443). A runtime-unknown code (impossible typewise;\n * a cast could smuggle one) degrades to its humanized form, never the raw enum.\n *\n * `severity` is optional and additive (EC-507): pass the issue's own severity and a\n * code with a softer warning title (see {@link CMS_ISSUE_WARNING_LABEL_KEYS}) uses\n * it; omit it, or pass `'error'`, and nothing changes. Every render site passes it —\n * the parameter is optional so that a pure/unit caller naming only a code still\n * compiles, not because a surface may skip it.\n */\nexport function cmsIssueLabel(code: CmsIssueCode, severity?: CmsIssueSeverity): string {\n const key =\n (severity === 'warning' ? CMS_ISSUE_WARNING_LABEL_KEYS[code] : undefined) ??\n (CMS_ISSUE_LABEL_KEYS[code] as string | undefined)\n return key !== undefined ? translate(key) : humanizeName(code)\n}\n\n/**\n * The localized text of a {@link CustomizationIssue} (EC-478) — the ONE place the\n * studio turns a config-level finding into rendered prose.\n *\n * The producing layer (`studio-core/customization.ts`, `app-views/default-modules.tsx`,\n * `builder/reuse.ts`) cannot own the wording: the EC-236 seam lives in `packages/core`,\n * a leaf of the DAG that may not import this catalog. So it emits a\n * `CustomizationMessageRef` — a stable key plus its interpolation values, all of\n * which are IDENTIFIERS (entry ids, extension points, component ids) and therefore the\n * same in every language — and this resolves it against the active UI locale. Same\n * shape as {@link cmsIssueLabel}, one layer down: the engine speaks codes and keys, the\n * studio speaks the editor's language.\n *\n * `messageRef.reason` fills the `{reason}` slot of `invalid-override`, whose detail is\n * itself prose (a `validate` rejection) and so carries its own key. A ref-less issue —\n * a third-party producer of this published type — degrades to its English\n * `issue.message`, which is honest: a visible English sentence beats a missing key.\n */\nexport function customizationIssueMessage(issue: CustomizationIssue): string {\n const ref = issue.messageRef\n if (!ref) return issue.message\n const vars = { ...ref.vars }\n if (ref.reason) vars.reason = translate(ref.reason.key, ref.reason.vars)\n return translate(ref.key, vars)\n}\n\n/**\n * The localized BODY of a {@link CmsValidationIssue} (EC-487) — the ONE place the\n * studio turns a CMS validation finding into rendered prose, exactly as\n * {@link customizationIssueMessage} does one layer up.\n *\n * EC-443 localized the issue TITLES ({@link cmsIssueLabel}); the bodies stayed English\n * because their producers live in `packages/core`, below this catalog in the DAG. So\n * the producer emits a `CmsMessageRef` — a stable key plus its interpolation values,\n * all of which are IDENTIFIERS (field/collection/route ids, locales, patterns, counts)\n * and therefore the same in every language — and this resolves it against the active\n * UI locale.\n *\n * `messageRef.details` fill placeholders whose text is itself prose, keyed by\n * placeholder name. A placeholder with no entry keeps whatever the producer put in\n * `vars` — the honest outcome for prose the engine did not author (a config-as-code\n * `validate` message, a composition finding), which then stays in its original\n * language rather than vanishing. A ref-less issue — a third-party producer of this\n * published type — degrades to its English `issue.message`: a visible English sentence\n * beats a missing key.\n */\nexport function cmsValidationIssueMessage(issue: CmsValidationIssue): string {\n return resolveMessageRef(issue.messageRef, issue.message)\n}\n\n/**\n * EC-494 — the ONE resolver every carried message ref goes through.\n *\n * Four published types now carry the same `{ key, vars?, details? }` reference —\n * `CmsValidationIssue` (EC-487), `CustomizationIssue` (EC-478, via its own `reason`\n * slot), and now `OperationIssue`/`OperationError` and the clipboard + content-query\n * errors. They are separate declarations only because the EC-265 DAG forbids the\n * lower subpaths from importing `cms-core`; the RESOLUTION is one rule, so it lives\n * in one function.\n *\n * `fallback` is the producer's prebuilt English, rendered whenever there is no ref —\n * a third-party producer, a Zod diagnostic, a persistence-layer message, a\n * config-authored `validate` string. A visible English sentence beats a missing key\n * (EC-443).\n */\nfunction resolveMessageRef(\n ref:\n | {\n readonly key: string\n readonly vars?: Readonly<Record<string, string | number>>\n readonly details?: Readonly<\n Record<string, { readonly key: string; readonly vars?: Readonly<Record<string, string | number>> }>\n >\n }\n | undefined,\n fallback: string,\n): string {\n if (!ref) return fallback\n const vars: Record<string, string | number> = { ...ref.vars }\n for (const [placeholder, nested] of Object.entries(ref.details ?? {})) {\n vars[placeholder] = translate(nested.key, nested.vars)\n }\n return translate(ref.key, vars)\n}\n\n/**\n * The localized text of ONE {@link OperationIssue} line in the error banner (EC-494).\n *\n * These issues cross a process boundary: `operations/documents.ts` rebuilds a\n * `CmsValidationIssue` as an `OperationIssue` **server-side, with no locale**, so the\n * producer cannot translate and the catalog key rides on the wire instead. A ref-less\n * issue — a Zod input diagnostic, a persistence-layer message, a third-party producer\n * of this published type — keeps its English.\n */\nexport function operationIssueMessage(issue: OperationIssue): string {\n return resolveMessageRef(issue.messageRef, issue.message)\n}\n\n/**\n * The localized ENVELOPE sentence of an {@link OperationError} (EC-494) — the line\n * beside the localized badge in `OperationErrorBanner`. Localizing only the issue\n * lines would have left exactly one English sentence under a German badge.\n */\nexport function operationErrorMessage(error: OperationError): string {\n return resolveMessageRef(error.messageRef, error.message)\n}\n\n/**\n * The localized text of a clipboard fragment failure (EC-494) — parse (`corrupt-json`,\n * `foreign-format`, `unsupported-version`, `invalid-payload`) or build\n * (`node-not-found`, `fixture-not-found`), rendered by the builder's paste notice.\n */\nexport function clipboardErrorMessage(error: FragmentParseError | BuildFragmentError): string {\n return resolveMessageRef(error.messageRef, error.message)\n}\n\n/**\n * The localized text of a {@link ContentQueryError} (EC-494) — the filter builder's\n * per-clause line and the canvas listing notice, both of which render errors built by\n * `core/content`'s `validateWhere`/`validateSort`/`validateLimit`.\n */\nexport function contentQueryErrorMessage(error: ContentQueryError): string {\n return resolveMessageRef(error.messageRef, error.message)\n}\n\n/**\n * Plain-language title for an operation failure code — localized against the active\n * studio-UI locale. A runtime-unknown code (impossible typewise; a cast could smuggle\n * one) degrades to the generic title, never the raw enum.\n */\nexport function operationErrorLabel(code: OperationErrorCode): string {\n const key = OPERATION_ERROR_LABEL_KEYS[code] as string | undefined\n return translate(key ?? OPERATION_ERROR_LABEL_KEYS.unknown)\n}\n\n/** The last dot-segment of a component id (`site.Hero` → `Hero`; `Heading` → `Heading`). */\nexport function componentLeafId(componentId: string): string {\n const dot = componentId.lastIndexOf('.')\n return dot === -1 ? componentId : componentId.slice(dot + 1)\n}\n\n/**\n * The human display name of a component TYPE for editor-facing surfaces: the\n * manifest's authored `title` (EC-407: resolved through {@link resolveLabel}, so a\n * `{ en, de }` localized title renders in the active studio-UI locale) when present,\n * else the humanized leaf of its id (`Heading` → `Heading`,\n * `site.MarketingHero` → `Marketing hero`). EC-591: the engine strips no id\n * prefix anywhere else — a label reads the manifest title.\n *\n * `locale` is optional: omit it (the render-site default) to resolve against the\n * active studio-UI locale non-reactively; pass it explicitly for pure/unit callers.\n */\nexport function componentDisplayName(\n componentId: string,\n manifest?: ComponentManifest | undefined,\n locale?: string,\n): string {\n const resolved =\n locale === undefined ? localizedLabel(manifest?.title) : (resolveLabel(manifest?.title, locale) ?? '')\n return resolved || humanizeName(componentLeafId(componentId))\n}\n"]}
1
+ {"version":3,"file":"labels.js","sourceRoot":"","sources":["../../../src/lib/persona/labels.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CmsIssueCode, CmsIssueSeverity, CmsValidationIssue } from '@elytracms/core/cms-core'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { OperationError, OperationErrorCode, OperationIssue } from '@elytracms/core/operations'\nimport type { BuildFragmentError, FragmentParseError } from '@elytracms/core/clipboard'\nimport type { ContentQueryError } from '@elytracms/core/content'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '../utils'\n\n/**\n * Persona-aware labels (EC-192 item 2, Milestone B promise).\n *\n * A non-technical editor must never see a manifest/binding/path-token/validation\n * identifier on — or one click off — the editor path. The engine speaks in stable\n * machine codes (`cmsIssueCodeSchema`, `operationErrorCodeSchema`) and machine ids\n * (`Heading`, `r-blog-slug-en`); this module is the single translation layer the\n * editor-facing surfaces route through.\n *\n * The maps are exhaustive `Record<…Code, string>` over the Zod enums, so adding a\n * code in `@elytracms/core/cms-core` or `@elytracms/core/operations`\n * fails typecheck here until it is given an editor-facing label — the persona\n * boundary cannot silently regress. A table is only worth that guard while something\n * renders it: EC-509 deleted two that nothing did (see below). Engineers keep the raw\n * code: every surface that\n * shows a label also carries the code in a `title=`/`data-*` attribute (inspectable\n * via hover/devtools, never rendered as visible copy).\n */\n\n/**\n * EC-509 — two label tables were DELETED here rather than localized, because measuring\n * first showed nothing rendered them.\n *\n * `GRAPH_ISSUE_LABELS`/`graphIssueLabel` (16 entries, project-graph `IssueCode`) and\n * `CONDITION_OPERATOR_LABELS`/`conditionOperatorLabel` (8, `ConditionOperator`) had\n * **zero call sites** outside this file and its own test — measured by grep over\n * `packages/` + `apps/`, with `cmsIssueLabel` (4 sites) and `operationErrorLabel` (1)\n * as the known-positive control that the search finds a render site when one exists.\n *\n * The graph table was unreachable structurally, not just unused: the ONE live path from\n * graph validation into the studio was `cms-composition-validators.ts` (since EC-582\n * `@elytracms/core/validation`'s `compositionValidators`), which maps a\n * graph issue to `CompositionIssue` — a type with `message`/`severity`/`nodePath` and\n * **no `code` field at all** (`cms-core/validate-document.ts:809`). The code an\n * exhaustiveness guard exists to force a label for cannot reach an editor surface.\n * `ConditionOperator` is live in the runtime renderer's condition gate\n * (`runtime-renderer/render.tsx:48`) but has no studio surface: v1 canvas is static\n * props, editor bindings parked, so no inspector ever shows an operator to name.\n *\n * **This overturns an EC-210 decision** (2026-06-17), which kept `graphIssueLabel` when\n * the validation panel was removed, on the rationale that it was \"the EC-192 boundary's\n * exhaustiveness guard, not panel code\". That was right then and its premise is false\n * now — the guard guards a boundary with no traffic, so it only forced editor copy for\n * strings no editor can see. Localizing them would have added 24 keys to the same\n * effect. If a surface ever renders these codes it needs labels written for it, and\n * will write them localized from the start.\n *\n * Neither symbol is referenced by either consumer repo (measured read-only) and neither\n * appears in docs.\n */\n\n/**\n * EC-443: the CMS issue titles live in the EC-241 message catalog\n * (`cms.issue.<code>`, EN source + shipped de), so the Issues pane / for-review\n * lane localize with the rest of the chrome. This map stays the exhaustive\n * `Record` over the Zod enum — a new code still fails typecheck here until it\n * gets an editor-facing catalog key (the persona boundary cannot silently\n * regress); {@link cmsIssueLabel} resolves it through `translate`.\n */\nconst CMS_ISSUE_LABEL_KEYS: Record<CmsIssueCode, string> = {\n 'duplicate-collection': 'cms.issue.duplicate-collection',\n 'duplicate-field': 'cms.issue.duplicate-field',\n 'unknown-collection': 'cms.issue.unknown-collection',\n 'invalid-collection-config': 'cms.issue.invalid-collection-config',\n 'invalid-field-config': 'cms.issue.invalid-field-config',\n 'reserved-field-name': 'cms.issue.reserved-field-name',\n 'missing-required-field': 'cms.issue.missing-required-field',\n 'invalid-field-value': 'cms.issue.invalid-field-value',\n 'unknown-relation-target': 'cms.issue.unknown-relation-target',\n 'unpublished-relation-target': 'cms.issue.unpublished-relation-target',\n 'cardinality-violation': 'cms.issue.cardinality-violation',\n 'unknown-asset': 'cms.issue.unknown-asset',\n 'duplicate-document': 'cms.issue.duplicate-document',\n 'unknown-locale': 'cms.issue.unknown-locale',\n 'missing-localized-value': 'cms.issue.missing-localized-value',\n 'route-conflict': 'cms.issue.route-conflict',\n 'redirect-loop': 'cms.issue.redirect-loop',\n 'invalid-redirect': 'cms.issue.invalid-redirect',\n 'unknown-route-target': 'cms.issue.unknown-route-target',\n 'hierarchy-cycle': 'cms.issue.hierarchy-cycle',\n 'document-nesting-depth': 'cms.issue.document-nesting-depth',\n 'singleton-document-drift': 'cms.issue.singleton-document-drift',\n 'duplicate-alias': 'cms.issue.duplicate-alias',\n 'unknown-alias': 'cms.issue.unknown-alias',\n 'unfilled-slot': 'cms.issue.unfilled-slot',\n 'custom-validation': 'cms.issue.custom-validation',\n 'hidden-predicate-error': 'cms.issue.hidden-predicate-error',\n 'unaddressable-url': 'cms.issue.unaddressable-url',\n 'unknown-version': 'cms.issue.unknown-version',\n 'invalid-stored-document': 'cms.issue.invalid-stored-document',\n}\n\n/**\n * EC-507 rider — the few codes whose TITLE overstates at `warning` severity.\n *\n * `invalid-field-value` reads \"Invalid value\", which is right when the value is\n * rejected (wrong type, not among its options) and wrong when it is not: EC-472\n * deliberately STORES `0202-09-18` as a legal ISO date and merely flags the year as\n * probably unintended. A title calling that invalid contradicts the value the editor\n * can plainly see was accepted, on all three surfaces at once.\n *\n * Keyed on SEVERITY rather than on the implausible-year case specifically, because\n * the overstatement belongs to the severity: any future warning under this code has\n * the same property (the value was kept), and the fix then needs no new entry. A\n * sparse `Partial` on purpose — a code with no entry keeps one title for both\n * severities, which is the correct default and keeps\n * {@link CMS_ISSUE_LABEL_KEYS} the single exhaustive map.\n */\nconst CMS_ISSUE_WARNING_LABEL_KEYS: Partial<Record<CmsIssueCode, string>> = {\n 'invalid-field-value': 'cms.issue.invalid-field-value.warning',\n}\n\n/**\n * EC-494: the badge titles live in the EC-241 catalog (`operations.errorLabel.<code>`),\n * exactly as EC-443 moved the CMS issue titles. **Measured before changing it:** this\n * map was a hard-coded English `Record`, so once the EC-494 bodies localized the banner\n * rendered a German sentence under an English badge — the inverse of the bug this\n * ticket exists for, and not what the ticket said was there. Still the exhaustive\n * `Record` over the Zod enum, so a new code fails typecheck until it has a key.\n */\nconst OPERATION_ERROR_LABEL_KEYS: Record<OperationErrorCode, string> = {\n validation: 'operations.errorLabel.validation',\n 'not-found': 'operations.errorLabel.not-found',\n conflict: 'operations.errorLabel.conflict',\n 'backend-unavailable': 'operations.errorLabel.backend-unavailable',\n unauthorized: 'operations.errorLabel.unauthorized',\n unknown: 'operations.errorLabel.unknown',\n}\n\n/**\n * Plain-language title for a CMS validation issue code — localized against the\n * active studio-UI locale (EC-443). A runtime-unknown code (impossible typewise;\n * a cast could smuggle one) degrades to its humanized form, never the raw enum.\n *\n * `severity` is optional and additive (EC-507): pass the issue's own severity and a\n * code with a softer warning title (see {@link CMS_ISSUE_WARNING_LABEL_KEYS}) uses\n * it; omit it, or pass `'error'`, and nothing changes. Every render site passes it —\n * the parameter is optional so that a pure/unit caller naming only a code still\n * compiles, not because a surface may skip it.\n */\nexport function cmsIssueLabel(code: CmsIssueCode, severity?: CmsIssueSeverity): string {\n const key =\n (severity === 'warning' ? CMS_ISSUE_WARNING_LABEL_KEYS[code] : undefined) ??\n (CMS_ISSUE_LABEL_KEYS[code] as string | undefined)\n return key !== undefined ? translate(key) : humanizeName(code)\n}\n\n/**\n * The localized text of a {@link CustomizationIssue} (EC-478) — the ONE place the\n * studio turns a config-level finding into rendered prose.\n *\n * The producing layer (`studio-core/customization.ts`, `app-views/default-modules.tsx`,\n * `builder/reuse.ts`) cannot own the wording: the EC-236 seam lives in `packages/core`,\n * a leaf of the DAG that may not import this catalog. So it emits a\n * `CustomizationMessageRef` — a stable key plus its interpolation values, all of\n * which are IDENTIFIERS (entry ids, extension points, component ids) and therefore the\n * same in every language — and this resolves it against the active UI locale. Same\n * shape as {@link cmsIssueLabel}, one layer down: the engine speaks codes and keys, the\n * studio speaks the editor's language.\n *\n * `messageRef.reason` fills the `{reason}` slot of `invalid-override`, whose detail is\n * itself prose (a `validate` rejection) and so carries its own key. A ref-less issue —\n * a third-party producer of this published type — degrades to its English\n * `issue.message`, which is honest: a visible English sentence beats a missing key.\n */\nexport function customizationIssueMessage(issue: CustomizationIssue): string {\n const ref = issue.messageRef\n if (!ref) return issue.message\n const vars = { ...ref.vars }\n if (ref.reason) vars.reason = translate(ref.reason.key, ref.reason.vars)\n return translate(ref.key, vars)\n}\n\n/**\n * The localized BODY of a {@link CmsValidationIssue} (EC-487) — the ONE place the\n * studio turns a CMS validation finding into rendered prose, exactly as\n * {@link customizationIssueMessage} does one layer up.\n *\n * EC-443 localized the issue TITLES ({@link cmsIssueLabel}); the bodies stayed English\n * because their producers live in `packages/core`, below this catalog in the DAG. So\n * the producer emits a `CmsMessageRef` — a stable key plus its interpolation values,\n * all of which are IDENTIFIERS (field/collection/route ids, locales, patterns, counts)\n * and therefore the same in every language — and this resolves it against the active\n * UI locale.\n *\n * `messageRef.details` fill placeholders whose text is itself prose, keyed by\n * placeholder name. A placeholder with no entry keeps whatever the producer put in\n * `vars` — the honest outcome for prose the engine did not author (a config-as-code\n * `validate` message, a composition finding), which then stays in its original\n * language rather than vanishing. A ref-less issue — a third-party producer of this\n * published type — degrades to its English `issue.message`: a visible English sentence\n * beats a missing key.\n */\nexport function cmsValidationIssueMessage(issue: CmsValidationIssue): string {\n return resolveMessageRef(issue.messageRef, issue.message)\n}\n\n/**\n * EC-494 — the ONE resolver every carried message ref goes through.\n *\n * Four published types now carry the same `{ key, vars?, details? }` reference —\n * `CmsValidationIssue` (EC-487), `CustomizationIssue` (EC-478, via its own `reason`\n * slot), and now `OperationIssue`/`OperationError` and the clipboard + content-query\n * errors. They are separate declarations only because the EC-265 DAG forbids the\n * lower subpaths from importing `cms-core`; the RESOLUTION is one rule, so it lives\n * in one function.\n *\n * `fallback` is the producer's prebuilt English, rendered whenever there is no ref —\n * a third-party producer, a Zod diagnostic, a persistence-layer message, a\n * config-authored `validate` string. A visible English sentence beats a missing key\n * (EC-443).\n */\nfunction resolveMessageRef(\n ref:\n | {\n readonly key: string\n readonly vars?: Readonly<Record<string, string | number>>\n readonly details?: Readonly<\n Record<string, { readonly key: string; readonly vars?: Readonly<Record<string, string | number>> }>\n >\n }\n | undefined,\n fallback: string,\n): string {\n if (!ref) return fallback\n const vars: Record<string, string | number> = { ...ref.vars }\n for (const [placeholder, nested] of Object.entries(ref.details ?? {})) {\n vars[placeholder] = translate(nested.key, nested.vars)\n }\n return translate(ref.key, vars)\n}\n\n/**\n * The localized text of ONE {@link OperationIssue} line in the error banner (EC-494).\n *\n * These issues cross a process boundary: `operations/documents.ts` rebuilds a\n * `CmsValidationIssue` as an `OperationIssue` **server-side, with no locale**, so the\n * producer cannot translate and the catalog key rides on the wire instead. A ref-less\n * issue — a Zod input diagnostic, a persistence-layer message, a third-party producer\n * of this published type — keeps its English.\n */\nexport function operationIssueMessage(issue: OperationIssue): string {\n return resolveMessageRef(issue.messageRef, issue.message)\n}\n\n/**\n * The localized ENVELOPE sentence of an {@link OperationError} (EC-494) — the line\n * beside the localized badge in `OperationErrorBanner`. Localizing only the issue\n * lines would have left exactly one English sentence under a German badge.\n */\nexport function operationErrorMessage(error: OperationError): string {\n return resolveMessageRef(error.messageRef, error.message)\n}\n\n/**\n * The localized text of a clipboard fragment failure (EC-494) — parse (`corrupt-json`,\n * `foreign-format`, `unsupported-version`, `invalid-payload`) or build\n * (`node-not-found`, `fixture-not-found`), rendered by the builder's paste notice.\n */\nexport function clipboardErrorMessage(error: FragmentParseError | BuildFragmentError): string {\n return resolveMessageRef(error.messageRef, error.message)\n}\n\n/**\n * The localized text of a {@link ContentQueryError} (EC-494) — the filter builder's\n * per-clause line and the canvas listing notice, both of which render errors built by\n * `core/content`'s `validateWhere`/`validateSort`/`validateLimit`.\n */\nexport function contentQueryErrorMessage(error: ContentQueryError): string {\n return resolveMessageRef(error.messageRef, error.message)\n}\n\n/**\n * Plain-language title for an operation failure code — localized against the active\n * studio-UI locale. A runtime-unknown code (impossible typewise; a cast could smuggle\n * one) degrades to the generic title, never the raw enum.\n */\nexport function operationErrorLabel(code: OperationErrorCode): string {\n const key = OPERATION_ERROR_LABEL_KEYS[code] as string | undefined\n return translate(key ?? OPERATION_ERROR_LABEL_KEYS.unknown)\n}\n\n/** The last dot-segment of a component id (`site.Hero` → `Hero`; `Heading` → `Heading`). */\nexport function componentLeafId(componentId: string): string {\n const dot = componentId.lastIndexOf('.')\n return dot === -1 ? componentId : componentId.slice(dot + 1)\n}\n\n/**\n * The human display name of a component TYPE for editor-facing surfaces: the\n * manifest's authored `title` (EC-407: resolved through {@link resolveLabel}, so a\n * `{ en, de }` localized title renders in the active studio-UI locale) when present,\n * else the humanized leaf of its id (`Heading` → `Heading`,\n * `site.MarketingHero` → `Marketing hero`). EC-591: the engine strips no id\n * prefix anywhere else — a label reads the manifest title.\n *\n * `locale` is optional: omit it (the render-site default) to resolve against the\n * active studio-UI locale non-reactively; pass it explicitly for pure/unit callers.\n */\nexport function componentDisplayName(\n componentId: string,\n manifest?: ComponentManifest | undefined,\n locale?: string,\n): string {\n const resolved =\n locale === undefined ? localizedLabel(manifest?.title) : (resolveLabel(manifest?.title, locale) ?? '')\n return resolved || humanizeName(componentLeafId(componentId))\n}\n"]}
@@ -23,7 +23,7 @@ import type { OperationsClient } from '@elytracms/studio/lib/operations';
23
23
  */
24
24
  /**
25
25
  * Trailing-debounce window (ms) for event-driven invalidation: a burst of
26
- * commands — e.g. one `documents.upsert` per keystroke from autosave, or one
26
+ * commands — e.g. a run of `documents.patch` commits from one editor, or one
27
27
  * event per row in a bulk action — coalesces into a single refetch once the
28
28
  * writes settle. (The shared replacement for the per-store `*_RELOAD_DEBOUNCE_MS`.)
29
29
  */
@@ -24,7 +24,7 @@ import { getStudioOperations } from '@elytracms/studio/lib/operations';
24
24
  */
25
25
  /**
26
26
  * Trailing-debounce window (ms) for event-driven invalidation: a burst of
27
- * commands — e.g. one `documents.upsert` per keystroke from autosave, or one
27
+ * commands — e.g. a run of `documents.patch` commits from one editor, or one
28
28
  * event per row in a bulk action — coalesces into a single refetch once the
29
29
  * writes settle. (The shared replacement for the per-store `*_RELOAD_DEBOUNCE_MS`.)
30
30
  */
@@ -1 +1 @@
1
- {"version":3,"file":"operations-query.js","sourceRoot":"","sources":["../../../src/lib/query/operations-query.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n QueryClient,\n QueryClientProvider,\n useQuery,\n useQueryClient,\n} from '@tanstack/react-query'\nimport type { QueryKey, UseQueryResult } from '@tanstack/react-query'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationEvent, OperationsClient } from '@elytracms/studio/lib/operations'\n\n/**\n * EC-308 / AD-17 — the ONE studio client read seam.\n *\n * Every studio **server read** goes through {@link useOperationQuery} over the\n * shared operations client, cached by TanStack Query; invalidation rides the\n * operations event bus into the same cache. This replaces the bespoke per-store\n * `useState` + epoch-guard + debounce + reload machinery that each store used to\n * hand-roll (the patchwork AD-17 calls out). See\n * [`docs/client-data-flow.md`](../../../../../docs/client-data-flow.md) and\n * [`docs/principles.md`](../../../../../docs/principles.md) §12.\n *\n * It is also the **Phase-2 prep** (EC-309): reactivity/multiplayer is added by\n * swapping pull → push *behind this hook* (an adapter `subscribe` capability\n * driving `queryClient.setQueryData`), never at the call sites — so call sites\n * never change when push lands.\n *\n * Deliberately NOT here: the optimistic editing session (the draft being typed,\n * live validation, the table working view-state in `cms-store`). That is client\n * state, not server state, and stays local (principle 12).\n */\n\n/**\n * Trailing-debounce window (ms) for event-driven invalidation: a burst of\n * commands — e.g. one `documents.upsert` per keystroke from autosave, or one\n * event per row in a bulk action — coalesces into a single refetch once the\n * writes settle. (The shared replacement for the per-store `*_RELOAD_DEBOUNCE_MS`.)\n */\nexport const OPERATION_INVALIDATE_DEBOUNCE_MS = 300\n\n/** Studio-tuned cache defaults: invalidation (not polling/focus) drives freshness. */\nfunction createStudioQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 5_000,\n refetchOnWindowFocus: false,\n retry: false,\n },\n },\n })\n}\n\n/**\n * Mounts the single studio `QueryClient` (one per studio lifetime, surviving\n * project switches). Mounted package-owned in `StudioProvider`, above every\n * per-project provider, so every studio hook can reach the cache.\n */\nexport function StudioQueryProvider({ children }: { children: ReactNode }): ReactNode {\n const [client] = useState(createStudioQueryClient)\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>\n}\n\nexport interface UseOperationQueryOptions<T> {\n /** Stable query key; the first element is the domain tag used for grouping. */\n queryKey: QueryKey\n /** Loads the data from the shared operations client. */\n loader: (operations: OperationsClient) => Promise<T>\n /**\n * Which succeeded commands invalidate this query — a list of operation names\n * (`'documents.publish'`) and/or domains (`'documents'`). A succeeded command\n * matches when its `operation` OR `domain` is in the list. Keep it a stable\n * (module-level) array. Omit for a query that only loads once / refetches\n * manually.\n */\n invalidateOn?: readonly string[]\n /** Skip the query (e.g. fixture/playground sessions with no backend). */\n enabled?: boolean\n}\n\n/**\n * One operations-backed, cached, self-invalidating query. Thin wrapper over\n * `useQuery` whose `queryFn` resolves the shared operations client and runs the\n * loader, plus one shared invalidation subscriber (the only place that touches\n * the operations event bus for reads).\n */\nexport function useOperationQuery<T>(options: UseOperationQueryOptions<T>): UseQueryResult<T> {\n const { queryKey, loader, invalidateOn, enabled = true } = options\n const queryClient = useQueryClient()\n\n const result = useQuery<T>({\n queryKey,\n queryFn: async () => {\n const { operations } = await getStudioOperations()\n return loader(operations)\n },\n enabled,\n })\n\n // Stable serializations so an inline `queryKey`/`invalidateOn` doesn't churn the\n // subscription every render (re-subscribe only when their CONTENTS change).\n const keyJson = JSON.stringify(queryKey)\n const invalidateJson = JSON.stringify(invalidateOn ?? [])\n\n useEffect(() => {\n const triggers = new Set<string>(JSON.parse(invalidateJson) as string[])\n if (triggers.size === 0) return\n const key = JSON.parse(keyJson) as QueryKey\n let timer: ReturnType<typeof setTimeout> | undefined\n let unsubscribe: (() => void) | undefined\n let disposed = false\n const matches = (event: OperationEvent): boolean =>\n event.kind === 'command' &&\n event.status === 'succeeded' &&\n (triggers.has(event.operation) || triggers.has(event.domain))\n void getStudioOperations().then((studio) => {\n if (disposed) return\n unsubscribe = studio.operations.events.subscribe((event) => {\n if (!matches(event)) return\n // Trailing-debounce: a burst (per-keystroke autosave, bulk actions)\n // collapses into one invalidation; the active query then refetches once.\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => {\n void queryClient.invalidateQueries({ queryKey: key })\n }, OPERATION_INVALIDATE_DEBOUNCE_MS)\n })\n })\n return () => {\n disposed = true\n if (timer) clearTimeout(timer)\n unsubscribe?.()\n }\n }, [keyJson, invalidateJson, queryClient])\n\n return result\n}\n\n/**\n * The write-side companion to {@link useOperationQuery}: an imperative handle on\n * the studio cache for a store that OWNS its writes. Lets a store apply an\n * EC-301-style optimistic patch (drop/replace one record) instead of re-listing\n * the whole collection on every edit — keeping `@tanstack/react-query` out of the\n * call sites so the seam stays the only place that knows the cache exists.\n */\nexport interface OperationCache {\n /**\n * Patch a cached query in place — no refetch. The EC-301 pattern: a focal-point\n * or alt edit patches the one changed record; it does NOT trigger a full\n * `*.list` round-trip. No-op if nothing is cached under `queryKey` yet.\n */\n patch<T>(queryKey: QueryKey, updater: (prev: T | undefined) => T): void\n /**\n * Invalidate a cached query, refetching it if it is currently active. Returns a\n * promise that settles when the refetch does, so a caller that must sequence a\n * read after a local write (e.g. flush-then-reload) can await it.\n */\n invalidate(queryKey: QueryKey): Promise<void>\n}\n\nexport function useOperationCache(): OperationCache {\n const queryClient = useQueryClient()\n const patch = useCallback(\n <T,>(queryKey: QueryKey, updater: (prev: T | undefined) => T): void => {\n queryClient.setQueryData<T>(queryKey, updater)\n },\n [queryClient],\n )\n const invalidate = useCallback(\n (queryKey: QueryKey): Promise<void> => queryClient.invalidateQueries({ queryKey }),\n [queryClient],\n )\n return useMemo(() => ({ patch, invalidate }), [patch, invalidate])\n}\n\n/**\n * Derive a list store's public `{ status, errorMessage }` from its operation query —\n * the identical mapping the assets/users/redirects rail stores (EC-319) each\n * hand-rolled. `errorMessage` is set only on an error; a store spreads it\n * conditionally onto its public shape.\n */\nexport function listStoreState(query: { isPending: boolean; isError: boolean; error: unknown }): {\n status: 'loading' | 'ready' | 'error'\n errorMessage: string | undefined\n} {\n return {\n status: query.isPending ? 'loading' : query.isError ? 'error' : 'ready',\n errorMessage: query.isError && query.error instanceof Error ? query.error.message : undefined,\n }\n}\n"]}
1
+ {"version":3,"file":"operations-query.js","sourceRoot":"","sources":["../../../src/lib/query/operations-query.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEjE,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,QAAQ,EACR,cAAc,GACf,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAGtE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAA;AAEnD,sFAAsF;AACtF,SAAS,uBAAuB;IAC9B,OAAO,IAAI,WAAW,CAAC;QACrB,cAAc,EAAE;YACd,OAAO,EAAE;gBACP,SAAS,EAAE,KAAK;gBAChB,oBAAoB,EAAE,KAAK;gBAC3B,KAAK,EAAE,KAAK;aACb;SACF;KACF,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAE,QAAQ,EAA2B;IACvE,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,uBAAuB,CAAC,CAAA;IAClD,OAAO,KAAC,mBAAmB,IAAC,MAAM,EAAE,MAAM,YAAG,QAAQ,GAAuB,CAAA;AAC9E,CAAC;AAmBD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAI,OAAoC;IACvE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,OAAO,CAAA;IAClE,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IAEpC,MAAM,MAAM,GAAG,QAAQ,CAAI;QACzB,QAAQ;QACR,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAA;YAClD,OAAO,MAAM,CAAC,UAAU,CAAC,CAAA;QAC3B,CAAC;QACD,OAAO;KACR,CAAC,CAAA;IAEF,iFAAiF;IACjF,4EAA4E;IAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IACxC,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,IAAI,EAAE,CAAC,CAAA;IAEzD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAS,IAAI,CAAC,KAAK,CAAC,cAAc,CAAa,CAAC,CAAA;QACxE,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;YAAE,OAAM;QAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAa,CAAA;QAC3C,IAAI,KAAgD,CAAA;QACpD,IAAI,WAAqC,CAAA;QACzC,IAAI,QAAQ,GAAG,KAAK,CAAA;QACpB,MAAM,OAAO,GAAG,CAAC,KAAqB,EAAW,EAAE,CACjD,KAAK,CAAC,IAAI,KAAK,SAAS;YACxB,KAAK,CAAC,MAAM,KAAK,WAAW;YAC5B,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;QAC/D,KAAK,mBAAmB,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACzC,IAAI,QAAQ;gBAAE,OAAM;YACpB,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;gBACzD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;oBAAE,OAAM;gBAC3B,oEAAoE;gBACpE,yEAAyE;gBACzE,IAAI,KAAK;oBAAE,YAAY,CAAC,KAAK,CAAC,CAAA;gBAC9B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBACtB,KAAK,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAA;gBACvD,CAAC,EAAE,gCAAgC,CAAC,CAAA;YACtC,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,EAAE;YACV,QAAQ,GAAG,IAAI,CAAA;YACf,IAAI,KAAK;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAA;YAC9B,WAAW,EAAE,EAAE,CAAA;QACjB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,CAAC,CAAC,CAAA;IAE1C,OAAO,MAAM,CAAA;AACf,CAAC;AAwBD,MAAM,UAAU,iBAAiB;IAC/B,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IACpC,MAAM,KAAK,GAAG,WAAW,CACvB,CAAK,QAAkB,EAAE,OAAmC,EAAQ,EAAE;QACpE,WAAW,CAAC,YAAY,CAAI,QAAQ,EAAE,OAAO,CAAC,CAAA;IAChD,CAAC,EACD,CAAC,WAAW,CAAC,CACd,CAAA;IACD,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,QAAkB,EAAiB,EAAE,CAAC,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,CAAC,EAClF,CAAC,WAAW,CAAC,CACd,CAAA;IACD,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAA;AACpE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAA+D;IAI5F,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;QACvE,YAAY,EAAE,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;KAC9F,CAAA;AACH,CAAC","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n QueryClient,\n QueryClientProvider,\n useQuery,\n useQueryClient,\n} from '@tanstack/react-query'\nimport type { QueryKey, UseQueryResult } from '@tanstack/react-query'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationEvent, OperationsClient } from '@elytracms/studio/lib/operations'\n\n/**\n * EC-308 / AD-17 — the ONE studio client read seam.\n *\n * Every studio **server read** goes through {@link useOperationQuery} over the\n * shared operations client, cached by TanStack Query; invalidation rides the\n * operations event bus into the same cache. This replaces the bespoke per-store\n * `useState` + epoch-guard + debounce + reload machinery that each store used to\n * hand-roll (the patchwork AD-17 calls out). See\n * [`docs/client-data-flow.md`](../../../../../docs/client-data-flow.md) and\n * [`docs/principles.md`](../../../../../docs/principles.md) §12.\n *\n * It is also the **Phase-2 prep** (EC-309): reactivity/multiplayer is added by\n * swapping pull → push *behind this hook* (an adapter `subscribe` capability\n * driving `queryClient.setQueryData`), never at the call sites — so call sites\n * never change when push lands.\n *\n * Deliberately NOT here: the optimistic editing session (the draft being typed,\n * live validation, the table working view-state in `cms-store`). That is client\n * state, not server state, and stays local (principle 12).\n */\n\n/**\n * Trailing-debounce window (ms) for event-driven invalidation: a burst of\n * commands — e.g. a run of `documents.patch` commits from one editor, or one\n * event per row in a bulk action — coalesces into a single refetch once the\n * writes settle. (The shared replacement for the per-store `*_RELOAD_DEBOUNCE_MS`.)\n */\nexport const OPERATION_INVALIDATE_DEBOUNCE_MS = 300\n\n/** Studio-tuned cache defaults: invalidation (not polling/focus) drives freshness. */\nfunction createStudioQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 5_000,\n refetchOnWindowFocus: false,\n retry: false,\n },\n },\n })\n}\n\n/**\n * Mounts the single studio `QueryClient` (one per studio lifetime, surviving\n * project switches). Mounted package-owned in `StudioProvider`, above every\n * per-project provider, so every studio hook can reach the cache.\n */\nexport function StudioQueryProvider({ children }: { children: ReactNode }): ReactNode {\n const [client] = useState(createStudioQueryClient)\n return <QueryClientProvider client={client}>{children}</QueryClientProvider>\n}\n\nexport interface UseOperationQueryOptions<T> {\n /** Stable query key; the first element is the domain tag used for grouping. */\n queryKey: QueryKey\n /** Loads the data from the shared operations client. */\n loader: (operations: OperationsClient) => Promise<T>\n /**\n * Which succeeded commands invalidate this query — a list of operation names\n * (`'documents.publish'`) and/or domains (`'documents'`). A succeeded command\n * matches when its `operation` OR `domain` is in the list. Keep it a stable\n * (module-level) array. Omit for a query that only loads once / refetches\n * manually.\n */\n invalidateOn?: readonly string[]\n /** Skip the query (e.g. fixture/playground sessions with no backend). */\n enabled?: boolean\n}\n\n/**\n * One operations-backed, cached, self-invalidating query. Thin wrapper over\n * `useQuery` whose `queryFn` resolves the shared operations client and runs the\n * loader, plus one shared invalidation subscriber (the only place that touches\n * the operations event bus for reads).\n */\nexport function useOperationQuery<T>(options: UseOperationQueryOptions<T>): UseQueryResult<T> {\n const { queryKey, loader, invalidateOn, enabled = true } = options\n const queryClient = useQueryClient()\n\n const result = useQuery<T>({\n queryKey,\n queryFn: async () => {\n const { operations } = await getStudioOperations()\n return loader(operations)\n },\n enabled,\n })\n\n // Stable serializations so an inline `queryKey`/`invalidateOn` doesn't churn the\n // subscription every render (re-subscribe only when their CONTENTS change).\n const keyJson = JSON.stringify(queryKey)\n const invalidateJson = JSON.stringify(invalidateOn ?? [])\n\n useEffect(() => {\n const triggers = new Set<string>(JSON.parse(invalidateJson) as string[])\n if (triggers.size === 0) return\n const key = JSON.parse(keyJson) as QueryKey\n let timer: ReturnType<typeof setTimeout> | undefined\n let unsubscribe: (() => void) | undefined\n let disposed = false\n const matches = (event: OperationEvent): boolean =>\n event.kind === 'command' &&\n event.status === 'succeeded' &&\n (triggers.has(event.operation) || triggers.has(event.domain))\n void getStudioOperations().then((studio) => {\n if (disposed) return\n unsubscribe = studio.operations.events.subscribe((event) => {\n if (!matches(event)) return\n // Trailing-debounce: a burst (per-keystroke autosave, bulk actions)\n // collapses into one invalidation; the active query then refetches once.\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => {\n void queryClient.invalidateQueries({ queryKey: key })\n }, OPERATION_INVALIDATE_DEBOUNCE_MS)\n })\n })\n return () => {\n disposed = true\n if (timer) clearTimeout(timer)\n unsubscribe?.()\n }\n }, [keyJson, invalidateJson, queryClient])\n\n return result\n}\n\n/**\n * The write-side companion to {@link useOperationQuery}: an imperative handle on\n * the studio cache for a store that OWNS its writes. Lets a store apply an\n * EC-301-style optimistic patch (drop/replace one record) instead of re-listing\n * the whole collection on every edit — keeping `@tanstack/react-query` out of the\n * call sites so the seam stays the only place that knows the cache exists.\n */\nexport interface OperationCache {\n /**\n * Patch a cached query in place — no refetch. The EC-301 pattern: a focal-point\n * or alt edit patches the one changed record; it does NOT trigger a full\n * `*.list` round-trip. No-op if nothing is cached under `queryKey` yet.\n */\n patch<T>(queryKey: QueryKey, updater: (prev: T | undefined) => T): void\n /**\n * Invalidate a cached query, refetching it if it is currently active. Returns a\n * promise that settles when the refetch does, so a caller that must sequence a\n * read after a local write (e.g. flush-then-reload) can await it.\n */\n invalidate(queryKey: QueryKey): Promise<void>\n}\n\nexport function useOperationCache(): OperationCache {\n const queryClient = useQueryClient()\n const patch = useCallback(\n <T,>(queryKey: QueryKey, updater: (prev: T | undefined) => T): void => {\n queryClient.setQueryData<T>(queryKey, updater)\n },\n [queryClient],\n )\n const invalidate = useCallback(\n (queryKey: QueryKey): Promise<void> => queryClient.invalidateQueries({ queryKey }),\n [queryClient],\n )\n return useMemo(() => ({ patch, invalidate }), [patch, invalidate])\n}\n\n/**\n * Derive a list store's public `{ status, errorMessage }` from its operation query —\n * the identical mapping the assets/users/redirects rail stores (EC-319) each\n * hand-rolled. `errorMessage` is set only on an error; a store spreads it\n * conditionally onto its public shape.\n */\nexport function listStoreState(query: { isPending: boolean; isError: boolean; error: unknown }): {\n status: 'loading' | 'ready' | 'error'\n errorMessage: string | undefined\n} {\n return {\n status: query.isPending ? 'loading' : query.isError ? 'error' : 'ready',\n errorMessage: query.isError && query.error instanceof Error ? query.error.message : undefined,\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
- import type { CollectionDef, DocumentRef, RelationFieldDef } from '@elytracms/core/cms-core';
1
+ import type { DocumentRef } from '@elytracms/core/cms-core';
2
+ import type { ConflictReferrer } from '@elytracms/core/operations';
2
3
  import type { ReferenceEntryView } from './reference-model.js';
3
4
  /**
4
5
  * Pure impact model for the EC-173 safe-unpublish/delete flows: given the
@@ -12,26 +13,30 @@ import type { ReferenceEntryView } from './reference-model.js';
12
13
  * draft-only reference never serves the published site, so it neither
13
14
  * counts nor blocks.
14
15
  * - **delete** breaks both variants — the stored value dangles either way.
15
- * - An entry **blocks** when its holding field is a relation declared
16
- * `strict: true` in the schema (and, for unpublish, it is published).
17
- * - Entries whose *source* is itself among the acted-on documents are
18
- * excluded: a reference between two documents deleted/unpublished together
19
- * does not survive the action, so it cannot break.
16
+ * - **Blocking is the guard's own answer** (EC-589): the strict referrers the core
17
+ * `documents.delete` / `unpublish` guard computes (`references.strictReferrers` — THE
18
+ * reference walk over live drafts and pinned snapshots, every position, severity off
19
+ * the declaration). The preview used to re-derive it from the index path's first
20
+ * field and the top-level `collection.fields`, so a nested strict relation never
21
+ * showed as blocking while the core refused (round3-D), and a bulk selection
22
+ * excluded acted-on referrers the core still met (IDENTITY #4).
23
+ * - Entries whose *source* is itself among the acted-on documents do not count as
24
+ * broken: a reference between two documents deleted/unpublished together does not
25
+ * survive the action. They still BLOCK when the guard says so — the core runs one
26
+ * operation per document and meets the referrer.
20
27
  */
21
28
  export type ReferenceAction = 'unpublish' | 'delete';
22
29
  /** Does this entry break (dangle / fall back) when `action` proceeds? */
23
30
  export declare function entryBreaksOnAction(entry: ReferenceEntryView, action: ReferenceAction): boolean;
24
- /** The relation field holding a document-sourced relation entry, if resolvable. */
25
- export declare function relationFieldOfEntry(collections: readonly CollectionDef[], entry: ReferenceEntryView): RelationFieldDef | undefined;
26
- /** Does this entry *block* the action (strict opt-in, EC-173)? */
27
- export declare function entryBlocksAction(collections: readonly CollectionDef[], entry: ReferenceEntryView, action: ReferenceAction): boolean;
28
31
  /** The impact of one action on one target document. */
29
32
  export interface DocumentReferenceImpact {
30
33
  ref: DocumentRef;
31
34
  /** Inbound entries that will break (fallbacks + validation issues). */
32
35
  broken: ReferenceEntryView[];
33
- /** The subset held by strict relation fields — these block the action. */
36
+ /** The inbound entries the guard's strict referrers hold — these block the action. */
34
37
  blocking: ReferenceEntryView[];
38
+ /** The guard's strict referrers themselves (EC-589) — the count the core refuses with. */
39
+ strictReferrers: readonly ConflictReferrer[];
35
40
  }
36
41
  /** The aggregate impact of one action over a set of target documents. */
37
42
  export interface ReferenceImpact {
@@ -42,13 +47,14 @@ export interface ReferenceImpact {
42
47
  totalBlocking: number;
43
48
  }
44
49
  /**
45
- * Build the impact of `action` over `targets` from their raw inbound entry
46
- * views (keyed by `documentKey`). Sources that are themselves targets are
47
- * excluded — they do not survive the action.
50
+ * Build the impact of `action` over `targets` from their raw inbound entry views
51
+ * (keyed by `documentKey`) and the guard's strict referrers per target. Sources that
52
+ * are themselves targets do not count as broken (they do not survive the action).
48
53
  */
49
- export declare function buildReferenceImpact(collections: readonly CollectionDef[], action: ReferenceAction, targets: readonly {
54
+ export declare function buildReferenceImpact(action: ReferenceAction, targets: readonly {
50
55
  ref: DocumentRef;
51
56
  inbound: readonly ReferenceEntryView[];
57
+ strictReferrers?: readonly ConflictReferrer[];
52
58
  }[]): ReferenceImpact;
53
59
  /**
54
60
  * Impact of deleting an ASSET (EC-202), reusing the document break/block model.
@@ -1,48 +1,35 @@
1
1
  import { documentKey } from '@elytracms/core/cms-core';
2
- import { parseDocumentFieldPath, parseDocumentKey } from './reference-model.js';
3
2
  /** Does this entry break (dangle / fall back) when `action` proceeds? */
4
3
  export function entryBreaksOnAction(entry, action) {
5
4
  return action === 'delete' ? entry.draft || entry.published : entry.published;
6
5
  }
7
- /** The relation field holding a document-sourced relation entry, if resolvable. */
8
- export function relationFieldOfEntry(collections, entry) {
9
- if (entry.kind !== 'relation-field' || entry.source.type !== 'document')
10
- return undefined;
11
- const sourceRef = parseDocumentKey(entry.source.id);
12
- const parsedPath = parseDocumentFieldPath(entry.path);
13
- if (!sourceRef || !parsedPath)
14
- return undefined;
15
- const collection = collections.find((c) => c.id === sourceRef.collection);
16
- const field = collection?.fields.find((f) => f.name === parsedPath.field);
17
- return field?.type === 'relation' ? field : undefined;
18
- }
19
- /** Does this entry *block* the action (strict opt-in, EC-173)? */
20
- export function entryBlocksAction(collections, entry, action) {
21
- if (!entryBreaksOnAction(entry, action))
22
- return false;
23
- return relationFieldOfEntry(collections, entry)?.strict === true;
6
+ /** The (source, path) identity a guard referrer and an index entry share. */
7
+ function referrerKey(sourceId, path) {
8
+ return `${sourceId}\n${path}`;
24
9
  }
25
10
  /**
26
- * Build the impact of `action` over `targets` from their raw inbound entry
27
- * views (keyed by `documentKey`). Sources that are themselves targets are
28
- * excluded — they do not survive the action.
11
+ * Build the impact of `action` over `targets` from their raw inbound entry views
12
+ * (keyed by `documentKey`) and the guard's strict referrers per target. Sources that
13
+ * are themselves targets do not count as broken (they do not survive the action).
29
14
  */
30
- export function buildReferenceImpact(collections, action, targets) {
15
+ export function buildReferenceImpact(action, targets) {
31
16
  const actedKeys = new Set(targets.map((target) => documentKey(target.ref)));
32
17
  const documents = targets.map((target) => {
33
18
  const surviving = target.inbound.filter((entry) => !(entry.source.type === 'document' && actedKeys.has(entry.source.id)));
34
- const broken = surviving.filter((entry) => entryBreaksOnAction(entry, action));
19
+ const strictReferrers = target.strictReferrers ?? [];
20
+ const strictKeys = new Set(strictReferrers.map((referrer) => referrerKey(documentKey(referrer), referrer.path)));
35
21
  return {
36
22
  ref: target.ref,
37
- broken,
38
- blocking: broken.filter((entry) => entryBlocksAction(collections, entry, action)),
23
+ broken: surviving.filter((entry) => entryBreaksOnAction(entry, action)),
24
+ blocking: target.inbound.filter((entry) => entry.source.type === 'document' && strictKeys.has(referrerKey(entry.source.id, entry.path))),
25
+ strictReferrers,
39
26
  };
40
27
  });
41
28
  return {
42
29
  action,
43
30
  documents,
44
31
  totalBroken: documents.reduce((sum, doc) => sum + doc.broken.length, 0),
45
- totalBlocking: documents.reduce((sum, doc) => sum + doc.blocking.length, 0),
32
+ totalBlocking: documents.reduce((sum, doc) => sum + doc.strictReferrers.length, 0),
46
33
  };
47
34
  }
48
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"impact.js","sourceRoot":"","sources":["../../../src/lib/references/impact.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentKey } from '@elytracms/core/cms-core'\nimport type { CollectionDef, DocumentRef, RelationFieldDef } from '@elytracms/core/cms-core'\nimport { parseDocumentFieldPath, parseDocumentKey } from './reference-model'\nimport type { ReferenceEntryView } from './reference-model'\n\n/**\n * Pure impact model for the EC-173 safe-unpublish/delete flows: given the\n * inbound reference entries of one or more target documents (the EC-172 panel\n * views), compute what *breaks* when the action proceeds and what *blocks* it\n * outright (strict relation fields). No data fetching, no React — fully\n * deterministic and node-testable, like `reference-model.ts`.\n *\n * Semantics (weak by default, vision 3.6):\n * - **unpublish** breaks only the *published* variant of an entry — a\n * draft-only reference never serves the published site, so it neither\n * counts nor blocks.\n * - **delete** breaks both variants — the stored value dangles either way.\n * - An entry **blocks** when its holding field is a relation declared\n * `strict: true` in the schema (and, for unpublish, it is published).\n * - Entries whose *source* is itself among the acted-on documents are\n * excluded: a reference between two documents deleted/unpublished together\n * does not survive the action, so it cannot break.\n */\n\nexport type ReferenceAction = 'unpublish' | 'delete'\n\n/** Does this entry break (dangle / fall back) when `action` proceeds? */\nexport function entryBreaksOnAction(entry: ReferenceEntryView, action: ReferenceAction): boolean {\n return action === 'delete' ? entry.draft || entry.published : entry.published\n}\n\n/** The relation field holding a document-sourced relation entry, if resolvable. */\nexport function relationFieldOfEntry(\n collections: readonly CollectionDef[],\n entry: ReferenceEntryView,\n): RelationFieldDef | undefined {\n if (entry.kind !== 'relation-field' || entry.source.type !== 'document') return undefined\n const sourceRef = parseDocumentKey(entry.source.id)\n const parsedPath = parseDocumentFieldPath(entry.path)\n if (!sourceRef || !parsedPath) return undefined\n const collection = collections.find((c) => c.id === sourceRef.collection)\n const field = collection?.fields.find((f) => f.name === parsedPath.field)\n return field?.type === 'relation' ? field : undefined\n}\n\n/** Does this entry *block* the action (strict opt-in, EC-173)? */\nexport function entryBlocksAction(\n collections: readonly CollectionDef[],\n entry: ReferenceEntryView,\n action: ReferenceAction,\n): boolean {\n if (!entryBreaksOnAction(entry, action)) return false\n return relationFieldOfEntry(collections, entry)?.strict === true\n}\n\n/** The impact of one action on one target document. */\nexport interface DocumentReferenceImpact {\n ref: DocumentRef\n /** Inbound entries that will break (fallbacks + validation issues). */\n broken: ReferenceEntryView[]\n /** The subset held by strict relation fields — these block the action. */\n blocking: ReferenceEntryView[]\n}\n\n/** The aggregate impact of one action over a set of target documents. */\nexport interface ReferenceImpact {\n action: ReferenceAction\n documents: DocumentReferenceImpact[]\n /** Sum of `broken` across documents (\"M inbound references will break\"). */\n totalBroken: number\n totalBlocking: number\n}\n\n/**\n * Build the impact of `action` over `targets` from their raw inbound entry\n * views (keyed by `documentKey`). Sources that are themselves targets are\n * excluded — they do not survive the action.\n */\nexport function buildReferenceImpact(\n collections: readonly CollectionDef[],\n action: ReferenceAction,\n targets: readonly { ref: DocumentRef; inbound: readonly ReferenceEntryView[] }[],\n): ReferenceImpact {\n const actedKeys = new Set(targets.map((target) => documentKey(target.ref)))\n const documents: DocumentReferenceImpact[] = targets.map((target) => {\n const surviving = target.inbound.filter(\n (entry) => !(entry.source.type === 'document' && actedKeys.has(entry.source.id)),\n )\n const broken = surviving.filter((entry) => entryBreaksOnAction(entry, action))\n return {\n ref: target.ref,\n broken,\n blocking: broken.filter((entry) => entryBlocksAction(collections, entry, action)),\n }\n })\n return {\n action,\n documents,\n totalBroken: documents.reduce((sum, doc) => sum + doc.broken.length, 0),\n totalBlocking: documents.reduce((sum, doc) => sum + doc.blocking.length, 0),\n }\n}\n\n/**\n * Impact of deleting an ASSET (EC-202), reusing the document break/block model.\n * A delete dangles EVERY inbound reference (`delete` breaks both variants), and\n * nothing *blocks*: assets are held by `asset` fields / `asset`-typed props,\n * never by `strict: true` relation fields, so the asset path has no block\n * concept — it is always weak-by-default. Returns the same `{ broken, blocking }`\n * shape the shared EC-173 impact rows render.\n */\nexport function buildAssetReferenceImpact(inbound: readonly ReferenceEntryView[]): {\n broken: ReferenceEntryView[]\n blocking: ReferenceEntryView[]\n} {\n return {\n broken: inbound.filter((entry) => entryBreaksOnAction(entry, 'delete')),\n blocking: [],\n }\n}\n"]}
1
+ {"version":3,"file":"impact.js","sourceRoot":"","sources":["../../../src/lib/references/impact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAgCtD,yEAAyE;AACzE,MAAM,UAAU,mBAAmB,CAAC,KAAyB,EAAE,MAAuB;IACpF,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAA;AAC/E,CAAC;AAED,6EAA6E;AAC7E,SAAS,WAAW,CAAC,QAAgB,EAAE,IAAY;IACjD,OAAO,GAAG,QAAQ,KAAK,IAAI,EAAE,CAAA;AAC/B,CAAC;AAsBD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAuB,EACvB,OAIG;IAEH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAC3E,MAAM,SAAS,GAA8B,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAClE,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CACrC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CACjF,CAAA;QACD,MAAM,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,EAAE,CAAA;QACpD,MAAM,UAAU,GAAG,IAAI,GAAG,CACxB,eAAe,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CACrF,CAAA;QACD,OAAO;YACL,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACvE,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAC7B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,IAAI,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CACxG;YACD,eAAe;SAChB,CAAA;IACH,CAAC,CAAC,CAAA;IACF,OAAO;QACL,MAAM;QACN,SAAS;QACT,WAAW,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACvE,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,MAAM,EAAE,CAAC,CAAC;KACnF,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAsC;IAI9E,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACvE,QAAQ,EAAE,EAAE;KACb,CAAA;AACH,CAAC","sourcesContent":["import { documentKey } from '@elytracms/core/cms-core'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport type { ConflictReferrer } from '@elytracms/core/operations'\nimport type { ReferenceEntryView } from './reference-model'\n\n/**\n * Pure impact model for the EC-173 safe-unpublish/delete flows: given the\n * inbound reference entries of one or more target documents (the EC-172 panel\n * views), compute what *breaks* when the action proceeds and what *blocks* it\n * outright (strict relation fields). No data fetching, no React — fully\n * deterministic and node-testable, like `reference-model.ts`.\n *\n * Semantics (weak by default, vision 3.6):\n * - **unpublish** breaks only the *published* variant of an entry — a\n * draft-only reference never serves the published site, so it neither\n * counts nor blocks.\n * - **delete** breaks both variants — the stored value dangles either way.\n * - **Blocking is the guard's own answer** (EC-589): the strict referrers the core\n * `documents.delete` / `unpublish` guard computes (`references.strictReferrers` — THE\n * reference walk over live drafts and pinned snapshots, every position, severity off\n * the declaration). The preview used to re-derive it from the index path's first\n * field and the top-level `collection.fields`, so a nested strict relation never\n * showed as blocking while the core refused (round3-D), and a bulk selection\n * excluded acted-on referrers the core still met (IDENTITY #4).\n * - Entries whose *source* is itself among the acted-on documents do not count as\n * broken: a reference between two documents deleted/unpublished together does not\n * survive the action. They still BLOCK when the guard says so — the core runs one\n * operation per document and meets the referrer.\n */\n\nexport type ReferenceAction = 'unpublish' | 'delete'\n\n/** Does this entry break (dangle / fall back) when `action` proceeds? */\nexport function entryBreaksOnAction(entry: ReferenceEntryView, action: ReferenceAction): boolean {\n return action === 'delete' ? entry.draft || entry.published : entry.published\n}\n\n/** The (source, path) identity a guard referrer and an index entry share. */\nfunction referrerKey(sourceId: string, path: string): string {\n return `${sourceId}\\n${path}`\n}\n\n/** The impact of one action on one target document. */\nexport interface DocumentReferenceImpact {\n ref: DocumentRef\n /** Inbound entries that will break (fallbacks + validation issues). */\n broken: ReferenceEntryView[]\n /** The inbound entries the guard's strict referrers hold — these block the action. */\n blocking: ReferenceEntryView[]\n /** The guard's strict referrers themselves (EC-589) — the count the core refuses with. */\n strictReferrers: readonly ConflictReferrer[]\n}\n\n/** The aggregate impact of one action over a set of target documents. */\nexport interface ReferenceImpact {\n action: ReferenceAction\n documents: DocumentReferenceImpact[]\n /** Sum of `broken` across documents (\"M inbound references will break\"). */\n totalBroken: number\n totalBlocking: number\n}\n\n/**\n * Build the impact of `action` over `targets` from their raw inbound entry views\n * (keyed by `documentKey`) and the guard's strict referrers per target. Sources that\n * are themselves targets do not count as broken (they do not survive the action).\n */\nexport function buildReferenceImpact(\n action: ReferenceAction,\n targets: readonly {\n ref: DocumentRef\n inbound: readonly ReferenceEntryView[]\n strictReferrers?: readonly ConflictReferrer[]\n }[],\n): ReferenceImpact {\n const actedKeys = new Set(targets.map((target) => documentKey(target.ref)))\n const documents: DocumentReferenceImpact[] = targets.map((target) => {\n const surviving = target.inbound.filter(\n (entry) => !(entry.source.type === 'document' && actedKeys.has(entry.source.id)),\n )\n const strictReferrers = target.strictReferrers ?? []\n const strictKeys = new Set(\n strictReferrers.map((referrer) => referrerKey(documentKey(referrer), referrer.path)),\n )\n return {\n ref: target.ref,\n broken: surviving.filter((entry) => entryBreaksOnAction(entry, action)),\n blocking: target.inbound.filter(\n (entry) => entry.source.type === 'document' && strictKeys.has(referrerKey(entry.source.id, entry.path)),\n ),\n strictReferrers,\n }\n })\n return {\n action,\n documents,\n totalBroken: documents.reduce((sum, doc) => sum + doc.broken.length, 0),\n totalBlocking: documents.reduce((sum, doc) => sum + doc.strictReferrers.length, 0),\n }\n}\n\n/**\n * Impact of deleting an ASSET (EC-202), reusing the document break/block model.\n * A delete dangles EVERY inbound reference (`delete` breaks both variants), and\n * nothing *blocks*: assets are held by `asset` fields / `asset`-typed props,\n * never by `strict: true` relation fields, so the asset path has no block\n * concept — it is always weak-by-default. Returns the same `{ broken, blocking }`\n * shape the shared EC-173 impact rows render.\n */\nexport function buildAssetReferenceImpact(inbound: readonly ReferenceEntryView[]): {\n broken: ReferenceEntryView[]\n blocking: ReferenceEntryView[]\n} {\n return {\n broken: inbound.filter((entry) => entryBreaksOnAction(entry, 'delete')),\n blocking: [],\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { DocumentRef } from '@elytracms/core/cms-core';
2
+ import type { ConflictReferrer } from '@elytracms/core/operations';
2
3
  import type { ReferenceSource, ReferenceTarget } from '@elytracms/core/persistence';
3
4
  import type { OperationEvent, StudioOperations } from '../operations/index.js';
4
5
  import type { ReferenceEntryView } from './reference-model.js';
@@ -49,6 +50,14 @@ export declare function loadInboundReferenceViews(projectId: string, targets: re
49
50
  ref: DocumentRef;
50
51
  inbound: ReferenceEntryView[];
51
52
  }[]>;
53
+ /**
54
+ * The strict referrers of each acted-on document (EC-589) — the core guard's OWN
55
+ * computation (`references.strictReferrers`: THE reference walk over live drafts and
56
+ * pinned snapshots, every position, severity off the declaration), so the dialog blocks
57
+ * exactly when `documents.delete` / `unpublish` would refuse. One list per target, in
58
+ * `targets` order; pair with {@link loadInboundReferenceViews} in `buildReferenceImpact`.
59
+ */
60
+ export declare function loadStrictReferrers(projectId: string, targets: readonly DocumentRef[], action: 'delete' | 'unpublish'): Promise<ConflictReferrer[][]>;
52
61
  /**
53
62
  * Inbound reference views for ONE asset (EC-202): the "what uses this asset"
54
63
  * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference
@@ -3,7 +3,7 @@ import { documentKey } from '@elytracms/core/cms-core';
3
3
  import { useOperationQuery } from '@elytracms/studio/lib/query';
4
4
  import { localizedLabel } from '../i18n/index.js';
5
5
  import { getStudioLocaleConfig } from '../locale/locale-store.js';
6
- import { resolveDeploymentLocaleConfig } from '../workspace/index.js';
6
+ import { resolveDeploymentCollections, resolveDeploymentLocaleConfig } from '../workspace/index.js';
7
7
  import { getStudioOperations } from '../operations/index.js';
8
8
  import { getStudioRegistry } from '../registry/index.js';
9
9
  import { buildReferenceEntryViews } from './reference-model.js';
@@ -52,12 +52,12 @@ export function ensureReferenceIndex(studio, projectId) {
52
52
  }
53
53
  // --- Label context loading -------------------------------------------------------
54
54
  async function loadLabelContext(operations, projectId) {
55
- const [schemaResult, documentsResult, assetsResult] = await Promise.all([
56
- operations.schema.get({ projectId }),
55
+ const [documentsResult, assetsResult] = await Promise.all([
57
56
  operations.documents.list({ projectId }),
58
57
  operations.assets.list({ projectId }),
59
58
  ]);
60
- const collections = schemaResult.ok ? schemaResult.value.collections : [];
59
+ // EC-583 / EC-559: the labels' schema is the config's (the store holds no schema).
60
+ const collections = resolveDeploymentCollections();
61
61
  const documents = new Map();
62
62
  if (documentsResult.ok) {
63
63
  for (const record of documentsResult.value) {
@@ -183,6 +183,26 @@ export async function loadInboundReferenceViews(projectId, targets, toTarget = (
183
183
  inbound: buildReferenceEntryViews(recordsPerTarget[index] ?? [], context),
184
184
  }));
185
185
  }
186
+ /**
187
+ * The strict referrers of each acted-on document (EC-589) — the core guard's OWN
188
+ * computation (`references.strictReferrers`: THE reference walk over live drafts and
189
+ * pinned snapshots, every position, severity off the declaration), so the dialog blocks
190
+ * exactly when `documents.delete` / `unpublish` would refuse. One list per target, in
191
+ * `targets` order; pair with {@link loadInboundReferenceViews} in `buildReferenceImpact`.
192
+ */
193
+ export async function loadStrictReferrers(projectId, targets, action) {
194
+ if (targets.length === 0)
195
+ return [];
196
+ const studio = await getStudioOperations();
197
+ const result = await studio.operations.references.strictReferrers({
198
+ projectId,
199
+ targets: [...targets],
200
+ mode: action,
201
+ });
202
+ if (!result.ok)
203
+ throw new Error(result.error.message);
204
+ return result.value.map((referrers) => [...referrers]);
205
+ }
186
206
  /**
187
207
  * Inbound reference views for ONE asset (EC-202): the "what uses this asset"
188
208
  * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference