@elytracms/studio 0.0.88 → 0.0.91

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 (246) hide show
  1. package/convex/asset-revalidate.test.ts +49 -43
  2. package/convex/cms.ts +3 -4
  3. package/convex/delivery.ts +8 -11
  4. package/convex/document-revalidate.test.ts +188 -0
  5. package/convex/redirect-revalidate.test.ts +50 -37
  6. package/convex/revalidate.ts +18 -30
  7. package/convex/triggers.ts +47 -38
  8. package/convex/webhooks.ts +77 -62
  9. package/dist/components/account/self-account-view-source.js +2 -1
  10. package/dist/components/account/self-account-view-source.js.map +1 -1
  11. package/dist/components/ai/ai-chat-panel.js +1 -1
  12. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  13. package/dist/components/app-views/default-modules.js +3 -3
  14. package/dist/components/app-views/default-modules.js.map +1 -1
  15. package/dist/components/assets/asset-grid.js +1 -1
  16. package/dist/components/assets/asset-grid.js.map +1 -1
  17. package/dist/components/assets/asset-selection.d.ts +8 -9
  18. package/dist/components/assets/asset-selection.js +26 -16
  19. package/dist/components/assets/asset-selection.js.map +1 -1
  20. package/dist/components/assets/assets-collection-view-source.js +3 -2
  21. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  22. package/dist/components/builder/blocks-model.d.ts +7 -5
  23. package/dist/components/builder/blocks-model.js +11 -9
  24. package/dist/components/builder/blocks-model.js.map +1 -1
  25. package/dist/components/builder/blocks-panel.js +1 -1
  26. package/dist/components/builder/blocks-panel.js.map +1 -1
  27. package/dist/components/builder/builder-store.d.ts +11 -3
  28. package/dist/components/builder/builder-store.js +12 -5
  29. package/dist/components/builder/builder-store.js.map +1 -1
  30. package/dist/components/builder/canvas-dnd.d.ts +7 -1
  31. package/dist/components/builder/canvas-dnd.js +6 -3
  32. package/dist/components/builder/canvas-dnd.js.map +1 -1
  33. package/dist/components/builder/canvas-render-context.d.ts +13 -1
  34. package/dist/components/builder/canvas-render-context.js +28 -18
  35. package/dist/components/builder/canvas-render-context.js.map +1 -1
  36. package/dist/components/builder/canvas.js +1 -1
  37. package/dist/components/builder/canvas.js.map +1 -1
  38. package/dist/components/builder/clipboard-actions.d.ts +0 -6
  39. package/dist/components/builder/clipboard-actions.js +9 -4
  40. package/dist/components/builder/clipboard-actions.js.map +1 -1
  41. package/dist/components/builder/composition-field-editor.d.ts +22 -0
  42. package/dist/components/builder/composition-field-editor.js +47 -8
  43. package/dist/components/builder/composition-field-editor.js.map +1 -1
  44. package/dist/components/builder/inline-edit.d.ts +7 -4
  45. package/dist/components/builder/inline-edit.js +11 -8
  46. package/dist/components/builder/inline-edit.js.map +1 -1
  47. package/dist/components/builder/insert-context.js +9 -5
  48. package/dist/components/builder/insert-context.js.map +1 -1
  49. package/dist/components/builder/layout-inspector.d.ts +2 -2
  50. package/dist/components/builder/layout-inspector.js +8 -6
  51. package/dist/components/builder/layout-inspector.js.map +1 -1
  52. package/dist/components/builder/node-factory.d.ts +31 -14
  53. package/dist/components/builder/node-factory.js +73 -93
  54. package/dist/components/builder/node-factory.js.map +1 -1
  55. package/dist/components/builder/page-tree.d.ts +3 -1
  56. package/dist/components/builder/page-tree.js +6 -4
  57. package/dist/components/builder/page-tree.js.map +1 -1
  58. package/dist/components/builder/property-inspector.js +2 -1
  59. package/dist/components/builder/property-inspector.js.map +1 -1
  60. package/dist/components/builder/reuse-context.js +2 -2
  61. package/dist/components/builder/reuse-context.js.map +1 -1
  62. package/dist/components/builder/reuse.d.ts +7 -3
  63. package/dist/components/builder/reuse.js +19 -11
  64. package/dist/components/builder/reuse.js.map +1 -1
  65. package/dist/components/builder/selection-overlay.d.ts +2 -2
  66. package/dist/components/builder/selection-overlay.js +3 -3
  67. package/dist/components/builder/selection-overlay.js.map +1 -1
  68. package/dist/components/builder/spacing-handles.d.ts +2 -2
  69. package/dist/components/builder/spacing-handles.js +3 -3
  70. package/dist/components/builder/spacing-handles.js.map +1 -1
  71. package/dist/components/cms/asset-field.d.ts +1 -1
  72. package/dist/components/cms/asset-field.js +15 -13
  73. package/dist/components/cms/asset-field.js.map +1 -1
  74. package/dist/components/cms/asset-picker-core.js +3 -4
  75. package/dist/components/cms/asset-picker-core.js.map +1 -1
  76. package/dist/components/cms/cell-edit.d.ts +0 -6
  77. package/dist/components/cms/cell-edit.js +5 -23
  78. package/dist/components/cms/cell-edit.js.map +1 -1
  79. package/dist/components/cms/cms-store.d.ts +3 -2
  80. package/dist/components/cms/cms-store.js +23 -24
  81. package/dist/components/cms/cms-store.js.map +1 -1
  82. package/dist/components/cms/collection-table.js +14 -4
  83. package/dist/components/cms/collection-table.js.map +1 -1
  84. package/dist/components/cms/detail-editor.js +28 -24
  85. package/dist/components/cms/detail-editor.js.map +1 -1
  86. package/dist/components/cms/discard-draft.d.ts +1 -2
  87. package/dist/components/cms/discard-draft.js +1 -1
  88. package/dist/components/cms/discard-draft.js.map +1 -1
  89. package/dist/components/cms/duplicate-document.d.ts +3 -2
  90. package/dist/components/cms/duplicate-document.js +5 -5
  91. package/dist/components/cms/duplicate-document.js.map +1 -1
  92. package/dist/components/cms/editable-cell.js +11 -7
  93. package/dist/components/cms/editable-cell.js.map +1 -1
  94. package/dist/components/cms/field-control-lib.d.ts +10 -0
  95. package/dist/components/cms/field-control-lib.js +20 -0
  96. package/dist/components/cms/field-control-lib.js.map +1 -1
  97. package/dist/components/cms/field-control.js +10 -9
  98. package/dist/components/cms/field-control.js.map +1 -1
  99. package/dist/components/cms/object-array-field.js +3 -5
  100. package/dist/components/cms/object-array-field.js.map +1 -1
  101. package/dist/components/cms/prop-fields.d.ts +16 -3
  102. package/dist/components/cms/prop-fields.js +57 -12
  103. package/dist/components/cms/prop-fields.js.map +1 -1
  104. package/dist/components/cms/publish-clean.d.ts +13 -6
  105. package/dist/components/cms/publish-clean.js +14 -6
  106. package/dist/components/cms/publish-clean.js.map +1 -1
  107. package/dist/components/cms/rich-text/embed-props.d.ts +6 -1
  108. package/dist/components/cms/rich-text/embed-props.js +7 -12
  109. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  110. package/dist/components/cms/rich-text/insert.d.ts +13 -6
  111. package/dist/components/cms/rich-text/insert.js +16 -17
  112. package/dist/components/cms/rich-text/insert.js.map +1 -1
  113. package/dist/components/cms/use-cell-edit.js +7 -12
  114. package/dist/components/cms/use-cell-edit.js.map +1 -1
  115. package/dist/components/cms/use-discard-controller.d.ts +1 -2
  116. package/dist/components/cms/use-discard-controller.js +1 -2
  117. package/dist/components/cms/use-discard-controller.js.map +1 -1
  118. package/dist/components/cms/use-publish-controller.js +3 -2
  119. package/dist/components/cms/use-publish-controller.js.map +1 -1
  120. package/dist/components/cms/versions-pane.js +2 -2
  121. package/dist/components/cms/versions-pane.js.map +1 -1
  122. package/dist/components/components/component-detail.d.ts +1 -1
  123. package/dist/components/components/component-detail.js +2 -4
  124. package/dist/components/components/component-detail.js.map +1 -1
  125. package/dist/components/components/component-preview.js +9 -4
  126. package/dist/components/components/component-preview.js.map +1 -1
  127. package/dist/components/components/components-index.d.ts +2 -2
  128. package/dist/components/components/components-index.js +3 -6
  129. package/dist/components/components/components-index.js.map +1 -1
  130. package/dist/components/components/field-inspector.js +5 -2
  131. package/dist/components/components/field-inspector.js.map +1 -1
  132. package/dist/components/components/registry-browser.d.ts +6 -5
  133. package/dist/components/components/registry-browser.js +11 -37
  134. package/dist/components/components/registry-browser.js.map +1 -1
  135. package/dist/components/components/taxonomy.d.ts +14 -21
  136. package/dist/components/components/taxonomy.js +5 -29
  137. package/dist/components/components/taxonomy.js.map +1 -1
  138. package/dist/components/components/transfer-dialog.js +1 -0
  139. package/dist/components/components/transfer-dialog.js.map +1 -1
  140. package/dist/components/components/transfer.d.ts +20 -12
  141. package/dist/components/components/transfer.js +27 -21
  142. package/dist/components/components/transfer.js.map +1 -1
  143. package/dist/components/delivery-health-banner.d.ts +3 -2
  144. package/dist/components/delivery-health-banner.js +3 -2
  145. package/dist/components/delivery-health-banner.js.map +1 -1
  146. package/dist/components/redirects/redirects-collection-view-source.js +3 -2
  147. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  148. package/dist/components/users/users-collection-view-source.js +2 -1
  149. package/dist/components/users/users-collection-view-source.js.map +1 -1
  150. package/dist/lib/ai/apply-content.js +32 -28
  151. package/dist/lib/ai/apply-content.js.map +1 -1
  152. package/dist/lib/ai/constraints.d.ts +16 -14
  153. package/dist/lib/ai/constraints.js +64 -48
  154. package/dist/lib/ai/constraints.js.map +1 -1
  155. package/dist/lib/ai/translate-document.js +11 -7
  156. package/dist/lib/ai/translate-document.js.map +1 -1
  157. package/dist/lib/ai/types.d.ts +9 -5
  158. package/dist/lib/ai/types.js.map +1 -1
  159. package/dist/lib/ai/update-fields.js +15 -32
  160. package/dist/lib/ai/update-fields.js.map +1 -1
  161. package/dist/lib/ai/vite.d.ts +22 -2
  162. package/dist/lib/ai/vite.js +12 -3
  163. package/dist/lib/ai/vite.js.map +1 -1
  164. package/dist/lib/assets/asset-placements.d.ts +3 -10
  165. package/dist/lib/assets/asset-placements.js +17 -8
  166. package/dist/lib/assets/asset-placements.js.map +1 -1
  167. package/dist/lib/clipboard/paste-target.js +3 -3
  168. package/dist/lib/clipboard/paste-target.js.map +1 -1
  169. package/dist/lib/cms/published-documents.d.ts +14 -3
  170. package/dist/lib/cms/published-documents.js +12 -3
  171. package/dist/lib/cms/published-documents.js.map +1 -1
  172. package/dist/lib/cms-views/view-model.d.ts +4 -4
  173. package/dist/lib/cms-views/view-model.js +26 -20
  174. package/dist/lib/cms-views/view-model.js.map +1 -1
  175. package/dist/lib/data/collection-content.d.ts +10 -7
  176. package/dist/lib/data/collection-content.js +70 -29
  177. package/dist/lib/data/collection-content.js.map +1 -1
  178. package/dist/lib/delivery/instant-publish.d.ts +0 -3
  179. package/dist/lib/delivery/instant-publish.js +0 -1
  180. package/dist/lib/delivery/instant-publish.js.map +1 -1
  181. package/dist/lib/dnd/drop-rules.d.ts +22 -7
  182. package/dist/lib/dnd/drop-rules.js +21 -14
  183. package/dist/lib/dnd/drop-rules.js.map +1 -1
  184. package/dist/lib/engine-version.d.ts +1 -1
  185. package/dist/lib/engine-version.js +1 -1
  186. package/dist/lib/engine-version.js.map +1 -1
  187. package/dist/lib/i18n/messages.de.js +25 -9
  188. package/dist/lib/i18n/messages.de.js.map +1 -1
  189. package/dist/lib/i18n/messages.en.d.ts +21 -7
  190. package/dist/lib/i18n/messages.en.js +28 -9
  191. package/dist/lib/i18n/messages.en.js.map +1 -1
  192. package/dist/lib/locale/fallback.d.ts +5 -19
  193. package/dist/lib/locale/fallback.js +18 -39
  194. package/dist/lib/locale/fallback.js.map +1 -1
  195. package/dist/lib/locale/locale-store.js +2 -1
  196. package/dist/lib/locale/locale-store.js.map +1 -1
  197. package/dist/lib/operations/cms-persistence.js +7 -4
  198. package/dist/lib/operations/cms-persistence.js.map +1 -1
  199. package/dist/lib/operations/studio-operations.js +4 -6
  200. package/dist/lib/operations/studio-operations.js.map +1 -1
  201. package/dist/lib/persistence/backend-mode.d.ts +5 -25
  202. package/dist/lib/persistence/backend-mode.js +13 -30
  203. package/dist/lib/persistence/backend-mode.js.map +1 -1
  204. package/dist/lib/persona/labels.d.ts +4 -9
  205. package/dist/lib/persona/labels.js +5 -12
  206. package/dist/lib/persona/labels.js.map +1 -1
  207. package/dist/lib/references/reference-model.d.ts +17 -5
  208. package/dist/lib/references/reference-model.js +20 -28
  209. package/dist/lib/references/reference-model.js.map +1 -1
  210. package/dist/lib/references/references-store.js +7 -0
  211. package/dist/lib/references/references-store.js.map +1 -1
  212. package/dist/lib/registry/__fixtures__/demo-components.d.ts +32 -0
  213. package/dist/lib/registry/{project-components.js → __fixtures__/demo-components.js} +18 -5
  214. package/dist/lib/registry/__fixtures__/demo-components.js.map +1 -0
  215. package/dist/lib/registry/index.d.ts +9 -13
  216. package/dist/lib/registry/index.js +34 -61
  217. package/dist/lib/registry/index.js.map +1 -1
  218. package/dist/lib/registry/preview.d.ts +22 -7
  219. package/dist/lib/registry/preview.js +26 -89
  220. package/dist/lib/registry/preview.js.map +1 -1
  221. package/dist/lib/workspace/index.d.ts +0 -7
  222. package/dist/lib/workspace/index.js +0 -9
  223. package/dist/lib/workspace/index.js.map +1 -1
  224. package/dist/previews/enumerate.d.ts +23 -27
  225. package/dist/previews/enumerate.js +27 -48
  226. package/dist/previews/enumerate.js.map +1 -1
  227. package/dist/previews/harness.js +1 -1
  228. package/dist/previews/harness.js.map +1 -1
  229. package/package.json +4 -7
  230. package/dist/lib/registry/project-components.d.ts +0 -16
  231. package/dist/lib/registry/project-components.js.map +0 -1
  232. package/dist/lib/webhooks/config.d.ts +0 -15
  233. package/dist/lib/webhooks/config.js +0 -65
  234. package/dist/lib/webhooks/config.js.map +0 -1
  235. package/dist/lib/webhooks/dispatcher.d.ts +0 -70
  236. package/dist/lib/webhooks/dispatcher.js +0 -61
  237. package/dist/lib/webhooks/dispatcher.js.map +0 -1
  238. package/dist/lib/webhooks/index.d.ts +0 -11
  239. package/dist/lib/webhooks/index.js +0 -10
  240. package/dist/lib/webhooks/index.js.map +0 -1
  241. package/dist/lib/webhooks/signing.d.ts +0 -15
  242. package/dist/lib/webhooks/signing.js +0 -24
  243. package/dist/lib/webhooks/signing.js.map +0 -1
  244. package/dist/lib/webhooks/tags.d.ts +0 -25
  245. package/dist/lib/webhooks/tags.js +0 -289
  246. package/dist/lib/webhooks/tags.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"update-fields.js","sourceRoot":"","sources":["../../../src/lib/ai/update-fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,aAAa,EACb,gBAAgB,GACjB,MAAM,0BAA0B,CAAA;AAUjC,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAExF,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAA;AAuE/D,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;AACtD,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CACtB,GAAgB,EAChB,SAAiB,EACjB,MAA0B,EAC1B,KAAc;IAEd,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;QACxD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC,SAAS,CAAC,CAAA;;YAC/C,SAAS,CAAC,SAAS,CAAC,GAAG,KAAK,CAAA;QACjC,OAAO,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,CAAA;IACzE,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,CAAA;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC,CAAA;;QAC5C,MAAM,CAAC,SAAS,CAAC,GAAG,KAAK,CAAA;IAC9B,OAAO,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,CAAA;AAC3B,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,GAAgB,EAAE,SAAiB,EAAE,MAA0B;IACrF,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAC5F,CAAC;AAED,oGAAoG;AACpG,SAAS,kBAAkB,CAAC,GAAgB,EAAE,SAAiB;IAC7D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7B,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,EAAE;QACjC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3D,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;YAClC,IAAI,GAAG;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC1B,CAAC;IACH,CAAC,CAAA;IACD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;IAC9B,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC;QAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;IACnF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,0GAA0G;AAC1G,SAAS,eAAe,CAAC,GAAgB,EAAE,SAAiB;IAC1D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7B,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,EAAE;QACjC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3D,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;YAC7B,IAAI,EAAE;gBAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACrB,CAAC;IACH,CAAC,CAAA;IACD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;IAC9B,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC;QAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;IACnF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CACrB,KAAe,EACf,MAAyB,EACzB,QAAqB,EACrB,OAAgB,EAChB,KAA+B,EAC/B,KAAqF;IAErF,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAA;IACxB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA,CAAC,QAAQ;IAC3E,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACjC,OAAO,KAAK,CACV,qBAAqB,EACrB,aAAa,KAAK,CAAC,IAAI,6DAA6D,CACrF,CAAA;YACH,CAAC;YACD,yEAAyE;YACzE,2DAA2D;YAC3D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC;gBACtB,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1D,GAAG,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC;aAC5C,CAAC,CAAA;YACF,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;YACpD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,OAAO,KAAK,CACV,yBAAyB,EACzB,aAAa,KAAK,CAAC,IAAI,6DAA6D,KAAK,CAAC,MAAM,iBAAiB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CACxI,CAAA;YACH,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,OAAO,KAAK,CAAC,qBAAqB,EAAE,UAAU,KAAK,CAAC,IAAI,2CAA2C,CAAC,CAAA;YACtG,CAAC;YACD,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;YAC7E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;QACnD,CAAC;QACD,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACjC,OAAO,KAAK,CACV,qBAAqB,EACrB,gBAAgB,KAAK,CAAC,IAAI,qDAAqD,CAChF,CAAA;YACH,CAAC;YACD,yEAAyE;YACzE,4EAA4E;YAC5E,mDAAmD;YACnD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC;gBACtB,GAAG,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/D,GAAG,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC;aACzC,CAAC,CAAA;YACF,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;YACpD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,OAAO,KAAK,CACV,eAAe,EACf,gBAAgB,KAAK,CAAC,IAAI,qFAAqF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CACtI,CAAA;YACH,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,OAAO,KAAK,CAAC,qBAAqB,EAAE,UAAU,KAAK,CAAC,IAAI,uCAAuC,CAAC,CAAA;YAClG,CAAC;YACD,4EAA4E;YAC5E,yEAAyE;YACzE,0EAA0E;YAC1E,oEAAoE;YACpE,OAAO;gBACL,KAAK,EAAE,oBAAoB,CACzB,OAAO,EACP,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAClF,UAAU,CACX;aACF,CAAA;QACH,CAAC;QACD,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,4EAA4E;YAC5E,0EAA0E;YAC1E,4EAA4E;YAC5E,wEAAwE;YACxE,MAAM,SAAS,GAAG,2BAA2B,CAAC,GAAG,CAAC,CAAA;YAClD,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;gBAClB,OAAO,KAAK,CAAC,qBAAqB,EAAE,UAAU,KAAK,CAAC,IAAI,MAAM,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC/F,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAA;QACnC,CAAC;QACD;YACE,mEAAmE;YACnE,mEAAmE;YACnE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAA;IACzB,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,SAAS,oBAAoB,CAC3B,GAAgB,EAChB,KAAe,EACf,MAAyB,EACzB,MAA0B,EAC1B,KAAqF;IAErF,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,KAAK,CACV,qBAAqB,EACrB,IAAI,MAAM,CAAC,KAAK,iFAAiF,CAClG,CAAA;IACH,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,KAAK,CACV,qBAAqB,EACrB,wBAAwB,MAAM,CAAC,KAAK,sDAAsD,CAC3F,CAAA;IACH,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,OAAO,KAAK,CACV,qBAAqB,EACrB,mBAAmB,MAAM,CAAC,OAAO,oDAAoD,CACtF,CAAA;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAC1D,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,MAAM,CAAC,OAAO,CAAC,CAAA;IACtE,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,OAAO,KAAK,CACV,qBAAqB,EACrB,UAAU,MAAM,CAAC,OAAO,uBAAuB,MAAM,CAAC,KAAK,+DAA+D,CAC3H,CAAA;IACH,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAE,CAAA;IAC5B,MAAM,KAAK,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAA;IACxC,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;;QAC7E,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,KAAkB,CAAC,CAAA;IAC/D,MAAM,SAAS,GAAkB,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAA;IACpD,6EAA6E;IAC7E,wEAAwE;IACxE,wEAAwE;IACxE,MAAM,KAAK,GAAG,cAAc,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACrG,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAA;AACnE,CAAC;AAED,mFAAmF;AACnF,SAAS,cAAc,CACrB,KAA+B,EAC/B,GAAgB,EAChB,MAAyB;IAEzB,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,CAAA;IAC5B,MAAM,KAAK,GAAG,CACZ,IAA4C,EAC5C,OAAe,EACF,EAAE,CAAC,CAAC;QACjB,KAAK,EAAE,QAAQ,CAAC;YACd,IAAI;YACJ,OAAO;YACP,IAAI,EAAE,CAAC,WAAW,EAAE,GAAG,UAAU,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC;YACzE,YAAY,EAAE,UAAU,CAAC,EAAE;YAC3B,UAAU,EAAE,GAAG,CAAC,EAAE;YAClB,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;SACjG,CAAC;KACH,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,KAAK,CAAC,CAAA;IACpE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,KAAK,CACV,qBAAqB,EACrB,kBAAkB,MAAM,CAAC,KAAK,oBAAoB,UAAU,CAAC,EAAE,uCAAuC,CACvG,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC,qBAAqB,EAAE,UAAU,MAAM,CAAC,KAAK,wCAAwC,CAAC,CAAA;IACrG,CAAC;IAED,MAAM,SAAS,GAAG,gBAAgB,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IAC5D,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9C,OAAO,KAAK,CAAC,gBAAgB,EAAE,UAAU,MAAM,CAAC,KAAK,qCAAqC,CAAC,CAAA;IAC7F,CAAC;IACD,IACE,MAAM,CAAC,MAAM,KAAK,SAAS;QAC3B,KAAK,CAAC,eAAe,EAAE,YAAY;QACnC,CAAC,KAAK,CAAC,eAAe,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EACnE,CAAC;QACD,OAAO,KAAK,CACV,gBAAgB,EAChB,mBAAmB,MAAM,CAAC,MAAM,yBAAyB,KAAK,CAAC,eAAe,EAAE,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACpH,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IACpE,IAAI,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC,yBAAyB,EAAE,UAAU,MAAM,CAAC,KAAK,oCAAoC,CAAC,CAAA;IACrG,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9D,OAAO,oBAAoB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;IAChE,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACzD,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IACnF,IAAI,OAAO,IAAI,MAAM;QAAE,OAAO,MAAM,CAAA;IACpC,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;AAC1E,CAAC;AAGD;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAA+B;IACjE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IAEhD,IAAI,IAAI,GAAgB,eAAe,CAAC,QAAQ,CAAC,CAAA;IACjD,MAAM,eAAe,GAAyB,EAAE,CAAA;IAChD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAA;QACnD,IAAI,OAAO,IAAI,OAAO;YAAE,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;;YACtD,IAAI,GAAG,OAAO,CAAC,GAAG,CAAA;IACzB,CAAC;IACD,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;IAE7E,wDAAwD;IACxD,MAAM,eAAe,GAA4B,KAAK,CAAC,eAAe,IAAI,EAAE,CAAA;IAC5E,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAA;IACpE,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,EAAE,eAAe,CAAC,CAAA;IAChE,MAAM,UAAU,GAAG,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;IAEnE,MAAM,IAAI,GAAG,aAAa,CAAC,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IACtD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IAErE,OAAO;QACL,EAAE,EAAE,IAAI;QACR,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE;YACL,UAAU,EAAE,UAAU,CAAC,EAAE;YACzB,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC;gBAC1D,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;gBAC/B,CAAC,CAAC,EAAE,CAAC;SACR;QACD,MAAM,EAAE;YACN,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjE;QACD,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CACzC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAClF;QACD,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC;KAC3B,CAAA;AACH,CAAC","sourcesContent":["/**\n * `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow\n * counterpart of `applyContentWithShape`, minus the generation step).\n *\n * The MODEL authors the field changes directly in the `updateFields` tool input\n * (it has the conversation + `getDocument` output as context); this module turns\n * those changes into a reviewable proposal with the SAME engine discipline the\n * create flow has:\n *\n * - **merge semantics**: changes apply per field over the EXISTING document —\n * never a whole-document rewrite; the patch is the merged document, the diff\n * (`diffDocuments`) is what the editor reviews (changed fields only).\n * - **targeting is addressed with the engine's existing conventions** — field\n * name + optional locale bucket (the diff / `RestoreScope` shape) and, inside\n * a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,\n * root blocks by stable node id). No new path grammar.\n * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND\n * asset ids (EC-381) must come from searched candidates (`searchDocuments` /\n * `listAssets`) or already be on the document; every targeting failure is an\n * explicit issue — never a silent drop (unlike the create shape, which makes bad\n * ids structurally unrepresentable, an update change is imperative — so it FAILS\n * instead of being filtered).\n * - **validation parity**: the merged document runs through `validateDocument`\n * with the studio's injected leashes. Only errors the change INTRODUCES block\n * the proposal — pre-existing brokenness on the document is not the\n * assistant's doing and must not force a whole-document repair (the editor's\n * own save path tolerates it the same way; errors gate publish, not drafts).\n *\n * Locale-awareness: a localized field's change lands in the change's `locale`,\n * defaulting to the editor's working locale (`input.locale`). Non-localized\n * fields reject a `locale` loudly.\n *\n * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires\n * it to `getDocument` + the candidate store and hands the result to the review\n * sink.\n */\nimport {\n cmsIssue,\n isFieldLocalized,\n readAssetId,\n readDocumentRef,\n referenceNode,\n validateDocument,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n FieldDef,\n FieldValues,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport { blockRootsOf, editBlockRoots, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { carryAssetPlacements } from '../assets/asset-placements'\nimport { introducedErrors, warningsOf } from './proposal-validation'\nimport { richTextValueFromModelInput } from './rich-text-input'\nimport type { AssetCandidates, ContentPatch, RelationCandidates } from './types'\n\n/**\n * One field change of an `updateFields` proposal, addressed with the engine's\n * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a\n * localized field), and for a `blocks` field optionally `blockId` + `prop` to\n * change one component prop of one root block. `value` is the new value\n * (`null`/omitted clears the field or prop).\n */\nexport interface UpdateFieldChange {\n field: string\n /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */\n locale?: string\n /** For a `blocks` field: the root block's stable node id (from `getDocument`). */\n blockId?: string\n /** With `blockId`: the component prop to change. */\n prop?: string\n /** The new value; `null` / omitted clears the field or prop. */\n value?: unknown\n}\n\nexport interface BuildUpdateProposalInput {\n registry: CollectionRegistry\n collection: CollectionDef\n /** The CURRENT document — the proposal's \"before\" side. Never mutated. */\n document: CmsDocument\n changes: readonly UpdateFieldChange[]\n /** The editor's working locale — the default bucket for localized fields. */\n locale?: string\n /** Relation candidates (searched ids) keyed by field name — the id leash. */\n candidates?: RelationCandidates\n /** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */\n assetCandidates?: AssetCandidates\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, whole. It also carries the project's\n * `localeConfig`, which is what the unknown-locale targeting check reads: one\n * source for \"which locales exist\", not two that can disagree.\n */\n validateOptions?: ValidateDocumentOptions\n}\n\n/** The `before` snapshot the review diff renders against. */\nexport interface UpdateBeforeSnapshot {\n values: FieldValues\n localized?: Record<string, FieldValues>\n}\n\nexport type BuildUpdateProposalResult =\n | {\n ok: true\n kind: 'proposal'\n /** The MERGED document as a patch (full values; the diff shows changed fields only). */\n patch: ContentPatch\n before: UpdateBeforeSnapshot\n /** Human-readable descriptors of the changed fields (e.g. `title (de)`). */\n changedFields: string[]\n /** Non-error issues on the accepted result (warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n /** Every change resolved to the current values — nothing to propose. */\n ok: true\n kind: 'no-change'\n }\n | { ok: false; issues: CmsValidationIssue[] }\n\ntype ChangeIssue = { issue: CmsValidationIssue }\ntype ChangeOutcome = { doc: CmsDocument } | ChangeIssue\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0\n}\n\n/**\n * The present/absent-key field write — the same semantics as the editor's\n * single-sourced `setFieldValue` (`lib/locale/fallback`): a resolved `locale`\n * writes the `localized[locale]` bucket, clearing DELETES the key. Local here\n * (not imported) because this module takes the locale as an already-resolved\n * argument per change; the semantics are pinned by tests on both sides.\n */\nfunction writeFieldValue(\n doc: CmsDocument,\n fieldName: string,\n locale: string | undefined,\n value: unknown,\n): CmsDocument {\n if (locale !== undefined) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n/**\n * The read twin of {@link writeFieldValue}: the field's value in the resolved\n * `locale` bucket, or in the shared `values` for a non-localized field. The exact\n * bucket only, with no default-locale fallback, because that bucket is the one the\n * write replaces.\n */\nfunction readFieldValue(doc: CmsDocument, fieldName: string, locale: string | undefined): unknown {\n return locale !== undefined ? doc.localized?.[locale]?.[fieldName] : doc.values[fieldName]\n}\n\n/** Relation ids CURRENTLY on the document for a field (any bucket) — re-usable without a search. */\nfunction currentRelationIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const ref = readDocumentRef(entry)\n if (ref) ids.add(ref.id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */\nfunction currentAssetIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const id = readAssetId(entry)\n if (id) ids.add(id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/**\n * Map a change's raw value into the stored value shape (envelopes; loud leash).\n * `document` is the ORIGINAL document, which is what the id leash reads. `current`\n * is the value this change replaces, taken from the resolved locale bucket of the\n * working copy. The asset sub-field carry reads it (EC-558).\n */\nfunction mapUpdateValue(\n field: FieldDef,\n change: UpdateFieldChange,\n document: CmsDocument,\n current: unknown,\n input: BuildUpdateProposalInput,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): { value: unknown } | ChangeIssue {\n const raw = change.value\n if (raw === undefined || raw === null) return { value: undefined } // clear\n switch (field.type) {\n case 'relation': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Relation \"${field.name}\" takes document id string(s) from searchDocuments results.`,\n )\n }\n // The id leash (same stance as the create enum, but LOUD): only searched\n // candidate ids or ids already on the document are usable.\n const allowed = new Set([\n ...(input.candidates?.[field.name] ?? []).map((c) => c.id),\n ...currentRelationIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-relation-target',\n `Relation \"${field.name}\" may only use ids from searchDocuments results — search \"${field.target}\" first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single reference — pass one id.`)\n }\n const refs = ids.map((id) => referenceNode({ collection: field.target, id }))\n return { value: field.multiple ? refs : refs[0] }\n }\n case 'asset': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Asset field \"${field.name}\" takes asset id string(s) from listAssets results.`,\n )\n }\n // EC-381 — the asset id leash (same stance as relations, but LOUD): only\n // searched library candidate ids or ids already on the document are usable;\n // the assistant never invents or uploads an asset.\n const allowed = new Set([\n ...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),\n ...currentAssetIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-asset',\n `Asset field \"${field.name}\" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single asset — pass one id.`)\n }\n // EC-558: replace-with-list through the one carry rule. A held id keeps its\n // placement's per-use sub-fields (a caption, say), a single swap carries\n // them to the new image, and a NEW id is an envelope (EC-381). Data about\n // the image itself (alt, an AI flag) belongs on the asset's `meta`.\n return {\n value: carryAssetPlacements(\n current,\n field.multiple ? { multiple: true, ids } : { multiple: false, id: ids[0] ?? null },\n 'envelope',\n ),\n }\n }\n case 'richText': {\n // EC-379: a string value is MARKDOWN, converted through the engine's strict\n // subset converter; out-of-subset constructs fail LOUD (the model retries\n // in-conversation). A `{ version, doc }` envelope object stays accepted for\n // robustness (getDocument may have shown one) — see rich-text-input.ts.\n const converted = richTextValueFromModelInput(raw)\n if (!converted.ok) {\n return issue('invalid-field-value', `Field \"${field.name}\": ${converted.problems.join(' ')}`)\n }\n return { value: converted.value }\n }\n default:\n // text / number / boolean / date / select / object / blocks: as-is\n // (the merged document's validateDocument run is the shape leash).\n return { value: raw }\n }\n}\n\n/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */\nfunction applyBlockPropChange(\n doc: CmsDocument,\n field: FieldDef,\n change: UpdateFieldChange,\n locale: string | undefined,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): ChangeOutcome {\n if (field.type !== 'blocks') {\n return issue(\n 'invalid-field-value',\n `\"${change.field}\" is not a blocks field — omit \"blockId\"/\"prop\" (set the field value directly).`,\n )\n }\n if (!isNonEmptyString(change.blockId)) {\n return issue(\n 'invalid-field-value',\n `Changing a block on \"${change.field}\" needs \"blockId\" — block ids come from getDocument.`,\n )\n }\n if (!isNonEmptyString(change.prop)) {\n return issue(\n 'invalid-field-value',\n `Changing block \"${change.blockId}\" needs \"prop\" — the component property to change.`,\n )\n }\n const original = readFieldValue(doc, change.field, locale)\n const blocks = blockRootsOf(original)\n const index = blocks.findIndex((block) => block.id === change.blockId)\n if (index < 0) {\n return issue(\n 'invalid-field-value',\n `Block \"${change.blockId}\" was not found on \"${change.field}\" — read the document first; block ids come from getDocument.`,\n )\n }\n const block = blocks[index]!\n const props = { ...(block.props ?? {}) }\n if (change.value === undefined || change.value === null) delete props[change.prop]\n else props[change.prop] = staticProp(change.value as JsonValue)\n const nextBlock: ComponentNode = { ...block, props }\n // EC-576: through the core write path — the other roots and every typography\n // item keep their place, and the write is the v2 sequence. (`blocks` is\n // `editBlockRoots`' own root list, so `index` addresses the same root.)\n const value = editBlockRoots(original, (roots) => roots.map((b, i) => (i === index ? nextBlock : b)))\n return { doc: writeFieldValue(doc, change.field, locale, value) }\n}\n\n/** Apply ONE change to a (copy of the) document, or report the targeting issue. */\nfunction applyOneChange(\n input: BuildUpdateProposalInput,\n doc: CmsDocument,\n change: UpdateFieldChange,\n): ChangeOutcome {\n const { collection } = input\n const issue = (\n code: Parameters<typeof cmsIssue>[0]['code'],\n message: string,\n ): ChangeIssue => ({\n issue: cmsIssue({\n code,\n message,\n path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],\n collectionId: collection.id,\n documentId: doc.id,\n meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },\n }),\n })\n\n const field = collection.fields.find((f) => f.name === change.field)\n if (!field) {\n return issue(\n 'invalid-field-value',\n `Unknown field \"${change.field}\" on collection \"${collection.id}\" — use a field name from the schema.`,\n )\n }\n if (field.ai?.exclude) {\n return issue('invalid-field-value', `Field \"${change.field}\" must not be edited by the assistant.`)\n }\n\n const localized = isFieldLocalized(collection, change.field)\n if (!localized && change.locale !== undefined) {\n return issue('unknown-locale', `Field \"${change.field}\" is not localized — omit \"locale\".`)\n }\n if (\n change.locale !== undefined &&\n input.validateOptions?.localeConfig &&\n !input.validateOptions.localeConfig.locales.includes(change.locale)\n ) {\n return issue(\n 'unknown-locale',\n `Unknown locale \"${change.locale}\" — this project has: ${input.validateOptions?.localeConfig?.locales.join(', ')}.`,\n )\n }\n const locale = localized ? change.locale ?? input.locale : undefined\n if (localized && locale === undefined) {\n return issue('missing-localized-value', `Field \"${change.field}\" is localized — provide \"locale\".`)\n }\n\n if (change.blockId !== undefined || change.prop !== undefined) {\n return applyBlockPropChange(doc, field, change, locale, issue)\n }\n\n const current = readFieldValue(doc, change.field, locale)\n const mapped = mapUpdateValue(field, change, input.document, current, input, issue)\n if ('issue' in mapped) return mapped\n return { doc: writeFieldValue(doc, change.field, locale, mapped.value) }\n}\n\n\n/**\n * Build the reviewable update proposal: apply every change over a copy of the\n * document (loud targeting), validate the merged result with the injected\n * leashes (only errors the change INTRODUCES block), and report the semantic\n * diff. `no-change` when the changes resolve to the current values.\n */\nexport function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult {\n const { registry, collection, document } = input\n\n let next: CmsDocument = structuredClone(document)\n const targetingIssues: CmsValidationIssue[] = []\n for (const change of input.changes) {\n const outcome = applyOneChange(input, next, change)\n if ('issue' in outcome) targetingIssues.push(outcome.issue)\n else next = outcome.doc\n }\n if (targetingIssues.length > 0) return { ok: false, issues: targetingIssues }\n\n // EC-497: THE bag, whole — no per-call-site reassembly.\n const validateOptions: ValidateDocumentOptions = input.validateOptions ?? {}\n const before = validateDocument(registry, document, validateOptions)\n const issues = validateDocument(registry, next, validateOptions)\n const introduced = introducedErrors(before, issues)\n if (introduced.length > 0) return { ok: false, issues: introduced }\n\n const diff = diffDocuments(collection, document, next)\n if (diff.changes.length === 0) return { ok: true, kind: 'no-change' }\n\n return {\n ok: true,\n kind: 'proposal',\n patch: {\n collection: collection.id,\n id: document.id,\n values: next.values,\n ...(next.localized && Object.keys(next.localized).length > 0\n ? { localized: next.localized }\n : {}),\n },\n before: {\n values: document.values,\n ...(document.localized ? { localized: document.localized } : {}),\n },\n changedFields: diff.changes.map((change) =>\n change.locale !== undefined ? `${change.field} (${change.locale})` : change.field,\n ),\n issues: warningsOf(issues),\n }\n}\n"]}
