@elytracms/studio 0.0.91 → 0.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/convex/asset-alt-migration.test.ts +127 -0
  2. package/convex/asset-replace.test.ts +168 -0
  3. package/convex/asset-revalidate.test.ts +6 -6
  4. package/convex/assets.ts +41 -5
  5. package/convex/cms.ts +537 -177
  6. package/convex/contract-parity.test.ts +173 -19
  7. package/convex/delivery.ts +26 -13
  8. package/convex/document-revalidate.test.ts +17 -16
  9. package/convex/orphan-hydration.test.ts +116 -0
  10. package/convex/presence-cost.test.ts +89 -0
  11. package/convex/rail-write-parity.test.ts +17 -14
  12. package/convex/referenceMaintenance.ts +15 -15
  13. package/convex/schema.ts +70 -5
  14. package/convex/triggers.ts +21 -8
  15. package/convex/write-limits.ts +1 -1
  16. package/dist/components/ai/ai-chat-panel.js +33 -22
  17. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  18. package/dist/components/ai/proposal-card.js +2 -1
  19. package/dist/components/ai/proposal-card.js.map +1 -1
  20. package/dist/components/ai/translate-document-action.d.ts +8 -2
  21. package/dist/components/ai/translate-document-action.js.map +1 -1
  22. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  23. package/dist/components/ai/translate-document-dialog.js +14 -8
  24. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  25. package/dist/components/ai/translate-skipped.d.ts +19 -0
  26. package/dist/components/ai/translate-skipped.js +46 -0
  27. package/dist/components/ai/translate-skipped.js.map +1 -0
  28. package/dist/components/app-views/default-modules.d.ts +23 -6
  29. package/dist/components/app-views/default-modules.js +33 -14
  30. package/dist/components/app-views/default-modules.js.map +1 -1
  31. package/dist/components/assets/asset-detail.js +18 -8
  32. package/dist/components/assets/asset-detail.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +15 -4
  34. package/dist/components/assets/asset-metadata.js +15 -2
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.js +4 -1
  37. package/dist/components/assets/asset-search.js.map +1 -1
  38. package/dist/components/assets/asset-selection.d.ts +0 -8
  39. package/dist/components/assets/asset-selection.js +4 -23
  40. package/dist/components/assets/asset-selection.js.map +1 -1
  41. package/dist/components/assets/asset-thumb.js +3 -2
  42. package/dist/components/assets/asset-thumb.js.map +1 -1
  43. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  44. package/dist/components/assets/assets-collection-view-source.js +9 -5
  45. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  46. package/dist/components/assets/assets-store.d.ts +14 -0
  47. package/dist/components/assets/assets-store.js +23 -1
  48. package/dist/components/assets/assets-store.js.map +1 -1
  49. package/dist/components/assets/replace-file.d.ts +22 -0
  50. package/dist/components/assets/replace-file.js +59 -0
  51. package/dist/components/assets/replace-file.js.map +1 -0
  52. package/dist/components/builder/blocks-model.d.ts +14 -26
  53. package/dist/components/builder/blocks-model.js +35 -41
  54. package/dist/components/builder/blocks-model.js.map +1 -1
  55. package/dist/components/builder/blocks-panel.d.ts +7 -3
  56. package/dist/components/builder/blocks-panel.js +6 -5
  57. package/dist/components/builder/blocks-panel.js.map +1 -1
  58. package/dist/components/builder/builder-store.d.ts +126 -30
  59. package/dist/components/builder/builder-store.js +169 -67
  60. package/dist/components/builder/builder-store.js.map +1 -1
  61. package/dist/components/builder/canvas-body.d.ts +30 -0
  62. package/dist/components/builder/canvas-body.js +38 -0
  63. package/dist/components/builder/canvas-body.js.map +1 -0
  64. package/dist/components/builder/canvas-chrome-css.js +7 -0
  65. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  66. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  67. package/dist/components/builder/canvas-dnd.js +5 -5
  68. package/dist/components/builder/canvas-dnd.js.map +1 -1
  69. package/dist/components/builder/canvas-render-context.d.ts +30 -22
  70. package/dist/components/builder/canvas-render-context.js +44 -44
  71. package/dist/components/builder/canvas-render-context.js.map +1 -1
  72. package/dist/components/builder/canvas-runs.d.ts +45 -0
  73. package/dist/components/builder/canvas-runs.js +51 -0
  74. package/dist/components/builder/canvas-runs.js.map +1 -0
  75. package/dist/components/builder/canvas.d.ts +5 -4
  76. package/dist/components/builder/canvas.js +34 -6
  77. package/dist/components/builder/canvas.js.map +1 -1
  78. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  79. package/dist/components/builder/clipboard-actions.js +50 -13
  80. package/dist/components/builder/clipboard-actions.js.map +1 -1
  81. package/dist/components/builder/composition-field-editor.d.ts +10 -16
  82. package/dist/components/builder/composition-field-editor.js +30 -40
  83. package/dist/components/builder/composition-field-editor.js.map +1 -1
  84. package/dist/components/builder/composition-patches.d.ts +7 -0
  85. package/dist/components/builder/composition-patches.js +256 -0
  86. package/dist/components/builder/composition-patches.js.map +1 -0
  87. package/dist/components/builder/delete-key.d.ts +5 -0
  88. package/dist/components/builder/delete-key.js +4 -0
  89. package/dist/components/builder/delete-key.js.map +1 -1
  90. package/dist/components/builder/insert-context.d.ts +17 -25
  91. package/dist/components/builder/insert-context.js +45 -92
  92. package/dist/components/builder/insert-context.js.map +1 -1
  93. package/dist/components/builder/page-tree.d.ts +3 -3
  94. package/dist/components/builder/page-tree.js +3 -3
  95. package/dist/components/builder/page-tree.js.map +1 -1
  96. package/dist/components/builder/placement-refusal.d.ts +18 -0
  97. package/dist/components/builder/placement-refusal.js +46 -0
  98. package/dist/components/builder/placement-refusal.js.map +1 -0
  99. package/dist/components/builder/property-inspector.js +4 -0
  100. package/dist/components/builder/property-inspector.js.map +1 -1
  101. package/dist/components/builder/reuse-context.js +65 -26
  102. package/dist/components/builder/reuse-context.js.map +1 -1
  103. package/dist/components/builder/reuse.d.ts +79 -8
  104. package/dist/components/builder/reuse.js +71 -23
  105. package/dist/components/builder/reuse.js.map +1 -1
  106. package/dist/components/builder/run-inspector.d.ts +11 -0
  107. package/dist/components/builder/run-inspector.js +33 -0
  108. package/dist/components/builder/run-inspector.js.map +1 -0
  109. package/dist/components/cms/asset-field.d.ts +38 -1
  110. package/dist/components/cms/asset-field.js +182 -58
  111. package/dist/components/cms/asset-field.js.map +1 -1
  112. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  113. package/dist/components/cms/asset-picker-core.js +20 -0
  114. package/dist/components/cms/asset-picker-core.js.map +1 -1
  115. package/dist/components/cms/asset-picker.js +12 -18
  116. package/dist/components/cms/asset-picker.js.map +1 -1
  117. package/dist/components/cms/bulk-actions.d.ts +4 -20
  118. package/dist/components/cms/bulk-actions.js +3 -24
  119. package/dist/components/cms/bulk-actions.js.map +1 -1
  120. package/dist/components/cms/cell-edit.d.ts +5 -5
  121. package/dist/components/cms/cell-edit.js.map +1 -1
  122. package/dist/components/cms/cell.js.map +1 -1
  123. package/dist/components/cms/cms-store.d.ts +87 -45
  124. package/dist/components/cms/cms-store.js +347 -203
  125. package/dist/components/cms/cms-store.js.map +1 -1
  126. package/dist/components/cms/collection-table.js +7 -2
  127. package/dist/components/cms/collection-table.js.map +1 -1
  128. package/dist/components/cms/commit-policy.d.ts +24 -0
  129. package/dist/components/cms/commit-policy.js +37 -0
  130. package/dist/components/cms/commit-policy.js.map +1 -0
  131. package/dist/components/cms/detail-editor.js +83 -137
  132. package/dist/components/cms/detail-editor.js.map +1 -1
  133. package/dist/components/cms/document-diff-view.d.ts +3 -1
  134. package/dist/components/cms/document-diff-view.js +12 -7
  135. package/dist/components/cms/document-diff-view.js.map +1 -1
  136. package/dist/components/cms/draft-commit.d.ts +70 -0
  137. package/dist/components/cms/draft-commit.js +82 -0
  138. package/dist/components/cms/draft-commit.js.map +1 -0
  139. package/dist/components/cms/duplicate-document.d.ts +11 -13
  140. package/dist/components/cms/duplicate-document.js +26 -34
  141. package/dist/components/cms/duplicate-document.js.map +1 -1
  142. package/dist/components/cms/field-anchors.d.ts +7 -4
  143. package/dist/components/cms/field-anchors.js +9 -7
  144. package/dist/components/cms/field-anchors.js.map +1 -1
  145. package/dist/components/cms/field-collaboration.d.ts +47 -0
  146. package/dist/components/cms/field-collaboration.js +87 -0
  147. package/dist/components/cms/field-collaboration.js.map +1 -0
  148. package/dist/components/cms/field-control-lib.d.ts +22 -1
  149. package/dist/components/cms/field-control-lib.js +33 -4
  150. package/dist/components/cms/field-control-lib.js.map +1 -1
  151. package/dist/components/cms/field-control.js +17 -14
  152. package/dist/components/cms/field-control.js.map +1 -1
  153. package/dist/components/cms/gate-validation.d.ts +21 -30
  154. package/dist/components/cms/gate-validation.js +11 -22
  155. package/dist/components/cms/gate-validation.js.map +1 -1
  156. package/dist/components/cms/object-array-field.js +4 -4
  157. package/dist/components/cms/object-array-field.js.map +1 -1
  158. package/dist/components/cms/prop-fields.d.ts +7 -2
  159. package/dist/components/cms/prop-fields.js +13 -6
  160. package/dist/components/cms/prop-fields.js.map +1 -1
  161. package/dist/components/cms/reference-confirm.js +10 -4
  162. package/dist/components/cms/reference-confirm.js.map +1 -1
  163. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  164. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  165. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  166. package/dist/components/cms/rich-text/insert.js +25 -17
  167. package/dist/components/cms/rich-text/insert.js.map +1 -1
  168. package/dist/components/cms/use-document-draft.d.ts +96 -0
  169. package/dist/components/cms/use-document-draft.js +283 -0
  170. package/dist/components/cms/use-document-draft.js.map +1 -0
  171. package/dist/components/cms/use-document-versions.js +2 -2
  172. package/dist/components/cms/use-document-versions.js.map +1 -1
  173. package/dist/components/cms/use-live-url.d.ts +6 -12
  174. package/dist/components/cms/use-live-url.js +16 -15
  175. package/dist/components/cms/use-live-url.js.map +1 -1
  176. package/dist/components/cms/use-publish-controller.d.ts +13 -18
  177. package/dist/components/cms/use-publish-controller.js +24 -41
  178. package/dist/components/cms/use-publish-controller.js.map +1 -1
  179. package/dist/components/cms/use-published-perspective.d.ts +2 -3
  180. package/dist/components/cms/use-published-perspective.js +4 -3
  181. package/dist/components/cms/use-published-perspective.js.map +1 -1
  182. package/dist/components/cms/versions-pane.js +8 -1
  183. package/dist/components/cms/versions-pane.js.map +1 -1
  184. package/dist/components/collection-view/cms-collection-view-source.js +24 -7
  185. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  186. package/dist/components/components/transfer-dialog.js +28 -23
  187. package/dist/components/components/transfer-dialog.js.map +1 -1
  188. package/dist/components/components/transfer.d.ts +31 -21
  189. package/dist/components/components/transfer.js +49 -35
  190. package/dist/components/components/transfer.js.map +1 -1
  191. package/dist/components/project-list.js.map +1 -1
  192. package/dist/components/project-session.d.ts +6 -3
  193. package/dist/components/project-session.js +14 -8
  194. package/dist/components/project-session.js.map +1 -1
  195. package/dist/components/project-switcher.js.map +1 -1
  196. package/dist/components/rail-view-source.js +2 -1
  197. package/dist/components/rail-view-source.js.map +1 -1
  198. package/dist/components/section-nav.js +1 -1
  199. package/dist/components/section-nav.js.map +1 -1
  200. package/dist/lib/ai/apply-content.js +56 -116
  201. package/dist/lib/ai/apply-content.js.map +1 -1
  202. package/dist/lib/ai/chat-context.d.ts +8 -0
  203. package/dist/lib/ai/chat-context.js +37 -6
  204. package/dist/lib/ai/chat-context.js.map +1 -1
  205. package/dist/lib/ai/chat-state.d.ts +14 -1
  206. package/dist/lib/ai/chat-state.js.map +1 -1
  207. package/dist/lib/ai/constraints.d.ts +20 -5
  208. package/dist/lib/ai/constraints.js +125 -33
  209. package/dist/lib/ai/constraints.js.map +1 -1
  210. package/dist/lib/ai/fake-provider.d.ts +16 -3
  211. package/dist/lib/ai/fake-provider.js +22 -6
  212. package/dist/lib/ai/fake-provider.js.map +1 -1
  213. package/dist/lib/ai/index.d.ts +2 -0
  214. package/dist/lib/ai/index.js +2 -0
  215. package/dist/lib/ai/index.js.map +1 -1
  216. package/dist/lib/ai/leash.d.ts +34 -0
  217. package/dist/lib/ai/leash.js +28 -0
  218. package/dist/lib/ai/leash.js.map +1 -0
  219. package/dist/lib/ai/provenance.d.ts +1 -1
  220. package/dist/lib/ai/provenance.js.map +1 -1
  221. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  222. package/dist/lib/ai/rich-text-input.js +2 -1
  223. package/dist/lib/ai/rich-text-input.js.map +1 -1
  224. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  225. package/dist/lib/ai/rich-text-translate.js +28 -10
  226. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  227. package/dist/lib/ai/shape.d.ts +13 -13
  228. package/dist/lib/ai/shape.js +69 -93
  229. package/dist/lib/ai/shape.js.map +1 -1
  230. package/dist/lib/ai/tools.d.ts +36 -41
  231. package/dist/lib/ai/tools.js +85 -87
  232. package/dist/lib/ai/tools.js.map +1 -1
  233. package/dist/lib/ai/translate-document.d.ts +39 -5
  234. package/dist/lib/ai/translate-document.js +160 -51
  235. package/dist/lib/ai/translate-document.js.map +1 -1
  236. package/dist/lib/ai/types.d.ts +34 -23
  237. package/dist/lib/ai/types.js.map +1 -1
  238. package/dist/lib/ai/update-fields.d.ts +21 -12
  239. package/dist/lib/ai/update-fields.js +118 -177
  240. package/dist/lib/ai/update-fields.js.map +1 -1
  241. package/dist/lib/ai/writer-form.d.ts +116 -0
  242. package/dist/lib/ai/writer-form.js +429 -0
  243. package/dist/lib/ai/writer-form.js.map +1 -0
  244. package/dist/lib/ai-current-document.d.ts +11 -11
  245. package/dist/lib/ai-current-document.js +4 -4
  246. package/dist/lib/ai-current-document.js.map +1 -1
  247. package/dist/lib/assets/upload.d.ts +31 -2
  248. package/dist/lib/assets/upload.js +61 -6
  249. package/dist/lib/assets/upload.js.map +1 -1
  250. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  251. package/dist/lib/clipboard/os-clipboard.js +8 -4
  252. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  253. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  254. package/dist/lib/clipboard/paste-target.js +8 -7
  255. package/dist/lib/clipboard/paste-target.js.map +1 -1
  256. package/dist/lib/cms/field-patch.d.ts +15 -0
  257. package/dist/lib/cms/field-patch.js +25 -0
  258. package/dist/lib/cms/field-patch.js.map +1 -0
  259. package/dist/lib/cms/published-documents.d.ts +1 -1
  260. package/dist/lib/cms/published-documents.js +1 -1
  261. package/dist/lib/cms/published-documents.js.map +1 -1
  262. package/dist/lib/cms-views/view-model.d.ts +3 -3
  263. package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
  264. package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
  265. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
  266. package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
  267. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  268. package/dist/lib/data/collection-content.d.ts +4 -13
  269. package/dist/lib/data/collection-content.js +11 -5
  270. package/dist/lib/data/collection-content.js.map +1 -1
  271. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  272. package/dist/lib/dnd/drop-rules.js +53 -59
  273. package/dist/lib/dnd/drop-rules.js.map +1 -1
  274. package/dist/lib/engine-version.d.ts +1 -1
  275. package/dist/lib/engine-version.js +1 -1
  276. package/dist/lib/engine-version.js.map +1 -1
  277. package/dist/lib/i18n/messages.de.js +72 -15
  278. package/dist/lib/i18n/messages.de.js.map +1 -1
  279. package/dist/lib/i18n/messages.en.d.ts +61 -12
  280. package/dist/lib/i18n/messages.en.js +76 -16
  281. package/dist/lib/i18n/messages.en.js.map +1 -1
  282. package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
  283. package/dist/lib/operations/__fixtures__/write-document.js +16 -0
  284. package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
  285. package/dist/lib/operations/cms-persistence.d.ts +124 -31
  286. package/dist/lib/operations/cms-persistence.js +447 -68
  287. package/dist/lib/operations/cms-persistence.js.map +1 -1
  288. package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
  289. package/dist/lib/operations/config-manifest-lookup.js +56 -0
  290. package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
  291. package/dist/lib/operations/project-runtime-config.d.ts +39 -0
  292. package/dist/lib/operations/project-runtime-config.js +54 -0
  293. package/dist/lib/operations/project-runtime-config.js.map +1 -0
  294. package/dist/lib/operations/publish-with-revision.d.ts +64 -0
  295. package/dist/lib/operations/publish-with-revision.js +66 -0
  296. package/dist/lib/operations/publish-with-revision.js.map +1 -0
  297. package/dist/lib/operations/runtime-validator.d.ts +32 -0
  298. package/dist/lib/operations/runtime-validator.js +18 -0
  299. package/dist/lib/operations/runtime-validator.js.map +1 -0
  300. package/dist/lib/operations/session-hydration.d.ts +31 -14
  301. package/dist/lib/operations/session-hydration.js +12 -16
  302. package/dist/lib/operations/session-hydration.js.map +1 -1
  303. package/dist/lib/operations/studio-operations.d.ts +59 -2
  304. package/dist/lib/operations/studio-operations.js +76 -14
  305. package/dist/lib/operations/studio-operations.js.map +1 -1
  306. package/dist/lib/persistence/convex-adapter.d.ts +13 -5
  307. package/dist/lib/persistence/convex-adapter.js +189 -74
  308. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  309. package/dist/lib/persistence/convex-client.d.ts +65 -5
  310. package/dist/lib/persistence/convex-client.js +106 -3
  311. package/dist/lib/persistence/convex-client.js.map +1 -1
  312. package/dist/lib/persistence/index.js +17 -18
  313. package/dist/lib/persistence/index.js.map +1 -1
  314. package/dist/lib/persona/labels.js +4 -1
  315. package/dist/lib/persona/labels.js.map +1 -1
  316. package/dist/lib/query/operations-query.d.ts +1 -1
  317. package/dist/lib/query/operations-query.js +1 -1
  318. package/dist/lib/query/operations-query.js.map +1 -1
  319. package/dist/lib/references/impact.d.ts +21 -15
  320. package/dist/lib/references/impact.js +13 -26
  321. package/dist/lib/references/impact.js.map +1 -1
  322. package/dist/lib/references/references-store.d.ts +9 -0
  323. package/dist/lib/references/references-store.js +24 -4
  324. package/dist/lib/references/references-store.js.map +1 -1
  325. package/dist/lib/routes/route-issues.d.ts +6 -6
  326. package/dist/lib/routes/route-issues.js +26 -80
  327. package/dist/lib/routes/route-issues.js.map +1 -1
  328. package/dist/lib/workspace/config-issues.js +2 -1
  329. package/dist/lib/workspace/config-issues.js.map +1 -1
  330. package/dist/lib/workspace/index.d.ts +31 -1
  331. package/dist/lib/workspace/index.js +64 -17
  332. package/dist/lib/workspace/index.js.map +1 -1
  333. package/dist/lib/workspace/switcher-source.d.ts +3 -3
  334. package/dist/lib/workspace/switcher-source.js.map +1 -1
  335. package/dist/lib/workspace/workspace-source.d.ts +2 -2
  336. package/dist/lib/workspace/workspace-source.js +2 -1
  337. package/dist/lib/workspace/workspace-source.js.map +1 -1
  338. package/package.json +3 -3
  339. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  340. package/dist/components/cms/cms-composition-validators.js +0 -133
  341. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  342. package/dist/components/cms/publish-clean.d.ts +0 -28
  343. package/dist/components/cms/publish-clean.js +0 -38
  344. package/dist/components/cms/publish-clean.js.map +0 -1
  345. package/dist/components/cms/use-save-mode.d.ts +0 -35
  346. package/dist/components/cms/use-save-mode.js +0 -54
  347. package/dist/components/cms/use-save-mode.js.map +0 -1
  348. package/dist/lib/assets/asset-placements.d.ts +0 -28
  349. package/dist/lib/assets/asset-placements.js +0 -83
  350. package/dist/lib/assets/asset-placements.js.map +0 -1
  351. package/dist/lib/cms/document-gate.d.ts +0 -47
  352. package/dist/lib/cms/document-gate.js +0 -61
  353. package/dist/lib/cms/document-gate.js.map +0 -1