1
+ {"version":3,"file":"update-fields.js","sourceRoot":"","sources":["../../../src/lib/ai/update-fields.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow\n * counterpart of `applyContentWithShape`, minus the generation step).\n *\n * The MODEL authors the field changes directly in the `updateFields` tool input\n * (it has the conversation + `getDocument` output as context); this module turns\n * those changes into a reviewable proposal with the SAME engine discipline the\n * create flow has:\n *\n * - **merge semantics**: changes apply per field over the EXISTING document —\n * never a whole-document rewrite; the patch is the merged document, the diff\n * (`diffDocuments`) is what the editor reviews (changed fields only).\n * - **targeting is addressed with the engine's existing conventions** — field\n * name + optional locale bucket (the diff / `RestoreScope` shape) and, inside\n * a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,\n * root blocks by stable node id). No new path grammar.\n * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND\n * asset ids (EC-381) must come from searched candidates (`searchDocuments` /\n * `listAssets`) or already be on the document; every targeting failure is an\n * explicit issue — never a silent drop (unlike the create shape, which makes bad\n * ids structurally unrepresentable, an update change is imperative — so it FAILS\n * instead of being filtered).\n * - **validation parity**: the merged document runs through `validateDocument`\n * with the studio's injected leashes. Only errors the change INTRODUCES block\n * the proposal — pre-existing brokenness on the document is not the\n * assistant's doing and must not force a whole-document repair (the editor's\n * own save path tolerates it the same way; errors gate publish, not drafts).\n *\n * Locale-awareness: a localized field's change lands in the change's `locale`,\n * defaulting to the editor's working locale (`input.locale`). Non-localized\n * fields reject a `locale` loudly.\n *\n * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires\n * it to `getDocument` + the candidate store and hands the result to the review\n * sink.\n */\nimport {\n cmsIssue,\n isFieldLocalized,\n readAssetId,\n readDocumentRef,\n referenceNode,\n validateDocument,\n writeField,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n FieldDef,\n FieldValues,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport { blockRootsOf, editBlockRoots, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { carryAssetPlacements } from '../assets/asset-placements'\nimport { introducedErrors, warningsOf } from './proposal-validation'\nimport { richTextValueFromModelInput } from './rich-text-input'\nimport type { AssetCandidates, ContentPatch, RelationCandidates } from './types'\n\n/**\n * One field change of an `updateFields` proposal, addressed with the engine's\n * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a\n * localized field), and for a `blocks` field optionally `blockId` + `prop` to\n * change one component prop of one root block. `value` is the new value\n * (`null`/omitted clears the field or prop).\n */\nexport interface UpdateFieldChange {\n field: string\n /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */\n locale?: string\n /** For a `blocks` field: the root block's stable node id (from `getDocument`). */\n blockId?: string\n /** With `blockId`: the component prop to change. */\n prop?: string\n /** The new value; `null` / omitted clears the field or prop. */\n value?: unknown\n}\n\nexport interface BuildUpdateProposalInput {\n registry: CollectionRegistry\n collection: CollectionDef\n /** The CURRENT document — the proposal's \"before\" side. Never mutated. */\n document: CmsDocument\n changes: readonly UpdateFieldChange[]\n /** The editor's working locale — the default bucket for localized fields. */\n locale?: string\n /** Relation candidates (searched ids) keyed by field name — the id leash. */\n candidates?: RelationCandidates\n /** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */\n assetCandidates?: AssetCandidates\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, whole. It also carries the project's\n * `localeConfig`, which is what the unknown-locale targeting check reads: one\n * source for \"which locales exist\", not two that can disagree.\n */\n validateOptions?: ValidateDocumentOptions\n}\n\n/** The `before` snapshot the review diff renders against. */\nexport interface UpdateBeforeSnapshot {\n values: FieldValues\n localized?: Record<string, FieldValues>\n}\n\nexport type BuildUpdateProposalResult =\n | {\n ok: true\n kind: 'proposal'\n /** The MERGED document as a patch (full values; the diff shows changed fields only). */\n patch: ContentPatch\n before: UpdateBeforeSnapshot\n /** Human-readable descriptors of the changed fields (e.g. `title (de)`). */\n changedFields: string[]\n /** Non-error issues on the accepted result (warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n /** Every change resolved to the current values — nothing to propose. */\n ok: true\n kind: 'no-change'\n }\n | { ok: false; issues: CmsValidationIssue[] }\n\ntype ChangeIssue = { issue: CmsValidationIssue }\ntype ChangeOutcome = { doc: CmsDocument } | ChangeIssue\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0\n}\n\n// The present/absent-key field write is core's `writeField` (EC-580) — this module\n// used to carry a local copy of the editor's `setFieldValue` (\"Local here (not\n// imported)\", MODE F5b.bkt). The locale is resolved per change below: a localized\n// field always arrives with one (a missing one is an explicit issue first).\n\n/**\n * The read twin of core's `writeField`: the field's value in the resolved\n * `locale` bucket, or in the shared `values` for a non-localized field. The exact\n * bucket only, with no default-locale fallback, because that bucket is the one the\n * write replaces.\n */\nfunction readFieldValue(doc: CmsDocument, fieldName: string, locale: string | undefined): unknown {\n return locale !== undefined ? doc.localized?.[locale]?.[fieldName] : doc.values[fieldName]\n}\n\n/** Relation ids CURRENTLY on the document for a field (any bucket) — re-usable without a search. */\nfunction currentRelationIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const ref = readDocumentRef(entry)\n if (ref) ids.add(ref.id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */\nfunction currentAssetIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const id = readAssetId(entry)\n if (id) ids.add(id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/**\n * Map a change's raw value into the stored value shape (envelopes; loud leash).\n * `document` is the ORIGINAL document, which is what the id leash reads. `current`\n * is the value this change replaces, taken from the resolved locale bucket of the\n * working copy. The asset sub-field carry reads it (EC-558).\n */\nfunction mapUpdateValue(\n field: FieldDef,\n change: UpdateFieldChange,\n document: CmsDocument,\n current: unknown,\n input: BuildUpdateProposalInput,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): { value: unknown } | ChangeIssue {\n const raw = change.value\n if (raw === undefined || raw === null) return { value: undefined } // clear\n switch (field.type) {\n case 'relation': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Relation \"${field.name}\" takes document id string(s) from searchDocuments results.`,\n )\n }\n // The id leash (same stance as the create enum, but LOUD): only searched\n // candidate ids or ids already on the document are usable.\n const allowed = new Set([\n ...(input.candidates?.[field.name] ?? []).map((c) => c.id),\n ...currentRelationIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-relation-target',\n `Relation \"${field.name}\" may only use ids from searchDocuments results — search \"${field.target}\" first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single reference — pass one id.`)\n }\n const refs = ids.map((id) => referenceNode({ collection: field.target, id }))\n return { value: field.multiple ? refs : refs[0] }\n }\n case 'asset': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Asset field \"${field.name}\" takes asset id string(s) from listAssets results.`,\n )\n }\n // EC-381 — the asset id leash (same stance as relations, but LOUD): only\n // searched library candidate ids or ids already on the document are usable;\n // the assistant never invents or uploads an asset.\n const allowed = new Set([\n ...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),\n ...currentAssetIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-asset',\n `Asset field \"${field.name}\" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single asset — pass one id.`)\n }\n // EC-558: replace-with-list through the one carry rule. A held id keeps its\n // placement's per-use sub-fields (a caption, say), a single swap carries\n // them to the new image, and a NEW id is an envelope (EC-381). Data about\n // the image itself (alt, an AI flag) belongs on the asset's `meta`.\n return {\n value: carryAssetPlacements(\n current,\n field.multiple ? { multiple: true, ids } : { multiple: false, id: ids[0] ?? null },\n ),\n }\n }\n case 'richText': {\n // EC-379: a string value is MARKDOWN, converted through the engine's strict\n // subset converter; out-of-subset constructs fail LOUD (the model retries\n // in-conversation). A `{ version, doc }` envelope object stays accepted for\n // robustness (getDocument may have shown one) — see rich-text-input.ts.\n const converted = richTextValueFromModelInput(raw)\n if (!converted.ok) {\n return issue('invalid-field-value', `Field \"${field.name}\": ${converted.problems.join(' ')}`)\n }\n return { value: converted.value }\n }\n default:\n // text / number / boolean / date / select / object / blocks: as sent. The\n // write below (`writeField`) canonicalises it (EC-586: a date into one UTC\n // instant, a multi-select scalar into a list, an empty list absent), and the\n // merged document's validateDocument run is the shape leash.\n return { value: raw }\n }\n}\n\n/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */\nfunction applyBlockPropChange(\n collection: CollectionDef,\n doc: CmsDocument,\n field: FieldDef,\n change: UpdateFieldChange,\n locale: string | undefined,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): ChangeOutcome {\n if (field.type !== 'blocks') {\n return issue(\n 'invalid-field-value',\n `\"${change.field}\" is not a blocks field — omit \"blockId\"/\"prop\" (set the field value directly).`,\n )\n }\n if (!isNonEmptyString(change.blockId)) {\n return issue(\n 'invalid-field-value',\n `Changing a block on \"${change.field}\" needs \"blockId\" — block ids come from getDocument.`,\n )\n }\n if (!isNonEmptyString(change.prop)) {\n return issue(\n 'invalid-field-value',\n `Changing block \"${change.blockId}\" needs \"prop\" — the component property to change.`,\n )\n }\n const original = readFieldValue(doc, change.field, locale)\n const blocks = blockRootsOf(original)\n const index = blocks.findIndex((block) => block.id === change.blockId)\n if (index < 0) {\n return issue(\n 'invalid-field-value',\n `Block \"${change.blockId}\" was not found on \"${change.field}\" — read the document first; block ids come from getDocument.`,\n )\n }\n const block = blocks[index]!\n const props = { ...(block.props ?? {}) }\n if (change.value === undefined || change.value === null) delete props[change.prop]\n else props[change.prop] = staticProp(change.value as JsonValue)\n const nextBlock: ComponentNode = { ...block, props }\n // EC-576: through the core write path — the other roots and every typography\n // item keep their place, and the write is the v2 sequence. (`blocks` is\n // `editBlockRoots`' own root list, so `index` addresses the same root.)\n const value = editBlockRoots(original, (roots) => roots.map((b, i) => (i === index ? nextBlock : b)))\n return { doc: writeField(collection, doc, change.field, locale, value) }\n}\n\n/** Apply ONE change to a (copy of the) document, or report the targeting issue. */\nfunction applyOneChange(\n input: BuildUpdateProposalInput,\n doc: CmsDocument,\n change: UpdateFieldChange,\n): ChangeOutcome {\n const { collection } = input\n const issue = (\n code: Parameters<typeof cmsIssue>[0]['code'],\n message: string,\n ): ChangeIssue => ({\n issue: cmsIssue({\n code,\n message,\n path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],\n collectionId: collection.id,\n documentId: doc.id,\n meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },\n }),\n })\n\n const field = collection.fields.find((f) => f.name === change.field)\n if (!field) {\n return issue(\n 'invalid-field-value',\n `Unknown field \"${change.field}\" on collection \"${collection.id}\" — use a field name from the schema.`,\n )\n }\n if (field.ai?.exclude) {\n return issue('invalid-field-value', `Field \"${change.field}\" must not be edited by the assistant.`)\n }\n\n const localized = isFieldLocalized(collection, change.field)\n if (!localized && change.locale !== undefined) {\n return issue('unknown-locale', `Field \"${change.field}\" is not localized — omit \"locale\".`)\n }\n if (\n change.locale !== undefined &&\n input.validateOptions?.localeConfig &&\n !input.validateOptions.localeConfig.locales.includes(change.locale)\n ) {\n return issue(\n 'unknown-locale',\n `Unknown locale \"${change.locale}\" — this project has: ${input.validateOptions?.localeConfig?.locales.join(', ')}.`,\n )\n }\n const locale = localized ? change.locale ?? input.locale : undefined\n if (localized && locale === undefined) {\n return issue('missing-localized-value', `Field \"${change.field}\" is localized — provide \"locale\".`)\n }\n\n if (change.blockId !== undefined || change.prop !== undefined) {\n return applyBlockPropChange(collection, doc, field, change, locale, issue)\n }\n\n const current = readFieldValue(doc, change.field, locale)\n const mapped = mapUpdateValue(field, change, input.document, current, input, issue)\n if ('issue' in mapped) return mapped\n return { doc: writeField(collection, doc, change.field, locale, mapped.value) }\n}\n\n\n/**\n * Build the reviewable update proposal: apply every change over a copy of the\n * document (loud targeting), validate the merged result with the injected\n * leashes (only errors the change INTRODUCES block), and report the semantic\n * diff. `no-change` when the changes resolve to the current values.\n */\nexport function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult {\n const { registry, collection, document } = input\n\n let next: CmsDocument = structuredClone(document)\n const targetingIssues: CmsValidationIssue[] = []\n for (const change of input.changes) {\n const outcome = applyOneChange(input, next, change)\n if ('issue' in outcome) targetingIssues.push(outcome.issue)\n else next = outcome.doc\n }\n if (targetingIssues.length > 0) return { ok: false, issues: targetingIssues }\n\n // EC-497: THE bag, whole — no per-call-site reassembly.\n const validateOptions: ValidateDocumentOptions = input.validateOptions ?? {}\n const before = validateDocument(registry, document, validateOptions)\n const issues = validateDocument(registry, next, validateOptions)\n const introduced = introducedErrors(before, issues)\n if (introduced.length > 0) return { ok: false, issues: introduced }\n\n const diff = diffDocuments(collection, document, next)\n if (diff.changes.length === 0) return { ok: true, kind: 'no-change' }\n\n return {\n ok: true,\n kind: 'proposal',\n patch: {\n collection: collection.id,\n id: document.id,\n values: next.values,\n ...(next.localized && Object.keys(next.localized).length > 0\n ? { localized: next.localized }\n : {}),\n },\n before: {\n values: document.values,\n ...(document.localized ? { localized: document.localized } : {}),\n },\n changedFields: diff.changes.map((change) =>\n change.locale !== undefined ? `${change.field} (${change.locale})` : change.field,\n ),\n issues: warningsOf(issues),\n }\n}\n"]}
@@ -22,11 +22,31 @@ export interface AiDevPluginOptions {
22
22
  */
23
23
  insecureAllowAnonymousSessions?: boolean | 'playground-mode';
24
24
  }