@@ -0,0 +1,25 @@
1
+ import { fieldPath, isFieldLocalized, readValueAt } from '@elytracms/core/cms-core';
2
+ /**
3
+ * EC-595 A2 — the patch a FIELD write emits at its source.
4
+ *
5
+ * Every writer that writes one field's value through core's `writeField` (the form, a
6
+ * table cell, bulk set-field, an AI field change, a translation, fill-from-base) knows
7
+ * the position it wrote: the field, and — for a localized field — the locale bucket
8
+ * (EC-595 binding point 3: "localized fields patch per bucket"). This reads the value
9
+ * the write left at that position in `written` (the document AFTER `writeField`, so the
10
+ * value is the canonical one, EC-586) and says so as one `set` — or `unset` when the
11
+ * write cleared the key (writing `undefined`, an emptied list).
12
+ */
13
+ export function fieldWritePatch(collection, written, fieldName, locale) {
14
+ const path = fieldPositionPath(collection, fieldName, locale);
15
+ const value = readValueAt(written, path);
16
+ return value === undefined ? { op: 'unset', path } : { op: 'set', path, value };
17
+ }
18
+ /** The position a field write addresses: `[field]`, or `[field, locale]` for a localized field. */
19
+ export function fieldPositionPath(collection, fieldName, locale) {
20
+ return fieldPath(fieldName, isFieldLocalized(collection, fieldName) ? locale : undefined);
21
+ }
22
+ // EC-585: the field-position patches of the publish-time hidden strip (the one writer
23
+ // that hands back a whole document) are computed inside `documents.publish` now, where
24
+ // the strip runs for every publish path (`core/operations/documents.ts`, `stripPatches`).
25
+ //# sourceMappingURL=field-patch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-patch.js","sourceRoot":"","sources":["../../../src/lib/cms/field-patch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAGnF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAC7B,UAAyB,EACzB,OAAoB,EACpB,SAAiB,EACjB,MAA0B;IAE1B,MAAM,IAAI,GAAG,iBAAiB,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,CAAA;IAC7D,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IACxC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACjF,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,iBAAiB,CAC/B,UAAyB,EACzB,SAAiB,EACjB,MAA0B;IAE1B,OAAO,SAAS,CAAC,SAAS,EAAE,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAC3F,CAAC;AAED,sFAAsF;AACtF,uFAAuF;AACvF,0FAA0F","sourcesContent":["import { fieldPath, isFieldLocalized, readValueAt } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentPatch, Locale, ValuePath } from '@elytracms/core/cms-core'\n\n/**\n * EC-595 A2 — the patch a FIELD write emits at its source.\n *\n * Every writer that writes one field's value through core's `writeField` (the form, a\n * table cell, bulk set-field, an AI field change, a translation, fill-from-base) knows\n * the position it wrote: the field, and — for a localized field — the locale bucket\n * (EC-595 binding point 3: \"localized fields patch per bucket\"). This reads the value\n * the write left at that position in `written` (the document AFTER `writeField`, so the\n * value is the canonical one, EC-586) and says so as one `set` — or `unset` when the\n * write cleared the key (writing `undefined`, an emptied list).\n */\nexport function fieldWritePatch(\n collection: CollectionDef,\n written: CmsDocument,\n fieldName: string,\n locale: Locale | undefined,\n): DocumentPatch {\n const path = fieldPositionPath(collection, fieldName, locale)\n const value = readValueAt(written, path)\n return value === undefined ? { op: 'unset', path } : { op: 'set', path, value }\n}\n\n/** The position a field write addresses: `[field]`, or `[field, locale]` for a localized field. */\nexport function fieldPositionPath(\n collection: CollectionDef,\n fieldName: string,\n locale: Locale | undefined,\n): ValuePath {\n return fieldPath(fieldName, isFieldLocalized(collection, fieldName) ? locale : undefined)\n}\n\n// EC-585: the field-position patches of the publish-time hidden strip (the one writer\n// that hands back a whole document) are computed inside `documents.publish` now, where\n// the strip runs for every publish path (`core/operations/documents.ts`, `stripPatches`).\n"]}
@@ -25,7 +25,7 @@ export interface PublishedDocuments {
25
25
  * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a
26
26
  * changeset revert restores a prior live version — and was missing, so a revert left
27
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`),
28
+ * moves nothing: it writes the DRAFT from a recorded version (`documents.replace`),
29
29
  * never the pin. The same answer the Convex trigger gives server-side: only a pin
30
30
  * move or the delete of a pinned row changes published output.
31
31
  */
@@ -5,7 +5,7 @@ import { useOperationQuery } from '@elytracms/studio/lib/query';
5
5
  * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a
6
6
  * changeset revert restores a prior live version — and was missing, so a revert left
7
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`),
8
+ * moves nothing: it writes the DRAFT from a recorded version (`documents.replace`),
9
9
  * never the pin. The same answer the Convex trigger gives server-side: only a pin