25
- /** Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned). */
25
+ /**
26
+ * Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned).
27
+ * `'playground-mode'` asks the SAME question the studio asks of the same env — core's
28
+ * `parseBackendMode` (EC-603) — so a padded flag the studio boots as playground opens
29
+ * the bypass too, and a misspelled one (the studio's boot error) keeps it shut.
30
+ */
26
31
  export declare function resolveInsecureAnonymousOptIn(optIn: boolean | 'playground-mode' | undefined, env: Record<string, string | undefined>): boolean;
32
+ /** The plugin's `name` — how `elytra doctor` finds the AI route in a studio's Vite config. */
33
+ export declare const AI_ROUTE_PLUGIN_NAME = "elytra-ai-route";
34
+ /**
35
+ * What the plugin mounts, exposed on its `api` (Vite's inter-plugin channel) so a tool
36
+ * that loads the studio's Vite config can read the AI route from that config instead of
37
+ * guessing a file path (EC-603: `elytra doctor` reads it to decide whether an AI
38
+ * provider key is expected, and checks the handler module exists).
39
+ */
40
+ export interface AiDevPluginApi {
41
+ elytraAiRoute: {
42
+ route: string;
43
+ handlerModulePath: string;
44
+ exportName: string;
45
+ };
46
+ }
27
47
  /**
28
48
  * Mount the AI route in the Vite dev server. Dev-only by construction
29
49
  * (`apply: 'serve'`); a production deploy mounts the same handler module via
30
50
  * `toNodeHandler` in its serverless entry instead.
31
51
  */
32
- export declare function createAiDevPlugin(options?: AiDevPluginOptions): Plugin;
52
+ export declare function createAiDevPlugin(options?: AiDevPluginOptions): Plugin<AiDevPluginApi>;
@@ -19,6 +19,7 @@
19
19
  * `@elytracms/studio/lib/ai` directly.
20
20
  */
21
21
  import { loadEnv } from 'vite';
22
+ import { parseBackendMode } from '@elytracms/core/export-sync/deployment-ready';
22
23
  import { INSECURE_ANONYMOUS_SESSION_ENV_VALUE, INSECURE_ANONYMOUS_SESSION_ENV_VAR, } from './convex-session.js';
23
24
  import { toNodeHandler } from './node-handler.js';
24
25
  import { DEFAULT_AI_ENDPOINT } from './route-provider.js';
@@ -28,14 +29,21 @@ export const DEFAULT_AI_DEV_ENV_KEYS = [
28
29
  'VITE_CONVEX_SITE_URL',
29
30
  'VITE_CONVEX_URL',
30
31
  ];
31
- /** Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned). */
32
+ /**
33
+ * Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned).
34
+ * `'playground-mode'` asks the SAME question the studio asks of the same env — core's
35
+ * `parseBackendMode` (EC-603) — so a padded flag the studio boots as playground opens
36
+ * the bypass too, and a misspelled one (the studio's boot error) keeps it shut.
37
+ */
32
38
  export function resolveInsecureAnonymousOptIn(optIn, env) {
33
39
  if (optIn === true)
34
40
  return true;
35
41
  if (optIn === 'playground-mode')
36
- return env.VITE_ELYTRA_MODE === 'playground';
42
+ return parseBackendMode(env).kind === 'playground';
37
43
  return false;
38
44
  }