10
10
  * move or the delete of a pinned row changes published output.
11
11
  */
@@ -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;;;;;;;;;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
+ {"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 (`documents.replace`),\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"]}
@@ -38,8 +38,8 @@ export type FilterClause = z.infer<typeof filterClauseSchema>;
38
38
  export declare const viewSortSchema: z.ZodObject<{
39
39
  field: z.ZodString;
40
40
  dir: z.ZodEnum<{
41
- desc: "desc";
42
41
  asc: "asc";
42
+ desc: "desc";
43
43
  }>;
44
44
  }, z.core.$strip>;
45
45
  export type ViewSort = z.infer<typeof viewSortSchema>;
@@ -63,8 +63,8 @@ export declare const tableViewStateSchema: z.ZodObject<{
63
63
  sort: z.ZodNullable<z.ZodObject<{
64
64
  field: z.ZodString;
65
65
  dir: z.ZodEnum<{
66
- desc: "desc";
67
66
  asc: "asc";
67
+ desc: "desc";
68
68
  }>;
69
69
  }, z.core.$strip>>;
70
70
  columns: z.ZodArray<z.ZodString>;
@@ -305,8 +305,8 @@ export declare const configViewSchema: z.ZodObject<{
305
305
  sort: z.ZodOptional<z.ZodNullable<z.ZodObject<{
306
306
  field: z.ZodString;
307
307
  dir: z.ZodEnum<{
308
- desc: "desc";
309
308
  asc: "asc";
309
+ desc: "desc";
310
310
  }>;
311
311
  }, z.core.$strip>>>;
312
312
  columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -0,0 +1,6 @@
1
+ import type { DocumentWrites } from '../collection-view-source.js';
2
+ /**
3
+ * A no-op revisioned-document write model (EC-595 A2) for surface tests that render a
4
+ * CMS-shaped source with `renderToStaticMarkup` and never write.
5
+ */
6
+ export declare function noopDocumentWrites(): DocumentWrites;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A no-op revisioned-document write model (EC-595 A2) for surface tests that render a
3
+ * CMS-shaped source with `renderToStaticMarkup` and never write.
4
+ */
5
+ export function noopDocumentWrites() {
6
+ return {
7
+ kind: 'document',
8
+ commit: async () => undefined,
9
+ revisionOf: () => undefined,
10
+ insert: () => { },
11
+ newId: () => 'new-id',
12
+ conflicts: () => [],
13
+ resolveConflict: () => { },
14
+ adoptSnapshot: () => { },
15
+ flush: async () => { },
16
+ replace: () => { },
17
+ subscribe: () => () => { },
18
+ reportPresence: () => { },
19
+ clearPresence: () => { },
20
+ presenceSessionId: 'test-session',
21
+ };
22
+ }
23
+ //# sourceMappingURL=document-writes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-writes.js","sourceRoot":"","sources":["../../../../src/lib/collection-view/__fixtures__/document-writes.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS;QAC7B,UAAU,EAAE,GAAG,EAAE,CAAC,SAAS;QAC3B,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;QAChB,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ;QACrB,SAAS,EAAE,GAAG,EAAE,CAAC,EAAE;QACnB,eAAe,EAAE,GAAG,EAAE,GAAE,CAAC;QACzB,aAAa,EAAE,GAAG,EAAE,GAAE,CAAC;QACvB,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;QACrB,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC;QACjB,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAE,CAAC;QACzB,cAAc,EAAE,GAAG,EAAE,GAAE,CAAC;QACxB,aAAa,EAAE,GAAG,EAAE,GAAE,CAAC;QACvB,iBAAiB,EAAE,cAAc;KAClC,CAAA;AACH,CAAC","sourcesContent":["import type { DocumentWrites } from '../collection-view-source'\n\n/**\n * A no-op revisioned-document write model (EC-595 A2) for surface tests that render a\n * CMS-shaped source with `renderToStaticMarkup` and never write.\n */\nexport function noopDocumentWrites(): DocumentWrites {\n return {\n kind: 'document',\n commit: async () => undefined,\n revisionOf: () => undefined,\n insert: () => {},\n newId: () => 'new-id',\n conflicts: () => [],\n resolveConflict: () => {},\n adoptSnapshot: () => {},\n flush: async () => {},\n replace: () => {},\n subscribe: () => () => {},\n reportPresence: () => {},\n clearPresence: () => {},\n presenceSessionId: 'test-session',\n }\n}\n"]}
@@ -1,7 +1,8 @@
1
- import type { CmsDocument, CollectionDef, DocumentRef, DocumentUrl, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope } from '@elytracms/core/cms-core';
1
+ import type { CmsDocument, CollectionDef, DocumentPatch, DocumentRef, ValuePath, DocumentUrl, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope } from '@elytracms/core/cms-core';
2
2
  import type { CmsValidationIssue } from '@elytracms/core/cms-core';
3
3
  import type { OperationError } from '@elytracms/core/operations';
4
- import type { ReferenceTarget } from '@elytracms/core/persistence';
4
+ import type { CmsDocumentRecord, PresenceEntry, ReferenceTarget } from '@elytracms/core/persistence';
5
+ import type { CommitOutcome, DocumentConflict } from '../operations/cms-persistence.js';
5
6
  import type { TableViewState } from '../cms-views/view-model.js';
6
7
  import type { CellCommitInput, CellCommitOutcome } from '../../components/cms/cell-edit.js';
7
8
  import type { BulkAction, BulkReport, BulkSetField } from '../../components/cms/bulk-actions.js';
@@ -96,7 +97,7 @@ export interface CollectionViewSource {
96
97
  * when there is no asset backend (a source without assets degrades to a label
97
98
  * via {@link titleOf}, matching today's "no AssetsProvider" behaviour).
98
99
  */
99
- resolveAsset?(id: string): ResolvedAsset | undefined;
100
+ resolveAsset?(id: string): AssetDisplay | undefined;
100
101
  /**
101
102
  * How this rail's records are addressed in the EC-155 reference index — the
102
103
  * target shape the delete/unpublish confirmation's inbound pre-flight queries
@@ -135,7 +136,15 @@ export interface CollectionViewSource {
135
136
  viewState(): TableViewState;
136
137
  setViewState(update: TableViewState | ((prev: TableViewState) => TableViewState)): void;
137
138
  create(): DocumentRef;
138
- save(doc: CmsDocument): void;
139
+ /**
140
+ * EC-595 A2 — HOW this source's records are written: exactly one model per source,
141
+ * so a surface can never write a revisioned document the record way or vice versa.
142
+ * `record` — the explicit-save rails (users, redirects, assets, the account): the
143
+ * editor's Save button hands the whole record over. `document` — the CMS: every
144
+ * writer emits patches at its source, committed on quiescence and merged per path
145
+ * at the store; the editor shows per-field conflicts and follows the subscription.
146
+ */
147
+ readonly writes: CollectionWrites;
139
148
  remove(ref: DocumentRef): void;
140
149
  /**
141
150
  * Bulk publish/unpublish/delete/set-field (only when `capabilities.bulk`): one
@@ -171,6 +180,56 @@ export interface CollectionViewSource {
171
180
  */
172
181
  readonly imports?: ImportCapability;
173
182
  }
183
+ /** EC-595 A2 — the write model of a source (see {@link CollectionViewSource.writes}). */
184
+ export type CollectionWrites = RecordWrites | DocumentWrites;
185
+ /** An explicit-save rail: the editor's Save button writes the whole record. */
186
+ export interface RecordWrites {
187
+ readonly kind: 'record';
188
+ save(doc: CmsDocument): void;
189
+ }
190
+ /**
191
+ * The revisioned-document write model (EC-595, ratified 2026-10-24): patches at the
192
+ * source, the persister's queue, per-field conflicts, the document subscription.
193
+ */
194
+ export interface DocumentWrites {
195
+ readonly kind: 'document';
196
+ /**
197
+ * Commit the patches a writer made; resolves once acknowledged (`undefined` in a
198
+ * fixture session). `bases` — per patch, the revision the edit was started at.
199
+ */
200
+ commit(ref: DocumentRef, patches: readonly DocumentPatch[], bases?: readonly (number | undefined)[]): Promise<CommitOutcome | undefined>;
201
+ /** The revision the session's view of a document is at. */
202
+ revisionOf(ref: DocumentRef): number | undefined;
203
+ /** Create a document from a whole value (insert-if-absent): duplicate. */
204
+ insert(doc: CmsDocument): void;
205
+ /** A fresh document id from the session's injected factory (EC-578). */
206
+ newId(): string;
207
+ /** The open per-field conflicts on a document. */
208
+ conflicts(ref: DocumentRef): readonly DocumentConflict[];
209
+ resolveConflict(ref: DocumentRef, conflictId: string, choice: 'theirs' | 'mine'): void;
210
+ /** Adopt a row the document subscription pushed (local edits win on their paths until flushed). */
211
+ adoptSnapshot(ref: DocumentRef, record: CmsDocumentRecord | undefined): void;
212
+ /** The forced flush's persistence half: await every queued write of the document. */
213
+ flush(ref: DocumentRef): Promise<void>;
214
+ /**
215
+ * Write the whole document at the revision the session holds (a document-level key
216
+ * the value-position grammar has no position for — the slot `alias`). A move since
217
+ * is refused, never overwritten.
218
+ */
219
+ replace(doc: CmsDocument): void;
220
+ /**
221
+ * EC-595 point 7 — follow ONE document while it is open: every pushed row is adopted
222
+ * under the local edits (the editor rebases), and the presence on it is delivered to
223
+ * `onPresence`. Returns the unsubscribe function.
224
+ */
225
+ subscribe(ref: DocumentRef, onPresence: (presence: readonly PresenceEntry[]) => void): () => void;
226
+ /** Thin presence (Stage A): this session is on the document, in `path`'s field (or on no field). */
227
+ reportPresence(ref: DocumentRef, path: ValuePath | undefined): void;
228
+ /** This session left the document. */
229
+ clearPresence(ref: DocumentRef): void;
230
+ /** This session's presence id (one per tab), so the editor can leave itself out. */
231
+ readonly presenceSessionId: string;
232
+ }
174
233
  /**
175
234
  * The capability matrix. Each flag gates a shell region AND the presence of its
176
235
  * method group. The **users rail** (EC-240) sets `publish:false`, `versions:false`
@@ -220,8 +279,12 @@ export type FieldAccess = 'edit' | 'read' | 'hidden';
220
279
  * principal edit / see this field?", so the default-open fallback lives in one spot.
221
280
  */
222
281
  export declare function resolveFieldAccess(source: CollectionViewSource, field: FieldDef, record?: CmsDocument): FieldAccess;
223
- /** Asset display metadata a cell/field renders (thumbnail or labelled badge). */
224
- export interface ResolvedAsset {
282
+ /**
283
+ * Asset display metadata a cell/field renders (thumbnail or labelled badge). EC-587:
284
+ * named apart from core's `ResolvedAsset` (the delivery shape) — this is the studio's
285
+ * label/thumbnail slice; its `alt` is the one reader's (`assetAlt`).
286
+ */
287
+ export interface AssetDisplay {
225
288
  readonly url?: string;
226
289
  readonly contentType?: string;
227
290
  readonly title?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"collection-view-source.js","sourceRoot":"","sources":["../../../src/lib/collection-view/collection-view-source.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAkOvD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA4B,EAC5B,KAAe,EACf,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AACjG,CAAC;AAiDD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAA4B;IACjE,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE;QAClC,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,QAAQ,EAAE,MAAM,CAAC,YAAY,CAAC,OAAO;KACtC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n DocumentUrl,\n FieldDef,\n Locale,\n LocaleConfig,\n LocalizedString,\n RestoreScope,\n} from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue } from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport type { ReferenceTarget } from '@elytracms/core/persistence'\nimport type { TableViewState } from '../cms-views/view-model'\nimport { pseudoColumns } from '../cms-views/view-model'\n// The concrete inline-edit + bulk vocabulary (Stream A relocates it into `lib` as it settles).\nimport type { CellCommitInput, CellCommitOutcome } from '../../components/cms/cell-edit'\nimport type { BulkAction, BulkReport, BulkSetField } from '../../components/cms/bulk-actions'\nimport type { DocumentVersionsView, ImportReport } from '../../components/cms/cms-store'\n\n/**\n * `CollectionViewSource` — the studio's UNIVERSAL interaction seam (EC-240,\n * vision AD-19).\n *\n * The list surface (`CollectionTable`) and the edit-shell (`DetailEditor`) are\n * the studio's universal interaction surfaces. Historically they read the\n * concrete `CmsStore` through `useCms()`, so they only served `cmsDocuments`.\n * This interface is the data-agnostic seam they render over instead, so the SAME\n * surfaces drive the users rail (EC-240), redirects (EC-303), the Components app\n * edit-shell, and any future rail — no hand-rolled UI per rail.\n *\n * **Shape:** the source is scoped to ONE collection (it carries `schema` +\n * `collectionId`), and its methods mirror the subset of `CmsStore` the surfaces\n * actually use — dropping the `collectionId` argument (the source already knows\n * it). Keeping the names/signatures aligned makes the CMS adapter a\n * behaviour-preserving pass-through and the migration near-mechanical.\n *\n * **Referential stability:** the surfaces put `documents`, `isPublished`,\n * `localeConfig`, etc. in `useMemo` dependency arrays. An implementation MUST\n * return stable references across renders (delegate to the underlying store's\n * already-stable methods; never wrap in a fresh closure each render) or memos\n * bust and the inline-edit order snapshot / perf regress.\n *\n * **Capabilities:** flags gate whole shell regions AND method presence — a source\n * with `capabilities.versions === false` (the users rail) never mounts the\n * Versions pane, so no `listVersions` read fires on credential records. Optional\n * method groups are present iff the flag is set.\n */\nexport interface CollectionViewSource {\n /** The rail/collection this source serves. */\n readonly collectionId: string\n /** The schema lens: fields, form tabs/groups, `titleField`, display nouns. */\n readonly schema: CollectionDef\n /** Which regions/methods this source supports. */\n readonly capabilities: CollectionViewCapabilities\n\n /** The project scope (asset picker, published-pool queries). Absent in fixture sessions. */\n readonly projectId?: string\n /**\n * The collection id assets live in (for a cell/field's `titleOf` label fallback\n * when {@link resolveAsset} finds no record). Present only for sources that\n * reference assets — a rail without assets (users) omits it.\n */\n readonly assetCollectionId?: string\n /** The ambient locale lens (only meaningful when `capabilities.locales`). */\n readonly localeConfig: LocaleConfig\n readonly activeLocale: Locale\n\n // --- Reads -----------------------------------------------------------------\n /** The full row set (view mechanics filter/sort this client-side). */\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n /** A relation/asset target's display title (hides the string-vs-function `titleField`). */\n titleOf(ref: DocumentRef): string\n /**\n * List the candidate records of a relation field's declared `target` collection\n * — the pick-list for a relation picker (EC-072). References are first-class and\n * collection-based: the field's schema declares which collection(s) it targets,\n * and this resolves those rows for ANY target (cross-collection), not just this\n * source's own collection. The caller passes `field.target`; a multi-target field\n * (future) merges one call per declared target. An unknown/empty target yields `[]`.\n */\n referenceCandidates(collectionId: string): CmsDocument[]\n /**\n * The workspace schema a reference/asset can point at — the schema counterpart to\n * {@link referenceCandidates}. Needed for the cases that read a TARGET collection's\n * fields (an asset field editing the asset collection's own metadata; the reuse\n * capability correlating embed manifests against their target collection). A\n * reference is collection-based, so resolving one sometimes needs the target's\n * schema, not just its rows.\n */\n readonly collections: readonly CollectionDef[]\n isFieldReadOnly(field: FieldDef): boolean\n /**\n * Principal-based field access (EC-240, vision AD-18): `edit` (writable), `read`\n * (visible but read-only), or `hidden` (not rendered for this principal). This is\n * IDENTITY-driven and distinct from a field's content-conditional `hidden(values)`\n * — e.g. an editor's own `role` resolves to `read`, a viewer never SEES a\n * sensitive field (`hidden`). A single tri-state (never two independent flags) so\n * the nonsensical writable-but-invisible state can't be expressed. Optional:\n * absent ⇒ derived from {@link isFieldReadOnly} (`read`|`edit`), i.e. default-open\n * (permissions RESTRICT, never grant a baseline). Per-rail policy lives on the\n * source; the starter's policy flows into its construction (engine = mechanism).\n * Prefer {@link resolveFieldAccess} over calling this directly (it applies the\n * default-open fallback).\n */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Resolve an asset id to its display metadata for a cell/field, or `undefined`\n * when there is no asset backend (a source without assets degrades to a label\n * via {@link titleOf}, matching today's \"no AssetsProvider\" behaviour).\n */\n resolveAsset?(id: string): ResolvedAsset | undefined\n /**\n * How this rail's records are addressed in the EC-155 reference index — the\n * target shape the delete/unpublish confirmation's inbound pre-flight queries\n * (EC-433). Absent ⇒ the document shape (`{ type: 'document', id: 'collection:id' }`),\n * which every `cmsDocuments` collection uses. The assets rail supplies\n * `{ type: 'asset' }` so its list/bulk delete confirmation enumerates the SAME\n * inbound references its detail surface does — a document-shaped lookup would\n * report a referenced asset as \"not referenced\" (the EC-319 objection that\n * originally kept `remove` off the assets rail).\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n /** The last failed/denied mutation, surfaced by the shell (never a silent failure). */\n readonly lastError: OperationError | null\n clearLastError(): void\n /**\n * Land any queued/optimistic writes so a subsequent pre-flight READ sees them\n * (the reference index is maintained on write). A source with no async\n * persistence omits it — its reads are already authoritative.\n */\n flush?(): Promise<void>\n\n /**\n * The studio route splat for a record's detail surface (the row link + the\n * post-create navigation target), e.g. `studioPath.cmsDocument(collectionId, id)`.\n * Rail-owned so the list never hard-codes the CMS route. `undefined` ⇒ the rail\n * has no detail surface, so rows render as plain text (no navigation).\n */\n detailPath?(id: string): string\n /**\n * The studio route splat for this rail's LIST surface — the edit shell's\n * \"back to list\" breadcrumb crumb and its post-delete navigation target, e.g.\n * `studioPath.cmsCollection(collectionId)`. Rail-owned so the edit shell never\n * hard-codes the CMS route (the counterpart to {@link detailPath}). `undefined`\n * ⇒ the crumb is unlinked and delete falls back to the project index.\n */\n readonly listPath?: string\n\n // --- View state (list) -----------------------------------------------------\n viewState(): TableViewState\n setViewState(update: TableViewState | ((prev: TableViewState) => TableViewState)): void\n\n // --- Writes ----------------------------------------------------------------\n create(): DocumentRef\n save(doc: CmsDocument): void\n remove(ref: DocumentRef): void\n /**\n * Bulk publish/unpublish/delete/set-field (only when `capabilities.bulk`): one\n * command PER item, per-item outcomes enumerated in the report (partial failure\n * is never silent).\n */\n bulk?(ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>\n\n // --- Capability method groups (present iff the matching flag is set) --------\n /** `publish` — published-pool predicate + publish/unpublish. */\n readonly publishing?: PublishingCapability\n /**\n * Per-document timestamps (EC-445) — the data behind the `$updatedAt`\n * pseudo-column (and, once its data path lands, `$publishedAt`). Present iff the\n * source materializes them (the CMS source, sourced through session-hydration);\n * a rail that carries no timestamps omits it, so those pseudo-columns simply don't\n * apply (see {@link pseudoColumnsForSource}). The accessor shape mirrors\n * {@link PublishingCapability.isPublished} — a per-ref lookup, referentially stable.\n */\n readonly timestamps?: DocumentTimestampsCapability\n /** `versions` — version history + restore. */\n readonly versioning?: VersioningCapability\n /** `inlineEdit` — the list's single-command optimistic cell write. */\n readonly inlineEditing?: InlineEditCapability\n /**\n * Config-as-code named lenses (EC-246b) the header view selector switches\n * between. Optional: a source that declares no named views (a non-CMS rail\n * like users) omits it, and the selector renders nothing (single default).\n */\n readonly views?: NamedViewsCapability\n /**\n * `importExport` — JSON round-trip of the collection's records. CMS-specific;\n * a rail whose records are credential-bearing (users) omits it, so neither the\n * import dialog nor the export action mounts (identities aren't dumped to JSON).\n */\n readonly imports?: ImportCapability\n}\n\n/**\n * The capability matrix. Each flag gates a shell region AND the presence of its\n * method group. The **users rail** (EC-240) sets `publish:false`, `versions:false`\n * (credential caution), `create:false` (identity minted by sign-up), and no\n * `liveUrl`/`hierarchy`.\n */\nexport interface CollectionViewCapabilities {\n readonly publish: boolean\n readonly versions: boolean\n readonly locales: boolean\n readonly inlineEdit: boolean\n readonly create: boolean\n /**\n * Duplicate-document action (EC-442, amended same day). Deliberately its own\n * flag, never derived from `create`: the owner call (2026-07-17) is CMS\n * collections ONLY — no primitive rail offers Duplicate for now (identities\n * and uploads aren't copyable content; redirects deferred until requested as\n * a product feature).\n */\n readonly duplicate: boolean\n readonly remove: boolean\n readonly bulk: boolean\n /** The derived live-URL column (published pool × deployment routes). */\n readonly liveUrl: boolean\n /** Parent-self-relation tree mode (hierarchical collections only). */\n readonly hierarchy: boolean\n /** JSON import/export of the collection's records (off for credential rails). */\n readonly importExport: boolean\n /**\n * The rail is administration-scoped (mirrors `ADMIN_ONLY_OPERATIONS`). The\n * nav-permission layer reads this to gate the rail item. Absent ⇒ not admin-only.\n */\n readonly adminOnly?: boolean\n}\n\n/**\n * Principal-based field access (EC-240, AD-18). One tri-state drives BOTH visibility\n * and writability: `edit` (writable), `read` (visible, read-only), `hidden` (not\n * rendered). Default `edit` (default-open). Distinct from content-`hidden(values)`;\n * they compose — a field shows iff *not content-hidden AND access ≠ hidden*, and is\n * editable iff *access = edit AND not otherwise read-only* (published view, `form.readOnly`).\n */\nexport type FieldAccess = 'edit' | 'read' | 'hidden'\n\n/**\n * Resolve a field's effective principal access for a source — its {@link\n * CollectionViewSource.fieldAccess} policy when present, else derived from the\n * boolean `isFieldReadOnly` (default-open). The one place the shell asks \"may this\n * principal edit / see this field?\", so the default-open fallback lives in one spot.\n */\nexport function resolveFieldAccess(\n source: CollectionViewSource,\n field: FieldDef,\n record?: CmsDocument,\n): FieldAccess {\n return source.fieldAccess?.(field, record) ?? (source.isFieldReadOnly(field) ? 'read' : 'edit')\n}\n\n/** Asset display metadata a cell/field renders (thumbnail or labelled badge). */\nexport interface ResolvedAsset {\n readonly url?: string\n readonly contentType?: string\n readonly title?: string\n readonly filename?: string\n readonly alt?: string\n}\n\n/** `publish` capability. */\nexport interface PublishingCapability {\n /** Published-ness from the version pointer (NOT a row field). */\n isPublished(ref: DocumentRef): boolean\n /**\n * Does this PUBLISHED document's working draft carry unpublished changes\n * (EC-449)? The cheap per-row signal behind the amber \"changed\" `$state` dot —\n * sourced from the MATERIALIZED `hasUnpublishedChanges` record flag (computed\n * once at the write chokepoint), never a per-row snapshot compare. Optional: a\n * source without the signal omits it, and the list derives only\n * published/draft (exactly the pre-EC-449 rendering). Same referentially-stable\n * per-ref accessor shape as {@link isPublished}.\n */\n hasUnpublishedChanges?(ref: DocumentRef): boolean\n publish(ref: DocumentRef): Promise<void>\n unpublish(ref: DocumentRef): Promise<void>\n /**\n * Live URLs for the routable documents, keyed by id — derived from the\n * PUBLISHED pool (never draft rows) so an unpublished slug/parent edit can't\n * leak into a live link (EC-205/EC-216). `null` when the collection is not\n * routable → the live-URL column is hidden. Only present when `capabilities.liveUrl`.\n */\n liveUrls?(): Map<string, DocumentUrl> | null\n}\n\n/**\n * `timestamps` capability (EC-445/EC-447): per-document record timestamps as ISO\n * strings, or `undefined` when a document carries none. The `$updatedAt`/`$publishedAt`\n * pseudo-columns read + sort by these. `updatedAt` is the last-write instant;\n * `publishedAt` (EC-447) is the FIRST-publish instant — set-once on publish, cleared on\n * unpublish, so it is `undefined` for a draft / never-published / currently-unpublished\n * document (the column shows EmDash).\n */\nexport interface DocumentTimestampsCapability {\n updatedAt(ref: DocumentRef): string | undefined\n publishedAt(ref: DocumentRef): string | undefined\n}\n\n/**\n * The reserved pseudo-columns applicable to a source's list (EC-445), derived from\n * its capabilities: `$updatedAt` iff it materializes timestamps, `$state` iff it has a\n * publish lifecycle, `$url` iff it resolves delivery URLs. The one place the list\n * surface + the Display picker agree on which pseudo-columns exist for a source, so\n * the column universe never drifts between where columns render and where they toggle.\n */\nexport function pseudoColumnsForSource(source: CollectionViewSource): string[] {\n return pseudoColumns(source.schema, {\n timestamps: source.timestamps !== undefined,\n publishing: source.publishing !== undefined,\n routable: source.capabilities.liveUrl,\n })\n}\n\n/** `versions` capability. */\nexport interface VersioningCapability {\n restore(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>\n /**\n * The document's recorded version history + live pin. Folds the flush-before-read\n * ordering INTO the loader (the adapter's concern), so every fetch lists an\n * authoritative history regardless of which surface triggered the change (EC-224).\n */\n load(ref: DocumentRef): Promise<DocumentVersionsView>\n}\n\n/** `inlineEdit` capability: the list's single-command optimistic cell write (EC-168). */\nexport interface InlineEditCapability {\n commitField(input: CellCommitInput): Promise<CellCommitOutcome>\n}\n\n/** A named lens the header selector offers — id + display name only (the header never needs its state). */\nexport interface NamedView {\n readonly id: string\n /** Display name; a {@link LocalizedString} (EC-343), resolved at the render site like `form.label`. */\n readonly name: LocalizedString\n}\n\n/** `importExport` capability: validate + fold external JSON records into the collection (EC-079). */\nexport interface ImportCapability {\n importDocuments(items: unknown[]): ImportReport\n}\n\n/**\n * `views` capability: config-as-code named lenses (EC-246b). The header selector\n * only *picks* a lens — it never creates/edits/deletes one (shared views are code).\n */\nexport interface NamedViewsCapability {\n /** The built-in default + config-declared named views. */\n list(): readonly NamedView[]\n /** The currently-selected view id. */\n activeId(): string\n /** Switch views (resets the working table state to the view's baseline). */\n setActive(id: string): void\n}\n\nexport type { CellCommitInput, CellCommitOutcome, ImportReport }\n"]}
1
+ {"version":3,"file":"collection-view-source.js","sourceRoot":"","sources":["../../../src/lib/collection-view/collection-view-source.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAmSvD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA4B,EAC5B,KAAe,EACf,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AACjG,CAAC;AAqDD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAA4B;IACjE,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE;QAClC,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,QAAQ,EAAE,MAAM,CAAC,YAAY,CAAC,OAAO;KACtC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type {\n CmsDocument,\n CollectionDef,\n DocumentPatch,\n DocumentRef,\n ValuePath,\n DocumentUrl,\n FieldDef,\n Locale,\n LocaleConfig,\n LocalizedString,\n RestoreScope,\n} from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue } from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport type { CmsDocumentRecord, PresenceEntry, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { CommitOutcome, DocumentConflict } from '../operations/cms-persistence'\nimport type { TableViewState } from '../cms-views/view-model'\nimport { pseudoColumns } from '../cms-views/view-model'\n// The concrete inline-edit + bulk vocabulary (Stream A relocates it into `lib` as it settles).\nimport type { CellCommitInput, CellCommitOutcome } from '../../components/cms/cell-edit'\nimport type { BulkAction, BulkReport, BulkSetField } from '../../components/cms/bulk-actions'\nimport type { DocumentVersionsView, ImportReport } from '../../components/cms/cms-store'\n\n/**\n * `CollectionViewSource` — the studio's UNIVERSAL interaction seam (EC-240,\n * vision AD-19).\n *\n * The list surface (`CollectionTable`) and the edit-shell (`DetailEditor`) are\n * the studio's universal interaction surfaces. Historically they read the\n * concrete `CmsStore` through `useCms()`, so they only served `cmsDocuments`.\n * This interface is the data-agnostic seam they render over instead, so the SAME\n * surfaces drive the users rail (EC-240), redirects (EC-303), the Components app\n * edit-shell, and any future rail — no hand-rolled UI per rail.\n *\n * **Shape:** the source is scoped to ONE collection (it carries `schema` +\n * `collectionId`), and its methods mirror the subset of `CmsStore` the surfaces\n * actually use — dropping the `collectionId` argument (the source already knows\n * it). Keeping the names/signatures aligned makes the CMS adapter a\n * behaviour-preserving pass-through and the migration near-mechanical.\n *\n * **Referential stability:** the surfaces put `documents`, `isPublished`,\n * `localeConfig`, etc. in `useMemo` dependency arrays. An implementation MUST\n * return stable references across renders (delegate to the underlying store's\n * already-stable methods; never wrap in a fresh closure each render) or memos\n * bust and the inline-edit order snapshot / perf regress.\n *\n * **Capabilities:** flags gate whole shell regions AND method presence — a source\n * with `capabilities.versions === false` (the users rail) never mounts the\n * Versions pane, so no `listVersions` read fires on credential records. Optional\n * method groups are present iff the flag is set.\n */\nexport interface CollectionViewSource {\n /** The rail/collection this source serves. */\n readonly collectionId: string\n /** The schema lens: fields, form tabs/groups, `titleField`, display nouns. */\n readonly schema: CollectionDef\n /** Which regions/methods this source supports. */\n readonly capabilities: CollectionViewCapabilities\n\n /** The project scope (asset picker, published-pool queries). Absent in fixture sessions. */\n readonly projectId?: string\n /**\n * The collection id assets live in (for a cell/field's `titleOf` label fallback\n * when {@link resolveAsset} finds no record). Present only for sources that\n * reference assets — a rail without assets (users) omits it.\n */\n readonly assetCollectionId?: string\n /** The ambient locale lens (only meaningful when `capabilities.locales`). */\n readonly localeConfig: LocaleConfig\n readonly activeLocale: Locale\n\n // --- Reads -----------------------------------------------------------------\n /** The full row set (view mechanics filter/sort this client-side). */\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n /** A relation/asset target's display title (hides the string-vs-function `titleField`). */\n titleOf(ref: DocumentRef): string\n /**\n * List the candidate records of a relation field's declared `target` collection\n * — the pick-list for a relation picker (EC-072). References are first-class and\n * collection-based: the field's schema declares which collection(s) it targets,\n * and this resolves those rows for ANY target (cross-collection), not just this\n * source's own collection. The caller passes `field.target`; a multi-target field\n * (future) merges one call per declared target. An unknown/empty target yields `[]`.\n */\n referenceCandidates(collectionId: string): CmsDocument[]\n /**\n * The workspace schema a reference/asset can point at — the schema counterpart to\n * {@link referenceCandidates}. Needed for the cases that read a TARGET collection's\n * fields (an asset field editing the asset collection's own metadata; the reuse\n * capability correlating embed manifests against their target collection). A\n * reference is collection-based, so resolving one sometimes needs the target's\n * schema, not just its rows.\n */\n readonly collections: readonly CollectionDef[]\n isFieldReadOnly(field: FieldDef): boolean\n /**\n * Principal-based field access (EC-240, vision AD-18): `edit` (writable), `read`\n * (visible but read-only), or `hidden` (not rendered for this principal). This is\n * IDENTITY-driven and distinct from a field's content-conditional `hidden(values)`\n * — e.g. an editor's own `role` resolves to `read`, a viewer never SEES a\n * sensitive field (`hidden`). A single tri-state (never two independent flags) so\n * the nonsensical writable-but-invisible state can't be expressed. Optional:\n * absent ⇒ derived from {@link isFieldReadOnly} (`read`|`edit`), i.e. default-open\n * (permissions RESTRICT, never grant a baseline). Per-rail policy lives on the\n * source; the starter's policy flows into its construction (engine = mechanism).\n * Prefer {@link resolveFieldAccess} over calling this directly (it applies the\n * default-open fallback).\n */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Resolve an asset id to its display metadata for a cell/field, or `undefined`\n * when there is no asset backend (a source without assets degrades to a label\n * via {@link titleOf}, matching today's \"no AssetsProvider\" behaviour).\n */\n resolveAsset?(id: string): AssetDisplay | undefined\n /**\n * How this rail's records are addressed in the EC-155 reference index — the\n * target shape the delete/unpublish confirmation's inbound pre-flight queries\n * (EC-433). Absent ⇒ the document shape (`{ type: 'document', id: 'collection:id' }`),\n * which every `cmsDocuments` collection uses. The assets rail supplies\n * `{ type: 'asset' }` so its list/bulk delete confirmation enumerates the SAME\n * inbound references its detail surface does — a document-shaped lookup would\n * report a referenced asset as \"not referenced\" (the EC-319 objection that\n * originally kept `remove` off the assets rail).\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n /** The last failed/denied mutation, surfaced by the shell (never a silent failure). */\n readonly lastError: OperationError | null\n clearLastError(): void\n /**\n * Land any queued/optimistic writes so a subsequent pre-flight READ sees them\n * (the reference index is maintained on write). A source with no async\n * persistence omits it — its reads are already authoritative.\n */\n flush?(): Promise<void>\n\n /**\n * The studio route splat for a record's detail surface (the row link + the\n * post-create navigation target), e.g. `studioPath.cmsDocument(collectionId, id)`.\n * Rail-owned so the list never hard-codes the CMS route. `undefined` ⇒ the rail\n * has no detail surface, so rows render as plain text (no navigation).\n */\n detailPath?(id: string): string\n /**\n * The studio route splat for this rail's LIST surface — the edit shell's\n * \"back to list\" breadcrumb crumb and its post-delete navigation target, e.g.\n * `studioPath.cmsCollection(collectionId)`. Rail-owned so the edit shell never\n * hard-codes the CMS route (the counterpart to {@link detailPath}). `undefined`\n * ⇒ the crumb is unlinked and delete falls back to the project index.\n */\n readonly listPath?: string\n\n // --- View state (list) -----------------------------------------------------\n viewState(): TableViewState\n setViewState(update: TableViewState | ((prev: TableViewState) => TableViewState)): void\n\n // --- Writes ----------------------------------------------------------------\n create(): DocumentRef\n /**\n * EC-595 A2 — HOW this source's records are written: exactly one model per source,\n * so a surface can never write a revisioned document the record way or vice versa.\n * `record` — the explicit-save rails (users, redirects, assets, the account): the\n * editor's Save button hands the whole record over. `document` — the CMS: every\n * writer emits patches at its source, committed on quiescence and merged per path\n * at the store; the editor shows per-field conflicts and follows the subscription.\n */\n readonly writes: CollectionWrites\n remove(ref: DocumentRef): void\n /**\n * Bulk publish/unpublish/delete/set-field (only when `capabilities.bulk`): one\n * command PER item, per-item outcomes enumerated in the report (partial failure\n * is never silent).\n */\n bulk?(ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>\n\n // --- Capability method groups (present iff the matching flag is set) --------\n /** `publish` — published-pool predicate + publish/unpublish. */\n readonly publishing?: PublishingCapability\n /**\n * Per-document timestamps (EC-445) — the data behind the `$updatedAt`\n * pseudo-column (and, once its data path lands, `$publishedAt`). Present iff the\n * source materializes them (the CMS source, sourced through session-hydration);\n * a rail that carries no timestamps omits it, so those pseudo-columns simply don't\n * apply (see {@link pseudoColumnsForSource}). The accessor shape mirrors\n * {@link PublishingCapability.isPublished} — a per-ref lookup, referentially stable.\n */\n readonly timestamps?: DocumentTimestampsCapability\n /** `versions` — version history + restore. */\n readonly versioning?: VersioningCapability\n /** `inlineEdit` — the list's single-command optimistic cell write. */\n readonly inlineEditing?: InlineEditCapability\n /**\n * Config-as-code named lenses (EC-246b) the header view selector switches\n * between. Optional: a source that declares no named views (a non-CMS rail\n * like users) omits it, and the selector renders nothing (single default).\n */\n readonly views?: NamedViewsCapability\n /**\n * `importExport` — JSON round-trip of the collection's records. CMS-specific;\n * a rail whose records are credential-bearing (users) omits it, so neither the\n * import dialog nor the export action mounts (identities aren't dumped to JSON).\n */\n readonly imports?: ImportCapability\n}\n\n/** EC-595 A2 — the write model of a source (see {@link CollectionViewSource.writes}). */\nexport type CollectionWrites = RecordWrites | DocumentWrites\n\n/** An explicit-save rail: the editor's Save button writes the whole record. */\nexport interface RecordWrites {\n readonly kind: 'record'\n save(doc: CmsDocument): void\n}\n\n/**\n * The revisioned-document write model (EC-595, ratified 2026-10-24): patches at the\n * source, the persister's queue, per-field conflicts, the document subscription.\n */\nexport interface DocumentWrites {\n readonly kind: 'document'\n /**\n * Commit the patches a writer made; resolves once acknowledged (`undefined` in a\n * fixture session). `bases` — per patch, the revision the edit was started at.\n */\n commit(\n ref: DocumentRef,\n patches: readonly DocumentPatch[],\n bases?: readonly (number | undefined)[],\n ): Promise<CommitOutcome | undefined>\n /** The revision the session's view of a document is at. */\n revisionOf(ref: DocumentRef): number | undefined\n /** Create a document from a whole value (insert-if-absent): duplicate. */\n insert(doc: CmsDocument): void\n /** A fresh document id from the session's injected factory (EC-578). */\n newId(): string\n /** The open per-field conflicts on a document. */\n conflicts(ref: DocumentRef): readonly DocumentConflict[]\n resolveConflict(ref: DocumentRef, conflictId: string, choice: 'theirs' | 'mine'): void\n /** Adopt a row the document subscription pushed (local edits win on their paths until flushed). */\n adoptSnapshot(ref: DocumentRef, record: CmsDocumentRecord | undefined): void\n /** The forced flush's persistence half: await every queued write of the document. */\n flush(ref: DocumentRef): Promise<void>\n /**\n * Write the whole document at the revision the session holds (a document-level key\n * the value-position grammar has no position for — the slot `alias`). A move since\n * is refused, never overwritten.\n */\n replace(doc: CmsDocument): void\n /**\n * EC-595 point 7 — follow ONE document while it is open: every pushed row is adopted\n * under the local edits (the editor rebases), and the presence on it is delivered to\n * `onPresence`. Returns the unsubscribe function.\n */\n subscribe(ref: DocumentRef, onPresence: (presence: readonly PresenceEntry[]) => void): () => void\n /** Thin presence (Stage A): this session is on the document, in `path`'s field (or on no field). */\n reportPresence(ref: DocumentRef, path: ValuePath | undefined): void\n /** This session left the document. */\n clearPresence(ref: DocumentRef): void\n /** This session's presence id (one per tab), so the editor can leave itself out. */\n readonly presenceSessionId: string\n}\n\n/**\n * The capability matrix. Each flag gates a shell region AND the presence of its\n * method group. The **users rail** (EC-240) sets `publish:false`, `versions:false`\n * (credential caution), `create:false` (identity minted by sign-up), and no\n * `liveUrl`/`hierarchy`.\n */\nexport interface CollectionViewCapabilities {\n readonly publish: boolean\n readonly versions: boolean\n readonly locales: boolean\n readonly inlineEdit: boolean\n readonly create: boolean\n /**\n * Duplicate-document action (EC-442, amended same day). Deliberately its own\n * flag, never derived from `create`: the owner call (2026-07-17) is CMS\n * collections ONLY — no primitive rail offers Duplicate for now (identities\n * and uploads aren't copyable content; redirects deferred until requested as\n * a product feature).\n */\n readonly duplicate: boolean\n readonly remove: boolean\n readonly bulk: boolean\n /** The derived live-URL column (published pool × deployment routes). */\n readonly liveUrl: boolean\n /** Parent-self-relation tree mode (hierarchical collections only). */\n readonly hierarchy: boolean\n /** JSON import/export of the collection's records (off for credential rails). */\n readonly importExport: boolean\n /**\n * The rail is administration-scoped (mirrors `ADMIN_ONLY_OPERATIONS`). The\n * nav-permission layer reads this to gate the rail item. Absent ⇒ not admin-only.\n */\n readonly adminOnly?: boolean\n}\n\n/**\n * Principal-based field access (EC-240, AD-18). One tri-state drives BOTH visibility\n * and writability: `edit` (writable), `read` (visible, read-only), `hidden` (not\n * rendered). Default `edit` (default-open). Distinct from content-`hidden(values)`;\n * they compose — a field shows iff *not content-hidden AND access ≠ hidden*, and is\n * editable iff *access = edit AND not otherwise read-only* (published view, `form.readOnly`).\n */\nexport type FieldAccess = 'edit' | 'read' | 'hidden'\n\n/**\n * Resolve a field's effective principal access for a source — its {@link\n * CollectionViewSource.fieldAccess} policy when present, else derived from the\n * boolean `isFieldReadOnly` (default-open). The one place the shell asks \"may this\n * principal edit / see this field?\", so the default-open fallback lives in one spot.\n */\nexport function resolveFieldAccess(\n source: CollectionViewSource,\n field: FieldDef,\n record?: CmsDocument,\n): FieldAccess {\n return source.fieldAccess?.(field, record) ?? (source.isFieldReadOnly(field) ? 'read' : 'edit')\n}\n\n/**\n * Asset display metadata a cell/field renders (thumbnail or labelled badge). EC-587:\n * named apart from core's `ResolvedAsset` (the delivery shape) — this is the studio's\n * label/thumbnail slice; its `alt` is the one reader's (`assetAlt`).\n */\nexport interface AssetDisplay {\n readonly url?: string\n readonly contentType?: string\n readonly title?: string\n readonly filename?: string\n readonly alt?: string\n}\n\n/** `publish` capability. */\nexport interface PublishingCapability {\n /** Published-ness from the version pointer (NOT a row field). */\n isPublished(ref: DocumentRef): boolean\n /**\n * Does this PUBLISHED document's working draft carry unpublished changes\n * (EC-449)? The cheap per-row signal behind the amber \"changed\" `$state` dot —\n * sourced from the MATERIALIZED `hasUnpublishedChanges` record flag (computed\n * once at the write chokepoint), never a per-row snapshot compare. Optional: a\n * source without the signal omits it, and the list derives only\n * published/draft (exactly the pre-EC-449 rendering). Same referentially-stable\n * per-ref accessor shape as {@link isPublished}.\n */\n hasUnpublishedChanges?(ref: DocumentRef): boolean\n publish(ref: DocumentRef): Promise<void>\n unpublish(ref: DocumentRef): Promise<void>\n /**\n * Live URLs for the routable documents, keyed by id — derived from the\n * PUBLISHED pool (never draft rows) so an unpublished slug/parent edit can't\n * leak into a live link (EC-205/EC-216). `null` when the collection is not\n * routable → the live-URL column is hidden. Only present when `capabilities.liveUrl`.\n */\n liveUrls?(): Map<string, DocumentUrl> | null\n}\n\n/**\n * `timestamps` capability (EC-445/EC-447): per-document record timestamps as ISO\n * strings, or `undefined` when a document carries none. The `$updatedAt`/`$publishedAt`\n * pseudo-columns read + sort by these. `updatedAt` is the last-write instant;\n * `publishedAt` (EC-447) is the FIRST-publish instant — set-once on publish, cleared on\n * unpublish, so it is `undefined` for a draft / never-published / currently-unpublished\n * document (the column shows EmDash).\n */\nexport interface DocumentTimestampsCapability {\n updatedAt(ref: DocumentRef): string | undefined\n publishedAt(ref: DocumentRef): string | undefined\n}\n\n/**\n * The reserved pseudo-columns applicable to a source's list (EC-445), derived from\n * its capabilities: `$updatedAt` iff it materializes timestamps, `$state` iff it has a\n * publish lifecycle, `$url` iff it resolves delivery URLs. The one place the list\n * surface + the Display picker agree on which pseudo-columns exist for a source, so\n * the column universe never drifts between where columns render and where they toggle.\n */\nexport function pseudoColumnsForSource(source: CollectionViewSource): string[] {\n return pseudoColumns(source.schema, {\n timestamps: source.timestamps !== undefined,\n publishing: source.publishing !== undefined,\n routable: source.capabilities.liveUrl,\n })\n}\n\n/** `versions` capability. */\nexport interface VersioningCapability {\n restore(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>\n /**\n * The document's recorded version history + live pin. Folds the flush-before-read\n * ordering INTO the loader (the adapter's concern), so every fetch lists an\n * authoritative history regardless of which surface triggered the change (EC-224).\n */\n load(ref: DocumentRef): Promise<DocumentVersionsView>\n}\n\n/** `inlineEdit` capability: the list's single-command optimistic cell write (EC-168). */\nexport interface InlineEditCapability {\n commitField(input: CellCommitInput): Promise<CellCommitOutcome>\n}\n\n/** A named lens the header selector offers — id + display name only (the header never needs its state). */\nexport interface NamedView {\n readonly id: string\n /** Display name; a {@link LocalizedString} (EC-343), resolved at the render site like `form.label`. */\n readonly name: LocalizedString\n}\n\n/** `importExport` capability: validate + fold external JSON records into the collection (EC-079). */\nexport interface ImportCapability {\n importDocuments(items: unknown[]): ImportReport\n}\n\n/**\n * `views` capability: config-as-code named lenses (EC-246b). The header selector\n * only *picks* a lens — it never creates/edits/deletes one (shared views are code).\n */\nexport interface NamedViewsCapability {\n /** The built-in default + config-declared named views. */\n list(): readonly NamedView[]\n /** The currently-selected view id. */\n activeId(): string\n /** Switch views (resets the working table state to the view's baseline). */\n setActive(id: string): void\n}\n\nexport type { CellCommitInput, CellCommitOutcome, ImportReport }\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { ContentQueryError, GetDocumentResult, ListDocumentsQuery, ListDocumentsResult, ResolvedDocument } from '@elytracms/core/content';
2
- import type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig, OrphanedCollection, RouteRecord } from '@elytracms/core/cms-core';
2
+ import type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig, OrphanedCollection, Perspective, RouteRecord } from '@elytracms/core/cms-core';
3
3
  import type { AssetRecord } from '@elytracms/core/persistence';
4
4
  import type { ListingQuery } from '@elytracms/core/runtime-renderer';
5
5
  import type { Clock } from '@elytracms/core/determinism';
@@ -80,15 +80,6 @@ export interface ContentSessionState {
80
80
  * the same loader.
81
81
  */
82
82
  export declare const playgroundCollections: readonly CollectionDef[];
83
- /**
84
- * The perspective a canvas preview resolves references against (EC-300/AD-16). The
85
- * canvas is draft-coherent: the editable Draft view resolves references to their
86
- * `'draft'` targets (so a just-made reusable shows immediately), the read-only
87
- * Published view resolves `'published'` (an honest preview of what delivery serves;
88
- * an unpublished hard ref shows the in-canvas placeholder). Delivery stays
89
- * published-only — this does not weaken `live-is-published-only`.
90
- */
91
- export type CanvasPerspective = 'draft' | 'published';
92
83
  /** The current content-session epoch — bumps on every `configureContentSession`. */
93
84
  export declare function contentSessionEpoch(): number;
94
85
  /**
@@ -142,7 +133,7 @@ export declare function getCollectionDocument(collectionId: string, idOrSlug: st
142
133
  * Reusable's element) exactly as delivery renders them. `null` when the target is
143
134
  * missing or not visible → the block degrades to its own empty state.
144
135
  */
145
- export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale, perspective?: CanvasPerspective): ResolvedDocument | null;
136
+ export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale, perspective?: Perspective): ResolvedDocument | null;
146
137
  /**
147
138
  * Resolve a listing block's query to its documents for the canvas (EC-255) — the
148
139
  * studio analog of the host's baked listings map. Runs the query (collection +
@@ -154,7 +145,7 @@ export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale,
154
145
  * ({@link validateCanvasListingQuery}) says why. Reuses the same `listDocuments`
155
146
  * path as `runCollectionListQuery`.
156
147
  */
157
- export declare function resolveCanvasListing(query: ListingQuery, locale?: Locale, perspective?: CanvasPerspective, clock?: Clock): ResolvedDocument[];
148
+ export declare function resolveCanvasListing(query: ListingQuery, locale?: Locale, perspective?: Perspective, clock?: Clock): ResolvedDocument[];
158
149
  /**
159
150
  * Statically validate a listing block's query against the ACTIVE session schema
160
151
  * (EC-460, the never-silent rule): the exact `validateWhere`/`validateSort`/
@@ -175,7 +166,7 @@ export declare function validateCanvasListingQuery(query: ListingQuery, clock?:
175
166
  * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses
176
167
  * the same `resolveLink` path as the host bake, so both agree on the derived URL.
177
168
  */
178
- export declare function resolveCanvasLink(ref: DocumentRef, locale?: Locale, perspective?: CanvasPerspective): string | null;
169
+ export declare function resolveCanvasLink(ref: DocumentRef, locale?: Locale, perspective?: Perspective): string | null;
179
170
  /** A pickable entry of a collection (the template-page preview switcher). */
180
171
  export interface CollectionEntrySummary {
181
172
  id: string;
@@ -1,5 +1,5 @@
1
1
  import { createContentClient, createContentContext, queryError, validateLimit, validateSort, validateWhere, } from '@elytracms/core/content';
2
- import { documentKey, localeSchema, parseLocaleConfig, resolveDocumentTitle, valuesInLocale, } from '@elytracms/core/cms-core';
2
+ import { documentKey, localeSchema, parseLocaleConfig, perspectiveKey, resolveDocumentTitle, valuesInLocale, } from '@elytracms/core/cms-core';
3
3
  import { listingRequestOf } from '@elytracms/core/runtime-renderer';
4
4
  import { getStudioLocale, getStudioLocaleConfig } from '../locale/locale-store.js';
5
5
  /**
@@ -149,7 +149,7 @@ export function withPlaygroundFilterable(collections) {
149
149
  }
150
150
  /**
151
151
  * Synthesize the persistence-shaped asset record the asset resolver reads. EC-580:
152
- * the record's single `alt` is the DEFAULT locale's displayed value
152
+ * the record's single alt (`meta.alt`, EC-587) is the DEFAULT locale's displayed value
153
153
  * (`valuesInLocale`, the project's config default) — it used to read
154
154
  * `localized.en.alt` whatever the project's locales were (MODE F5b, "further
155
155
  * evidence"), so a `de`-default project's previews had no alt and a shared `alt`
@@ -167,7 +167,8 @@ function assetRecordOf(doc, collection, locales) {
167
167
  filename: typeof values.filename === 'string' ? values.filename : doc.id,
168
168
  contentType: 'image/png',
169
169
  byteSize: 0,
170
- alt: typeof alt === 'string' ? alt : undefined,
170
+ // EC-587: an asset's alt lives in `meta` (its one home), never the legacy column.
171
+ ...(typeof alt === 'string' ? { meta: { alt } } : {}),
171
172
  storageKey: typeof values.url === 'string' ? values.url : doc.id,
172
173
  createdAt: '2026-01-01T00:00:00.000Z',
173
174
  updatedAt: '2026-01-01T00:00:00.000Z',
@@ -219,7 +220,12 @@ function createLookup(collectionDefs, documents, publishedKeys, assetRecords, lo
219
220
  }
220
221
  /**
221
222
  * One deterministic client per (locale, perspective): EC-018 locale fallback,
222
- * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's
223
+ * draft-or-published perspective (EC-300/AD-16, default `'published'`). The canvas is
224
+ * draft-coherent: the editable Draft view resolves references against the draft
225
+ * perspective (`DRAFT_PERSPECTIVE` — a just-made reusable shows immediately), the
226
+ * read-only Published view against `'published'` (an honest preview of what delivery
227
+ * serves; an unpublished hard ref shows the in-canvas placeholder). Delivery stays
228
+ * published-only — this does not weaken `live-is-published-only`. The studio's
223
229
  * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the
224
230
  * canvas previews follow the locale switcher exactly like delivery would; the canvas's
225
231
  * Draft/Published view picks the perspective. The cache resets whenever the content
@@ -231,7 +237,7 @@ function clientFor(locale, perspective = 'published') {
231
237
  // from the ambient studio store, which changes independently of the session the
232
238
  // cache is cleared on, so keying by locale alone could serve a client built
233
239
  // against a superseded locale set.
234
- const key = `${locale}:${perspective}:${locales.default}|${locales.locales.join(',')}`;
240
+ const key = `${locale}:${perspectiveKey(perspective)}:${locales.default}|${locales.locales.join(',')}`;
235
241
  const existing = clients.get(key);
236
242
  if (existing)
237
243
  return existing;