45
+ /** The plugin's `name` — how `elytra doctor` finds the AI route in a studio's Vite config. */
46
+ export const AI_ROUTE_PLUGIN_NAME = 'elytra-ai-route';
39
47
  function jsonError(code, status) {
40
48
  return new Response(JSON.stringify({ error: code }), {
41
49
  status,
@@ -53,8 +61,9 @@ export function createAiDevPlugin(options = {}) {
53
61
  const exportName = options.exportName ?? 'aiRouteHandler';
54
62
  let env = {};
55
63
  return {
56
- name: 'elytra-ai-route',
64
+ name: AI_ROUTE_PLUGIN_NAME,
57
65
  apply: 'serve',
66
+ api: { elytraAiRoute: { route, handlerModulePath, exportName } },
58
67
  configResolved(config) {
59
68
  env = loadEnv(config.mode, config.root, '');
60
69
  for (const key of [...DEFAULT_AI_DEV_ENV_KEYS, ...(options.envKeys ?? [])]) {
@@ -1 +1 @@
1
- {"version":3,"file":"vite.js","sourceRoot":"","sources":["../../../src/lib/ai/vite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAE9B,OAAO,EACL,oCAAoC,EACpC,kCAAkC,GACnC,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAGtD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,mBAAmB;IACnB,sBAAsB;IACtB,iBAAiB;CACT,CAAA;AAwBV,sFAAsF;AACtF,MAAM,UAAU,6BAA6B,CAC3C,KAA8C,EAC9C,GAAuC;IAEvC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC/B,IAAI,KAAK,KAAK,iBAAiB;QAAE,OAAO,GAAG,CAAC,gBAAgB,KAAK,YAAY,CAAA;IAC7E,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,SAAS,CAAC,IAAY,EAAE,MAAc;IAC7C,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE;QACnD,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IAChE,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,2BAA2B,CAAA;IAClF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,mBAAmB,CAAA;IAClD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,gBAAgB,CAAA;IACzD,IAAI,GAAG,GAAuC,EAAE,CAAA;IAEhD,OAAO;QACL,IAAI,EAAE,iBAAiB;QACvB,KAAK,EAAE,OAAO;QACd,cAAc,CAAC,MAAM;YACnB,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;YAC3C,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,uBAAuB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC3E,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;YAChE,CAAC;QACH,CAAC;QACD,eAAe,CAAC,MAAqB;YACnC,IAAI,6BAA6B,CAAC,OAAO,CAAC,8BAA8B,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC/E,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,GAAG,oCAAoC,CAAA;gBACtF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CACvB,yEAAyE,KAAK,GAAG;oBAC/E,4FAA4F,CAC/F,CAAA;YACH,CAAC;YAED,+EAA+E;YAC/E,+EAA+E;YAC/E,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,OAAgB,EAAqB,EAAE;gBACzE,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAA4B,CAAA;oBACtF,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,CAAA;oBAC/B,IAAI,OAAO,OAAO,KAAK,UAAU;wBAAE,OAAO,SAAS,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAA;oBACjF,OAAO,MAAO,OAA0B,CAAC,OAAO,CAAC,CAAA;gBACnD,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,SAAS,CAAC,aAAa,EAAE,GAAG,CAAC,CAAA;gBACtC,CAAC;YACH,CAAC,CAAC,CAAA;YAEF,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;gBACxC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC;oBAAE,OAAO,IAAI,EAAE,CAAA;gBACzD,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;oBAC/B,IAAI,CAAC,GAAG,CAAC,WAAW;wBAAE,GAAG,CAAC,UAAU,GAAG,GAAG,CAAA;oBAC1C,GAAG,CAAC,GAAG,EAAE,CAAA;gBACX,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * `@elytracms/studio/lib/ai/vite` — `createAiDevPlugin`, the DEV half of the AI\n * route mounting kit (EC-371; closes EC-369 finding #2). A studio app is a client\n * SPA with no server build, so in dev the host route lives as a Vite dev-server\n * middleware: this plugin loads the framework-agnostic `Request → Response` handler\n * module through Vite's SSR pipeline (so its `@elytracms/*` imports resolve like the\n * rest of the app) and bridges Node req/res via the shared {@link toNodeHandler}.\n *\n * It also owns the env bridge the resolver needs (the piece the template had\n * drifted on): `loadEnv` surfaces `ANTHROPIC_API_KEY` / `VITE_CONVEX_SITE_URL` /\n * `VITE_CONVEX_URL` from `.env`/`.env.local` into `process.env` for the SSR-loaded\n * handler — Vite only exposes `VITE_`-prefixed vars via `import.meta.env`, and a\n * `VITE_` prefix on the provider key would leak it into the client bundle.\n *\n * SEPARATE ENTRY on purpose: this module statically imports `vite`, which is a\n * dev-only tool. It is NOT re-exported from the `lib/ai` barrel (pinned in\n * `bundle.test.ts`), and the plugin is `apply: 'serve'` — no production build or\n * serverless mount ever includes it. Production mounts use `toNodeHandler` from\n * `@elytracms/studio/lib/ai` directly.\n */\nimport { loadEnv } from 'vite'\nimport type { Plugin, ViteDevServer } from 'vite'\nimport {\n INSECURE_ANONYMOUS_SESSION_ENV_VALUE,\n INSECURE_ANONYMOUS_SESSION_ENV_VAR,\n} from './convex-session'\nimport { toNodeHandler } from './node-handler'\nimport { DEFAULT_AI_ENDPOINT } from './route-provider'\nimport type { AiRouteHandler } from './types'\n\n/** Env keys the plugin surfaces from `.env*` files into `process.env` for the handler. */\nexport const DEFAULT_AI_DEV_ENV_KEYS = [\n 'ANTHROPIC_API_KEY',\n 'VITE_CONVEX_SITE_URL',\n 'VITE_CONVEX_URL',\n] as const\n\nexport interface AiDevPluginOptions {\n /** The handler module (Vite root-relative), default `/src/server/ai-handler.ts`. */\n handlerModulePath?: string\n /** The mounted route path (default {@link DEFAULT_AI_ENDPOINT}, `/api/ai`). */\n route?: string\n /** The handler module's export name (default `aiRouteHandler`). */\n exportName?: string\n /** Extra env keys to surface into `process.env` (merged over the defaults). */\n envKeys?: readonly string[]\n /**\n * INSECURE, dev-only: let requests presenting NO credential pass the session gate.\n * A playground studio has no Convex session, so without this the assistant panel\n * answers 401 there. `'playground-mode'` scopes the bypass to dev servers booted\n * with `VITE_ELYTRA_MODE=playground` (the recommended setting); `true` applies it\n * to every dev boot. Defaults OFF. It cannot reach production: the plugin is\n * `apply: 'serve'`, the handshake env var is set only in `configureServer`, and\n * the resolver additionally refuses it under `NODE_ENV=production`. A PRESENTED\n * token must still verify for real — the bypass never masks a bad credential.\n */\n insecureAllowAnonymousSessions?: boolean | 'playground-mode'\n}\n\n/** Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned). */\nexport function resolveInsecureAnonymousOptIn(\n optIn: boolean | 'playground-mode' | undefined,\n env: Record<string, string | undefined>,\n): boolean {\n if (optIn === true) return true\n if (optIn === 'playground-mode') return env.VITE_ELYTRA_MODE === 'playground'\n return false\n}\n\nfunction jsonError(code: string, status: number): Response {\n return new Response(JSON.stringify({ error: code }), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\n/**\n * Mount the AI route in the Vite dev server. Dev-only by construction\n * (`apply: 'serve'`); a production deploy mounts the same handler module via\n * `toNodeHandler` in its serverless entry instead.\n */\nexport function createAiDevPlugin(options: AiDevPluginOptions = {}): Plugin {\n const handlerModulePath = options.handlerModulePath ?? '/src/server/ai-handler.ts'\n const route = options.route ?? DEFAULT_AI_ENDPOINT\n const exportName = options.exportName ?? 'aiRouteHandler'\n let env: Record<string, string | undefined> = {}\n\n return {\n name: 'elytra-ai-route',\n apply: 'serve',\n configResolved(config) {\n env = loadEnv(config.mode, config.root, '')\n for (const key of [...DEFAULT_AI_DEV_ENV_KEYS, ...(options.envKeys ?? [])]) {\n if (env[key] && !process.env[key]) process.env[key] = env[key]\n }\n },\n configureServer(server: ViteDevServer) {\n if (resolveInsecureAnonymousOptIn(options.insecureAllowAnonymousSessions, env)) {\n process.env[INSECURE_ANONYMOUS_SESSION_ENV_VAR] = INSECURE_ANONYMOUS_SESSION_ENV_VALUE\n server.config.logger.warn(\n `[elytra-ai] insecureAllowAnonymousSessions is ON for this dev server: ${route} ` +\n 'accepts requests without a session token (requests presenting a token are still verified).',\n )\n }\n\n // Per request: SSR-load the handler (picks up its @elytracms/* imports through\n // Vite) and run it; a load/handler failure is a structured 500, never a crash.\n const bridge = toNodeHandler(async (request: Request): Promise<Response> => {\n try {\n const mod = (await server.ssrLoadModule(handlerModulePath)) as Record<string, unknown>\n const handler = mod[exportName]\n if (typeof handler !== 'function') return jsonError('route-handler-missing', 500)\n return await (handler as AiRouteHandler)(request)\n } catch {\n return jsonError('route-error', 500)\n }\n })\n\n server.middlewares.use((req, res, next) => {\n if (!req.url || !req.url.startsWith(route)) return next()\n void bridge(req, res).catch(() => {\n if (!res.headersSent) res.statusCode = 500\n res.end()\n })\n })\n },\n }\n}\n"]}
1
+ {"version":3,"file":"vite.js","sourceRoot":"","sources":["../../../src/lib/ai/vite.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `@elytracms/studio/lib/ai/vite` — `createAiDevPlugin`, the DEV half of the AI\n * route mounting kit (EC-371; closes EC-369 finding #2). A studio app is a client\n * SPA with no server build, so in dev the host route lives as a Vite dev-server\n * middleware: this plugin loads the framework-agnostic `Request → Response` handler\n * module through Vite's SSR pipeline (so its `@elytracms/*` imports resolve like the\n * rest of the app) and bridges Node req/res via the shared {@link toNodeHandler}.\n *\n * It also owns the env bridge the resolver needs (the piece the template had\n * drifted on): `loadEnv` surfaces `ANTHROPIC_API_KEY` / `VITE_CONVEX_SITE_URL` /\n * `VITE_CONVEX_URL` from `.env`/`.env.local` into `process.env` for the SSR-loaded\n * handler — Vite only exposes `VITE_`-prefixed vars via `import.meta.env`, and a\n * `VITE_` prefix on the provider key would leak it into the client bundle.\n *\n * SEPARATE ENTRY on purpose: this module statically imports `vite`, which is a\n * dev-only tool. It is NOT re-exported from the `lib/ai` barrel (pinned in\n * `bundle.test.ts`), and the plugin is `apply: 'serve'` — no production build or\n * serverless mount ever includes it. Production mounts use `toNodeHandler` from\n * `@elytracms/studio/lib/ai` directly.\n */\nimport { loadEnv } from 'vite'\nimport type { Plugin, ViteDevServer } from 'vite'\nimport { parseBackendMode } from '@elytracms/core/export-sync/deployment-ready'\nimport {\n INSECURE_ANONYMOUS_SESSION_ENV_VALUE,\n INSECURE_ANONYMOUS_SESSION_ENV_VAR,\n} from './convex-session'\nimport { toNodeHandler } from './node-handler'\nimport { DEFAULT_AI_ENDPOINT } from './route-provider'\nimport type { AiRouteHandler } from './types'\n\n/** Env keys the plugin surfaces from `.env*` files into `process.env` for the handler. */\nexport const DEFAULT_AI_DEV_ENV_KEYS = [\n 'ANTHROPIC_API_KEY',\n 'VITE_CONVEX_SITE_URL',\n 'VITE_CONVEX_URL',\n] as const\n\nexport interface AiDevPluginOptions {\n /** The handler module (Vite root-relative), default `/src/server/ai-handler.ts`. */\n handlerModulePath?: string\n /** The mounted route path (default {@link DEFAULT_AI_ENDPOINT}, `/api/ai`). */\n route?: string\n /** The handler module's export name (default `aiRouteHandler`). */\n exportName?: string\n /** Extra env keys to surface into `process.env` (merged over the defaults). */\n envKeys?: readonly string[]\n /**\n * INSECURE, dev-only: let requests presenting NO credential pass the session gate.\n * A playground studio has no Convex session, so without this the assistant panel\n * answers 401 there. `'playground-mode'` scopes the bypass to dev servers booted\n * with `VITE_ELYTRA_MODE=playground` (the recommended setting); `true` applies it\n * to every dev boot. Defaults OFF. It cannot reach production: the plugin is\n * `apply: 'serve'`, the handshake env var is set only in `configureServer`, and\n * the resolver additionally refuses it under `NODE_ENV=production`. A PRESENTED\n * token must still verify for real — the bypass never masks a bad credential.\n */\n insecureAllowAnonymousSessions?: boolean | 'playground-mode'\n}\n\n/**\n * Resolve the anonymous-bypass opt-in against the loaded env (pure; unit-pinned).\n * `'playground-mode'` asks the SAME question the studio asks of the same env — core's\n * `parseBackendMode` (EC-603) — so a padded flag the studio boots as playground opens\n * the bypass too, and a misspelled one (the studio's boot error) keeps it shut.\n */\nexport function resolveInsecureAnonymousOptIn(\n optIn: boolean | 'playground-mode' | undefined,\n env: Record<string, string | undefined>,\n): boolean {\n if (optIn === true) return true\n if (optIn === 'playground-mode') return parseBackendMode(env).kind === 'playground'\n return false\n}\n\n/** The plugin's `name` — how `elytra doctor` finds the AI route in a studio's Vite config. */\nexport const AI_ROUTE_PLUGIN_NAME = 'elytra-ai-route'\n\n/**\n * What the plugin mounts, exposed on its `api` (Vite's inter-plugin channel) so a tool\n * that loads the studio's Vite config can read the AI route from that config instead of\n * guessing a file path (EC-603: `elytra doctor` reads it to decide whether an AI\n * provider key is expected, and checks the handler module exists).\n */\nexport interface AiDevPluginApi {\n elytraAiRoute: { route: string; handlerModulePath: string; exportName: string }\n}\n\nfunction jsonError(code: string, status: number): Response {\n return new Response(JSON.stringify({ error: code }), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\n/**\n * Mount the AI route in the Vite dev server. Dev-only by construction\n * (`apply: 'serve'`); a production deploy mounts the same handler module via\n * `toNodeHandler` in its serverless entry instead.\n */\nexport function createAiDevPlugin(options: AiDevPluginOptions = {}): Plugin<AiDevPluginApi> {\n const handlerModulePath = options.handlerModulePath ?? '/src/server/ai-handler.ts'\n const route = options.route ?? DEFAULT_AI_ENDPOINT\n const exportName = options.exportName ?? 'aiRouteHandler'\n let env: Record<string, string | undefined> = {}\n\n return {\n name: AI_ROUTE_PLUGIN_NAME,\n apply: 'serve',\n api: { elytraAiRoute: { route, handlerModulePath, exportName } },\n configResolved(config) {\n env = loadEnv(config.mode, config.root, '')\n for (const key of [...DEFAULT_AI_DEV_ENV_KEYS, ...(options.envKeys ?? [])]) {\n if (env[key] && !process.env[key]) process.env[key] = env[key]\n }\n },\n configureServer(server: ViteDevServer) {\n if (resolveInsecureAnonymousOptIn(options.insecureAllowAnonymousSessions, env)) {\n process.env[INSECURE_ANONYMOUS_SESSION_ENV_VAR] = INSECURE_ANONYMOUS_SESSION_ENV_VALUE\n server.config.logger.warn(\n `[elytra-ai] insecureAllowAnonymousSessions is ON for this dev server: ${route} ` +\n 'accepts requests without a session token (requests presenting a token are still verified).',\n )\n }\n\n // Per request: SSR-load the handler (picks up its @elytracms/* imports through\n // Vite) and run it; a load/handler failure is a structured 500, never a crash.\n const bridge = toNodeHandler(async (request: Request): Promise<Response> => {\n try {\n const mod = (await server.ssrLoadModule(handlerModulePath)) as Record<string, unknown>\n const handler = mod[exportName]\n if (typeof handler !== 'function') return jsonError('route-handler-missing', 500)\n return await (handler as AiRouteHandler)(request)\n } catch {\n return jsonError('route-error', 500)\n }\n })\n\n server.middlewares.use((req, res, next) => {\n if (!req.url || !req.url.startsWith(route)) return next()\n void bridge(req, res).catch(() => {\n if (!res.headersSent) res.statusCode = 500\n res.end()\n })\n })\n },\n }\n}\n"]}
@@ -1,10 +1,3 @@
1
- /**
2
- * How a writer spells a BRAND-NEW placement, which has no sub-fields yet. This is
3
- * the caller's choice, not part of the carry rule. The detail editor and the table
4
- * cell use `'bare'` on a field without sub-fields and `'envelope'` on one with them
5
- * (`assetPickForm`). AI always uses `'envelope'` (EC-381).
6
- */
7
- export type NewPlacementForm = 'bare' | 'envelope';
8
1
  /** The ids the field should hold next. The shape follows the field's cardinality. */
9
2
  export type AssetPlacementTarget = {
10
3
  multiple: false;
@@ -22,8 +15,8 @@ export type AssetPlacementTarget = {
22
15
  * - **Multiple field.** An id the value already held keeps its entry verbatim, with
23
16
  * its sub-fields and its form (a bare id stays bare). Entries are matched in order,
24
17
  * so an image placed twice keeps both placements. An id the value did not hold
25
- * becomes a new placement in `form`. A held entry whose id is not in `ids` is
26
- * removed.
18
+ * becomes a new envelope. A held entry whose id is not in `ids` is removed, and an
19
+ * emptied list is ABSENT (`undefined`, EC-586), never `[]`.
27
20
  *
28
21
  * The rule carries every key except `type`/`id`, the same set that `assetSubfieldsOf`
29
22
  * reads. That includes keys the config no longer declares (AD-14: the schema is a
@@ -32,4 +25,4 @@ export type AssetPlacementTarget = {
32
25
  * When nothing changes it returns `current` itself (the same reference), so a caller
33
26
  * can skip the write.
34
27
  */
35
- export declare function carryAssetPlacements(current: unknown, next: AssetPlacementTarget, form: NewPlacementForm): unknown;
28
+ export declare function carryAssetPlacements(current: unknown, next: AssetPlacementTarget): unknown;
@@ -25,8 +25,15 @@
25
25
  * (import, CLI, MCP) appears.
26
26
  */
27
27
  import { assetNode, assetSubfieldsOf, readAssetId } from '@elytracms/core/cms-core';
28
- function newPlacement(id, form) {
29
- return form === 'envelope' ? assetNode(id) : id;
28
+ /**
29
+ * A BRAND-NEW placement, which has no sub-fields yet: always the canonical envelope
30
+ * (EC-586, ratified 2c — an asset value is `{ type: 'asset', id, … }`). There used to be
31
+ * a second, bare spelling (`'bare'`, a plain id string) for fields without sub-fields;
32
+ * the write lens wrapped it on save, so the in-session draft held a shape the stored one
33
+ * did not, and a def-less "has unpublished changes" verdict read the two as different.
34
+ */
35
+ function newPlacement(id) {
36
+ return assetNode(id);
30
37
  }
31
38
  /**
32
39
  * Return the field's next value with every placement's per-use sub-fields kept.
@@ -37,8 +44,8 @@ function newPlacement(id, form) {
37
44
  * - **Multiple field.** An id the value already held keeps its entry verbatim, with
38
45
  * its sub-fields and its form (a bare id stays bare). Entries are matched in order,
39
46
  * so an image placed twice keeps both placements. An id the value did not hold
40
- * becomes a new placement in `form`. A held entry whose id is not in `ids` is
41
- * removed.
47
+ * becomes a new envelope. A held entry whose id is not in `ids` is removed, and an
48
+ * emptied list is ABSENT (`undefined`, EC-586), never `[]`.
42
49
  *
43
50
  * The rule carries every key except `type`/`id`, the same set that `assetSubfieldsOf`
44
51
  * reads. That includes keys the config no longer declares (AD-14: the schema is a
@@ -47,7 +54,7 @@ function newPlacement(id, form) {
47
54
  * When nothing changes it returns `current` itself (the same reference), so a caller
48
55
  * can skip the write.
49
56
  */
50
- export function carryAssetPlacements(current, next, form) {
57
+ export function carryAssetPlacements(current, next) {
51
58
  if (!next.multiple) {
52
59
  if (next.id === null)
53
60
  return undefined;
@@ -57,18 +64,20 @@ export function carryAssetPlacements(current, next, form) {
57
64
  if (readAssetId(held) === next.id)
58
65
  return current;
59
66
  const subfields = assetSubfieldsOf(held);
60
- return Object.keys(subfields).length > 0 ? assetNode(next.id, subfields) : newPlacement(next.id, form);
67
+ return Object.keys(subfields).length > 0 ? assetNode(next.id, subfields) : newPlacement(next.id);
61
68
  }
62
69
  const held = Array.isArray(current) ? current : [];
63
70
  const used = new Set();
64
71
  const out = next.ids.map((id) => {
65
72
  const index = held.findIndex((entry, i) => !used.has(i) && readAssetId(entry) === id);
66
73
  if (index < 0)
67
- return newPlacement(id, form);
74
+ return newPlacement(id);
68
75
  used.add(index);
69
76
  return held[index];
70
77
  });
71
78
  const unchanged = out.length === held.length && out.every((entry, i) => entry === held[i]);
72
- return unchanged && Array.isArray(current) ? current : out;
79
+ if (unchanged && Array.isArray(current))
80
+ return current;
81
+ return out.length === 0 ? undefined : out;
73
82
  }
74
83
  //# sourceMappingURL=asset-placements.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"asset-placements.js","sourceRoot":"","sources":["../../../src/lib/assets/asset-placements.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAenF,SAAS,YAAY,CAAC,EAAU,EAAE,IAAsB;IACtD,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AACjD,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAgB,EAChB,IAA0B,EAC1B,IAAsB;IAEtB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI;YAAE,OAAO,SAAS,CAAA;QACtC,iFAAiF;QACjF,8BAA8B;QAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;QAC1D,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE;YAAE,OAAO,OAAO,CAAA;QACjD,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;QACxC,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IACxG,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAE,OAAqB,CAAC,CAAC,CAAC,EAAE,CAAA;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;QACrF,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;QAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC,CAAC,CAAA;IACF,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1F,OAAO,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAA;AAC5D,CAAC","sourcesContent":["/**\n * The per-use sub-field carry rule for an asset field's value (EC-558).\n *\n * An asset field that declares per-use sub-fields (EC-301) stores envelopes such as\n * `{ type: 'asset', id, caption }`. Those sub-fields are PLACEMENT data: they\n * describe this use of the image, not the image itself (owner ruling 2026-09-26;\n * data that describes the image belongs on the asset record's `meta`). A writer\n * that changes which image a placement shows must therefore keep the placement's\n * sub-fields. Before EC-558 only the detail editor did that. The table cell and AI\n * `updateFields` rebuilt the value from bare ids and lost them.\n *\n * {@link carryAssetPlacements} is the one declaration of that rule. Two operations\n * go through it:\n * - **append / pick**: the table cell and the detail editor\n * (`applyAssetSelection` / `appendAssetSelections` in\n * `components/assets/asset-selection.ts`). The next list is the held list plus\n * the picked ids, and a single field swaps its one placement's image.\n * - **replace-with-list**: AI `updateFields` (`lib/ai/update-fields.ts`). The next\n * list is the list the model sent.\n *\n * It lives in `lib/` because `lib/ai` may not import from `components/`, and both\n * sides need it. Its only dependency is `@elytracms/core/cms-core`, so the AI host\n * route pulls in nothing from the browser. Its natural home is next to `assetNode`\n * in `core/cms-core/envelopes.ts`. Move it there if a writer outside the studio\n * (import, CLI, MCP) appears.\n */\nimport { assetNode, assetSubfieldsOf, readAssetId } from '@elytracms/core/cms-core'\n\n/**\n * How a writer spells a BRAND-NEW placement, which has no sub-fields yet. This is\n * the caller's choice, not part of the carry rule. The detail editor and the table\n * cell use `'bare'` on a field without sub-fields and `'envelope'` on one with them\n * (`assetPickForm`). AI always uses `'envelope'` (EC-381).\n */\nexport type NewPlacementForm = 'bare' | 'envelope'\n\n/** The ids the field should hold next. The shape follows the field's cardinality. */\nexport type AssetPlacementTarget =\n | { multiple: false; id: string | null }\n | { multiple: true; ids: readonly string[] }\n\nfunction newPlacement(id: string, form: NewPlacementForm): unknown {\n return form === 'envelope' ? assetNode(id) : id\n}\n\n/**\n * Return the field's next value with every placement's per-use sub-fields kept.\n *\n * - **Single field.** The placement's sub-fields carry over to whatever id now fills\n * it, so swapping the image keeps the caption. Re-picking the same id is a no-op,\n * and `null` clears the field (`undefined`).\n * - **Multiple field.** An id the value already held keeps its entry verbatim, with\n * its sub-fields and its form (a bare id stays bare). Entries are matched in order,\n * so an image placed twice keeps both placements. An id the value did not hold\n * becomes a new placement in `form`. A held entry whose id is not in `ids` is\n * removed.\n *\n * The rule carries every key except `type`/`id`, the same set that `assetSubfieldsOf`\n * reads. That includes keys the config no longer declares (AD-14: the schema is a\n * lens, so a writer never deletes what the lens does not show).\n *\n * When nothing changes it returns `current` itself (the same reference), so a caller\n * can skip the write.\n */\nexport function carryAssetPlacements(\n current: unknown,\n next: AssetPlacementTarget,\n form: NewPlacementForm,\n): unknown {\n if (!next.multiple) {\n if (next.id === null) return undefined\n // `singleAssetIdOf` tolerates a legacy one-item array on a single field, so read\n // its placement the same way.\n const held = Array.isArray(current) ? current[0] : current\n if (readAssetId(held) === next.id) return current\n const subfields = assetSubfieldsOf(held)\n return Object.keys(subfields).length > 0 ? assetNode(next.id, subfields) : newPlacement(next.id, form)\n }\n\n const held = Array.isArray(current) ? (current as unknown[]) : []\n const used = new Set<number>()\n const out = next.ids.map((id) => {\n const index = held.findIndex((entry, i) => !used.has(i) && readAssetId(entry) === id)\n if (index < 0) return newPlacement(id, form)\n used.add(index)\n return held[index]\n })\n const unchanged = out.length === held.length && out.every((entry, i) => entry === held[i])\n return unchanged && Array.isArray(current) ? current : out\n}\n"]}
1
+ {"version":3,"file":"asset-placements.js","sourceRoot":"","sources":["../../../src/lib/assets/asset-placements.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAOnF;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,EAAU;IAC9B,OAAO,SAAS,CAAC,EAAE,CAAC,CAAA;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgB,EAAE,IAA0B;IAC/E,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI;YAAE,OAAO,SAAS,CAAA;QACtC,iFAAiF;QACjF,8BAA8B;QAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;QAC1D,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE;YAAE,OAAO,OAAO,CAAA;QACjD,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;QACxC,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAClG,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAE,OAAqB,CAAC,CAAC,CAAC,EAAE,CAAA;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;QACrF,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,YAAY,CAAC,EAAE,CAAC,CAAA;QACtC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC,CAAC,CAAA;IACF,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1F,IAAI,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAA;IACvD,OAAO,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAA;AAC3C,CAAC","sourcesContent":["/**\n * The per-use sub-field carry rule for an asset field's value (EC-558).\n *\n * An asset field that declares per-use sub-fields (EC-301) stores envelopes such as\n * `{ type: 'asset', id, caption }`. Those sub-fields are PLACEMENT data: they\n * describe this use of the image, not the image itself (owner ruling 2026-09-26;\n * data that describes the image belongs on the asset record's `meta`). A writer\n * that changes which image a placement shows must therefore keep the placement's\n * sub-fields. Before EC-558 only the detail editor did that. The table cell and AI\n * `updateFields` rebuilt the value from bare ids and lost them.\n *\n * {@link carryAssetPlacements} is the one declaration of that rule. Two operations\n * go through it:\n * - **append / pick**: the table cell and the detail editor\n * (`applyAssetSelection` / `appendAssetSelections` in\n * `components/assets/asset-selection.ts`). The next list is the held list plus\n * the picked ids, and a single field swaps its one placement's image.\n * - **replace-with-list**: AI `updateFields` (`lib/ai/update-fields.ts`). The next\n * list is the list the model sent.\n *\n * It lives in `lib/` because `lib/ai` may not import from `components/`, and both\n * sides need it. Its only dependency is `@elytracms/core/cms-core`, so the AI host\n * route pulls in nothing from the browser. Its natural home is next to `assetNode`\n * in `core/cms-core/envelopes.ts`. Move it there if a writer outside the studio\n * (import, CLI, MCP) appears.\n */\nimport { assetNode, assetSubfieldsOf, readAssetId } from '@elytracms/core/cms-core'\n\n/** The ids the field should hold next. The shape follows the field's cardinality. */\nexport type AssetPlacementTarget =\n | { multiple: false; id: string | null }\n | { multiple: true; ids: readonly string[] }\n\n/**\n * A BRAND-NEW placement, which has no sub-fields yet: always the canonical envelope\n * (EC-586, ratified 2c — an asset value is `{ type: 'asset', id, … }`). There used to be\n * a second, bare spelling (`'bare'`, a plain id string) for fields without sub-fields;\n * the write lens wrapped it on save, so the in-session draft held a shape the stored one\n * did not, and a def-less \"has unpublished changes\" verdict read the two as different.\n */\nfunction newPlacement(id: string): unknown {\n return assetNode(id)\n}\n\n/**\n * Return the field's next value with every placement's per-use sub-fields kept.\n *\n * - **Single field.** The placement's sub-fields carry over to whatever id now fills\n * it, so swapping the image keeps the caption. Re-picking the same id is a no-op,\n * and `null` clears the field (`undefined`).\n * - **Multiple field.** An id the value already held keeps its entry verbatim, with\n * its sub-fields and its form (a bare id stays bare). Entries are matched in order,\n * so an image placed twice keeps both placements. An id the value did not hold\n * becomes a new envelope. A held entry whose id is not in `ids` is removed, and an\n * emptied list is ABSENT (`undefined`, EC-586), never `[]`.\n *\n * The rule carries every key except `type`/`id`, the same set that `assetSubfieldsOf`\n * reads. That includes keys the config no longer declares (AD-14: the schema is a\n * lens, so a writer never deletes what the lens does not show).\n *\n * When nothing changes it returns `current` itself (the same reference), so a caller\n * can skip the write.\n */\nexport function carryAssetPlacements(current: unknown, next: AssetPlacementTarget): unknown {\n if (!next.multiple) {\n if (next.id === null) return undefined\n // `singleAssetIdOf` tolerates a legacy one-item array on a single field, so read\n // its placement the same way.\n const held = Array.isArray(current) ? current[0] : current\n if (readAssetId(held) === next.id) return current\n const subfields = assetSubfieldsOf(held)\n return Object.keys(subfields).length > 0 ? assetNode(next.id, subfields) : newPlacement(next.id)\n }\n\n const held = Array.isArray(current) ? (current as unknown[]) : []\n const used = new Set<number>()\n const out = next.ids.map((id) => {\n const index = held.findIndex((entry, i) => !used.has(i) && readAssetId(entry) === id)\n if (index < 0) return newPlacement(id)\n used.add(index)\n return held[index]\n })\n const unchanged = out.length === held.length && out.every((entry, i) => entry === held[i])\n if (unchanged && Array.isArray(current)) return current\n return out.length === 0 ? undefined : out\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { getNode, getParentInfo } from '@elytracms/core/project-graph';
2
2
  import { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
3
- import { shortId } from '@elytracms/studio/lib/persona/labels';
3
+ import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
4
4
  export function resolvePasteTarget(args) {
5
5
  const { root, componentId, getManifest, fieldAllow } = args;
6
6
  const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined;
@@ -19,7 +19,7 @@ export function resolvePasteTarget(args) {
19
19
  const manifest = getManifest(node.componentId);
20
20
  const childSlot = primaryInsertSlot(manifestRules(manifest));
21
21
  if (childSlot) {
22
- return leashed(childSlot.name, childSlot, { parentId: node.id, slot: childSlot.name }, `${shortId(node.componentId)} · ${childSlot.name}`);
22
+ return leashed(childSlot.name, childSlot, { parentId: node.id, slot: childSlot.name }, `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`);
23
23
  }
24
24
  const info = getParentInfo(root, node.id);
25
25
  if (info) {
@@ -27,7 +27,7 @@ export function resolvePasteTarget(args) {
27
27
  // before/after case). An unknown parent manifest imposes no allow-list;
28
28
  // validation owns that issue.
29
29
  const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot);
30
- return leashed(info.slot, parentSlot, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${shortId(info.parent.componentId)} · ${info.slot}`);
30
+ return leashed(info.slot, parentSlot, { parentId: info.parent.id, slot: info.slot, index: info.index + 1 }, `${componentDisplayName(info.parent.componentId, getManifest(info.parent.componentId))} · ${info.slot}`);
31
31
  }
32
32
  // Leaf root with no declared slots (degenerate graphs): append to `children`.
33
33
  // No slot rule exists, but the paste still lands at the root — the field leash
@@ -1 +1 @@
1
- {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { SlotRule } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n *\n * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted\n * subtree's root, through the same `slotAllows` the drop rule reads — so a\n * component drag-and-drop refuses is refused by clipboard too, and a caller\n * cannot reach an `InsertTarget` without passing the check (the rejection is a\n * separate member of the result union, not a flag beside the target). Nested\n * validity of the pasted subtree stays with the publish gate, exactly like a\n * drop.\n *\n * EC-554: the composition FIELD's top-level `allow` is checked here too, through\n * the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer\n * read — a paste that lands as a direct child of the composition root must be in\n * the field's vocabulary.\n */\n\nexport interface PasteTarget {\n ok: true\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\n/** The resolved slot's own `allow` list excludes the pasted root (EC-545). */\nexport interface PasteSlotRejection {\n ok: false\n code: 'not-allowed'\n /** The pasted root the slot refuses. */\n componentId: string\n /** The resolved slot whose `allow` list excludes it. */\n slot: string\n /** Human label of the refused destination (`Section · body`). */\n destinationLabel: string\n}\n\n/**\n * The composition field's top-level `allow` excludes the pasted root (EC-554).\n * Deliberately carries NO slot/destination label: the refused slot is the\n * invisible synthetic root's (`Stack · children`), and the reason to show is the\n * FIELD — the notice names it from the editor's field scope.\n */\nexport interface PasteFieldRejection {\n ok: false\n code: 'field-not-allowed'\n /** The pasted root the field vocabulary refuses. */\n componentId: string\n}\n\nexport type PasteRejection = PasteSlotRejection | PasteFieldRejection\n\nexport type PasteResolution = PasteTarget | PasteRejection\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n /** The pasted subtree's root component — the id the slot leash judges. */\n componentId: string\n getManifest: (componentId: string) => ComponentManifest | undefined\n /**\n * The composition field's top-level `allow` (EC-554) — `undefined` when the\n * field declares none. Required so no caller can forget to pass it.\n */\n fieldAllow: readonly string[] | undefined\n}): PasteResolution {\n const { root, componentId, getManifest, fieldAllow } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const leashed = (\n slot: string,\n slotRule: SlotRule | undefined,\n target: InsertTarget,\n destinationLabel: string,\n ): PasteResolution => {\n const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId })\n if (refusal === 'field') return { ok: false, code: 'field-not-allowed', componentId }\n if (refusal === 'slot') return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel }\n return { ok: true, target, destinationLabel }\n }\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return leashed(\n childSlot.name,\n childSlot,\n { parentId: node.id, slot: childSlot.name },\n `${shortId(node.componentId)} · ${childSlot.name}`,\n )\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n // A sibling paste is governed by the PARENT's slot leash (the drop rule's\n // before/after case). An unknown parent manifest imposes no allow-list;\n // validation owns that issue.\n const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot)\n return leashed(\n info.slot,\n parentSlot,\n { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n `${shortId(info.parent.componentId)} · ${info.slot}`,\n )\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n // No slot rule exists, but the paste still lands at the root — the field leash\n // applies.\n return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root')\n}\n"]}
1
+ {"version":3,"file":"paste-target.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-target.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { manifestRules, placementRefusal, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { SlotRule } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Paste landing-spot resolution (EC-175) — the same placement rule the Insert\n * dialog uses (`insert-context.ts`): a pasted subtree lands *into*\n * the anchor node's child slot when it has one, else *after* the anchor as a\n * sibling, else appended to the page root. Pure and selection-driven so the\n * canvas, the page tree, and keyboard paste all share one rule.\n *\n * EC-545: the resolved slot's `allow` leash is checked HERE, for the pasted\n * subtree's root, through the same `slotAllows` the drop rule reads — so a\n * component drag-and-drop refuses is refused by clipboard too, and a caller\n * cannot reach an `InsertTarget` without passing the check (the rejection is a\n * separate member of the result union, not a flag beside the target). Nested\n * validity of the pasted subtree stays with the publish gate, exactly like a\n * drop.\n *\n * EC-554: the composition FIELD's top-level `allow` is checked here too, through\n * the same `placementRefusal` → `fieldAllowAt` the drop rule and the insert offer\n * read — a paste that lands as a direct child of the composition root must be in\n * the field's vocabulary.\n */\n\nexport interface PasteTarget {\n ok: true\n target: InsertTarget\n /** Human label of where the paste lands (notices/debugging). */\n destinationLabel: string\n}\n\n/** The resolved slot's own `allow` list excludes the pasted root (EC-545). */\nexport interface PasteSlotRejection {\n ok: false\n code: 'not-allowed'\n /** The pasted root the slot refuses. */\n componentId: string\n /** The resolved slot whose `allow` list excludes it. */\n slot: string\n /** Human label of the refused destination (`Section · body`). */\n destinationLabel: string\n}\n\n/**\n * The composition field's top-level `allow` excludes the pasted root (EC-554).\n * Deliberately carries NO slot/destination label: the refused slot is the\n * invisible synthetic root's (`Stack · children`), and the reason to show is the\n * FIELD — the notice names it from the editor's field scope.\n */\nexport interface PasteFieldRejection {\n ok: false\n code: 'field-not-allowed'\n /** The pasted root the field vocabulary refuses. */\n componentId: string\n}\n\nexport type PasteRejection = PasteSlotRejection | PasteFieldRejection\n\nexport type PasteResolution = PasteTarget | PasteRejection\n\nexport function resolvePasteTarget(args: {\n root: ComponentNode\n /** The anchor node (usually the selection); `null` falls back to the root. */\n anchorId: string | null\n /** The pasted subtree's root component — the id the slot leash judges. */\n componentId: string\n getManifest: (componentId: string) => ComponentManifest | undefined\n /**\n * The composition field's top-level `allow` (EC-554) — `undefined` when the\n * field declares none. Required so no caller can forget to pass it.\n */\n fieldAllow: readonly string[] | undefined\n}): PasteResolution {\n const { root, componentId, getManifest, fieldAllow } = args\n const anchor = args.anchorId !== null ? getNode(root, args.anchorId) : undefined\n const node = anchor ?? root\n\n const leashed = (\n slot: string,\n slotRule: SlotRule | undefined,\n target: InsertTarget,\n destinationLabel: string,\n ): PasteResolution => {\n const refusal = placementRefusal({ root, parentId: target.parentId, slot: slotRule, fieldAllow, componentId })\n if (refusal === 'field') return { ok: false, code: 'field-not-allowed', componentId }\n if (refusal === 'slot') return { ok: false, code: 'not-allowed', componentId, slot, destinationLabel }\n return { ok: true, target, destinationLabel }\n }\n\n // EC-386: the shared slot rule (`primaryInsertSlot` over the merged\n // `manifestRules` vocabulary) — a blocks-prop region (e.g. a Section's `body`)\n // receives the paste exactly like the insert dialog and a DnD `into`.\n const manifest = getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n if (childSlot) {\n return leashed(\n childSlot.name,\n childSlot,\n { parentId: node.id, slot: childSlot.name },\n `${componentDisplayName(node.componentId, manifest)} · ${childSlot.name}`,\n )\n }\n\n const info = getParentInfo(root, node.id)\n if (info) {\n // A sibling paste is governed by the PARENT's slot leash (the drop rule's\n // before/after case). An unknown parent manifest imposes no allow-list;\n // validation owns that issue.\n const parentSlot = manifestRules(getManifest(info.parent.componentId))?.slots.find((s) => s.name === info.slot)\n return leashed(\n info.slot,\n parentSlot,\n { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n `${componentDisplayName(info.parent.componentId, getManifest(info.parent.componentId))} · ${info.slot}`,\n )\n }\n\n // Leaf root with no declared slots (degenerate graphs): append to `children`.\n // No slot rule exists, but the paste still lands at the root — the field leash\n // applies.\n return leashed('children', undefined, { parentId: root.id, slot: 'children' }, 'root')\n}\n"]}
@@ -10,15 +10,26 @@ import type { OperationsClient } from '../operations/index.js';
10
10
  * URL from these ancestors yields the URL the live site actually resolves.
11
11
  *
12
12
  * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache
13
- * seam — invalidated only when a publish/unpublish/restore/delete command
14
- * succeeds (NOT on every autosave upsert — a draft edit doesn't change what's
15
- * published). Cost: ONE bulk `documents.listPublished` read (EC-307).
13
+ * seam — invalidated only when a command that moves a live pin succeeds (NOT on
14
+ * every autosave upsert — a draft edit doesn't change what's published). Cost: ONE
15
+ * bulk `documents.listPublished` read (EC-307).
16
16
  */
17
17
  export interface PublishedDocuments {
18
18
  /** Pinned published snapshot per published document id. Empty while loading. */
19
19
  byId: ReadonlyMap<string, CmsDocument>;
20
20
  status: 'loading' | 'ready';
21
21
  }
22
+ /**
23
+ * Commands that change which version is pinned live (and thus the pool).
24
+ *
25
+ * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a
26
+ * changeset revert restores a prior live version — and was missing, so a revert left
27
+ * the live URLs on the old pool until a reload. `documents.restore` was here and
28
+ * moves nothing: it writes the DRAFT from a recorded version (`cms.upsertDocument`),
29
+ * never the pin. The same answer the Convex trigger gives server-side: only a pin
30
+ * move or the delete of a pinned row changes published output.
31
+ */
32
+ export declare const PUBLISHED_RELOAD_OPS: readonly ["documents.publish", "documents.unpublish", "documents.pinVersion", "documents.delete", "documents.clear"];
22
33
  /**
23
34
  * Build the published pool of a collection (pure over the operations client): the
24
35
  * pinned published snapshot of every currently-published document, keyed by id.
@@ -1,9 +1,18 @@
1
1
  import { useOperationQuery } from '@elytracms/studio/lib/query';
2
- /** Commands that change which version is pinned live (and thus the pool). */
3
- const PUBLISHED_RELOAD_OPS = [
2
+ /**
3
+ * Commands that change which version is pinned live (and thus the pool).
4
+ *
5
+ * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a
6
+ * changeset revert restores a prior live version — and was missing, so a revert left
7
+ * the live URLs on the old pool until a reload. `documents.restore` was here and
8
+ * moves nothing: it writes the DRAFT from a recorded version (`cms.upsertDocument`),
9
+ * never the pin. The same answer the Convex trigger gives server-side: only a pin
10
+ * move or the delete of a pinned row changes published output.
11
+ */
12
+ export const PUBLISHED_RELOAD_OPS = [
4
13
  'documents.publish',
5
14
  'documents.unpublish',
6
- 'documents.restore',
15
+ 'documents.pinVersion',
7
16
  'documents.delete',
8
17
  'documents.clear',
9
18
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D,6EAA6E;AAC7E,MAAM,oBAAoB,GAAG;IAC3B,mBAAmB;IACnB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV,MAAM,UAAU,GAAqC,IAAI,GAAG,EAAE,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACzE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationsClient } from '../operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a publish/unpublish/restore/delete command\n * succeeds (NOT on every autosave upsert — a draft edit doesn't change what's\n * published). Cost: ONE bulk `documents.listPublished` read (EC-307).\n */\nexport interface PublishedDocuments {\n /** Pinned published snapshot per published document id. Empty while loading. */\n byId: ReadonlyMap<string, CmsDocument>\n status: 'loading' | 'ready'\n}\n\n/** Commands that change which version is pinned live (and thus the pool). */\nconst PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.restore',\n 'documents.delete',\n 'documents.clear',\n] as const\n\nconst EMPTY_POOL: ReadonlyMap<string, CmsDocument> = new Map()\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. Read failures degrade to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) return new Map()\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return { byId: EMPTY_POOL, status: 'ready' }\n return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' }\n}\n"]}
1
+ {"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAsB/D;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV,MAAM,UAAU,GAAqC,IAAI,GAAG,EAAE,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACzE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationsClient } from '../operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a command that moves a live pin succeeds (NOT on\n * every autosave upsert — a draft edit doesn't change what's published). Cost: ONE\n * bulk `documents.listPublished` read (EC-307).\n */\nexport interface PublishedDocuments {\n /** Pinned published snapshot per published document id. Empty while loading. */\n byId: ReadonlyMap<string, CmsDocument>\n status: 'loading' | 'ready'\n}\n\n/**\n * Commands that change which version is pinned live (and thus the pool).\n *\n * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a\n * changeset revert restores a prior live version — and was missing, so a revert left\n * the live URLs on the old pool until a reload. `documents.restore` was here and\n * moves nothing: it writes the DRAFT from a recorded version (`cms.upsertDocument`),\n * never the pin. The same answer the Convex trigger gives server-side: only a pin\n * move or the delete of a pinned row changes published output.\n */\nexport const PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.delete',\n 'documents.clear',\n] as const\n\nconst EMPTY_POOL: ReadonlyMap<string, CmsDocument> = new Map()\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. Read failures degrade to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) return new Map()\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return { byId: EMPTY_POOL, status: 'ready' }\n return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { CmsDocument, CollectionDef, FieldDef, LocaleConfig, LocalizedString } from '@elytracms/core/cms-core';
2
+ import type { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocalizedString } from '@elytracms/core/cms-core';
3
3
  import type { FilterOperator, FilterScalar } from '@elytracms/core/content';
4
4
  /**
5
5
  * The pure table-view model for the CMS collection table (EC-169).
@@ -239,14 +239,14 @@ export type IsPublished = (doc: CmsDocument) => boolean;
239
239
  * `matchesFieldValue` over the RAW field value (so `contains`/`empty`/multiple-
240
240
  * relation membership all behave the same on the table as on the live site).
241
241
  */
242
- export declare function clauseMatches(collection: CollectionDef, doc: CmsDocument, localeConfig: LocaleConfig, clause: FilterClause, isPublished?: IsPublished): boolean;
242
+ export declare function clauseMatches(collection: CollectionDef, doc: CmsDocument, localeConfig: LocaleConfig, locale: Locale, clause: FilterClause, isPublished?: IsPublished): boolean;
243
243
  /**
244
244
  * Sort documents by one column in the canonical deterministic order
245
245
  * (`compareListEntries`: typed value order, id-ascending tiebreak). Unlike the
246
246
  * content accessors, the *table* may sort any rendered column — sorting is a
247
247
  * presentation concern here; the `filterable` flag governs filters.
248
248
  */
249
- export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, sort: ViewSort | null, isPublished?: IsPublished, meta?: PseudoColumnMeta): CmsDocument[];
249
+ export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, locale: Locale, sort: ViewSort | null, isPublished?: IsPublished, meta?: PseudoColumnMeta): CmsDocument[];
250
250
  /** The filtered + sorted documents of a view, plus any clause issues. */
251
251
  export interface AppliedViewState {
252
252
  docs: CmsDocument[];
@@ -258,7 +258,7 @@ export interface AppliedViewState {
258
258
  * ones, sort. Invalid clauses never filter silently — they come back as
259
259
  * issues for the UI to surface.
260
260
  */
261
- export declare function applyViewState(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, state: TableViewState, isPublished?: IsPublished, meta?: PseudoColumnMeta): AppliedViewState;
261
+ export declare function applyViewState(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, locale: Locale, state: TableViewState, isPublished?: IsPublished, meta?: PseudoColumnMeta): AppliedViewState;
262
262
  /** The id of the synthesized built-in "All entries" baseline view (EC-444). */
263
263
  export declare const ALL_VIEW_ID = "all";
264
264
  export declare function defaultViewState(collection: CollectionDef, pseudo?: readonly string[]): TableViewState;