@elytracms/studio 0.0.92 → 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 (241) 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 +25 -11
  6. package/convex/contract-parity.test.ts +18 -3
  7. package/convex/document-revalidate.test.ts +10 -12
  8. package/convex/orphan-hydration.test.ts +116 -0
  9. package/convex/rail-write-parity.test.ts +11 -8
  10. package/convex/schema.ts +6 -0
  11. package/dist/components/ai/proposal-card.js +2 -1
  12. package/dist/components/ai/proposal-card.js.map +1 -1
  13. package/dist/components/ai/translate-document-dialog.js +4 -2
  14. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  15. package/dist/components/ai/translate-skipped.d.ts +19 -0
  16. package/dist/components/ai/translate-skipped.js +46 -0
  17. package/dist/components/ai/translate-skipped.js.map +1 -0
  18. package/dist/components/assets/asset-detail.js +18 -8
  19. package/dist/components/assets/asset-detail.js.map +1 -1
  20. package/dist/components/assets/asset-metadata.d.ts +15 -4
  21. package/dist/components/assets/asset-metadata.js +15 -2
  22. package/dist/components/assets/asset-metadata.js.map +1 -1
  23. package/dist/components/assets/asset-search.js +4 -1
  24. package/dist/components/assets/asset-search.js.map +1 -1
  25. package/dist/components/assets/asset-selection.d.ts +0 -8
  26. package/dist/components/assets/asset-selection.js +4 -23
  27. package/dist/components/assets/asset-selection.js.map +1 -1
  28. package/dist/components/assets/asset-thumb.js +3 -2
  29. package/dist/components/assets/asset-thumb.js.map +1 -1
  30. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  31. package/dist/components/assets/assets-collection-view-source.js +9 -5
  32. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  33. package/dist/components/assets/assets-store.d.ts +14 -0
  34. package/dist/components/assets/assets-store.js +23 -1
  35. package/dist/components/assets/assets-store.js.map +1 -1
  36. package/dist/components/assets/replace-file.d.ts +22 -0
  37. package/dist/components/assets/replace-file.js +59 -0
  38. package/dist/components/assets/replace-file.js.map +1 -0
  39. package/dist/components/builder/blocks-model.d.ts +14 -26
  40. package/dist/components/builder/blocks-model.js +35 -41
  41. package/dist/components/builder/blocks-model.js.map +1 -1
  42. package/dist/components/builder/blocks-panel.d.ts +7 -3
  43. package/dist/components/builder/blocks-panel.js +6 -5
  44. package/dist/components/builder/blocks-panel.js.map +1 -1
  45. package/dist/components/builder/builder-store.d.ts +104 -15
  46. package/dist/components/builder/builder-store.js +156 -31
  47. package/dist/components/builder/builder-store.js.map +1 -1
  48. package/dist/components/builder/canvas-body.d.ts +30 -0
  49. package/dist/components/builder/canvas-body.js +38 -0
  50. package/dist/components/builder/canvas-body.js.map +1 -0
  51. package/dist/components/builder/canvas-chrome-css.js +7 -0
  52. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  53. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  54. package/dist/components/builder/canvas-dnd.js +5 -5
  55. package/dist/components/builder/canvas-dnd.js.map +1 -1
  56. package/dist/components/builder/canvas-render-context.d.ts +27 -19
  57. package/dist/components/builder/canvas-render-context.js +42 -43
  58. package/dist/components/builder/canvas-render-context.js.map +1 -1
  59. package/dist/components/builder/canvas-runs.d.ts +45 -0
  60. package/dist/components/builder/canvas-runs.js +51 -0
  61. package/dist/components/builder/canvas-runs.js.map +1 -0
  62. package/dist/components/builder/canvas.js +32 -5
  63. package/dist/components/builder/canvas.js.map +1 -1
  64. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  65. package/dist/components/builder/clipboard-actions.js +44 -7
  66. package/dist/components/builder/clipboard-actions.js.map +1 -1
  67. package/dist/components/builder/composition-field-editor.d.ts +3 -15
  68. package/dist/components/builder/composition-field-editor.js +29 -40
  69. package/dist/components/builder/composition-field-editor.js.map +1 -1
  70. package/dist/components/builder/delete-key.d.ts +5 -0
  71. package/dist/components/builder/delete-key.js +4 -0
  72. package/dist/components/builder/delete-key.js.map +1 -1
  73. package/dist/components/builder/insert-context.d.ts +17 -25
  74. package/dist/components/builder/insert-context.js +45 -92
  75. package/dist/components/builder/insert-context.js.map +1 -1
  76. package/dist/components/builder/page-tree.d.ts +3 -3
  77. package/dist/components/builder/page-tree.js +3 -3
  78. package/dist/components/builder/page-tree.js.map +1 -1
  79. package/dist/components/builder/placement-refusal.d.ts +18 -0
  80. package/dist/components/builder/placement-refusal.js +46 -0
  81. package/dist/components/builder/placement-refusal.js.map +1 -0
  82. package/dist/components/builder/property-inspector.js +4 -0
  83. package/dist/components/builder/property-inspector.js.map +1 -1
  84. package/dist/components/builder/reuse-context.js +65 -27
  85. package/dist/components/builder/reuse-context.js.map +1 -1
  86. package/dist/components/builder/reuse.d.ts +79 -1
  87. package/dist/components/builder/reuse.js +69 -2
  88. package/dist/components/builder/reuse.js.map +1 -1
  89. package/dist/components/builder/run-inspector.d.ts +11 -0
  90. package/dist/components/builder/run-inspector.js +33 -0
  91. package/dist/components/builder/run-inspector.js.map +1 -0
  92. package/dist/components/cms/asset-field.d.ts +38 -1
  93. package/dist/components/cms/asset-field.js +182 -58
  94. package/dist/components/cms/asset-field.js.map +1 -1
  95. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  96. package/dist/components/cms/asset-picker-core.js +20 -0
  97. package/dist/components/cms/asset-picker-core.js.map +1 -1
  98. package/dist/components/cms/asset-picker.js +12 -18
  99. package/dist/components/cms/asset-picker.js.map +1 -1
  100. package/dist/components/cms/bulk-actions.d.ts +3 -19
  101. package/dist/components/cms/bulk-actions.js +3 -24
  102. package/dist/components/cms/bulk-actions.js.map +1 -1
  103. package/dist/components/cms/cell.js.map +1 -1
  104. package/dist/components/cms/cms-store.d.ts +5 -4
  105. package/dist/components/cms/cms-store.js +16 -36
  106. package/dist/components/cms/cms-store.js.map +1 -1
  107. package/dist/components/cms/detail-editor.js +0 -1
  108. package/dist/components/cms/detail-editor.js.map +1 -1
  109. package/dist/components/cms/document-diff-view.d.ts +3 -1
  110. package/dist/components/cms/document-diff-view.js +12 -7
  111. package/dist/components/cms/document-diff-view.js.map +1 -1
  112. package/dist/components/cms/field-anchors.d.ts +7 -4
  113. package/dist/components/cms/field-anchors.js +9 -7
  114. package/dist/components/cms/field-anchors.js.map +1 -1
  115. package/dist/components/cms/field-control-lib.d.ts +15 -1
  116. package/dist/components/cms/field-control-lib.js +22 -3
  117. package/dist/components/cms/field-control-lib.js.map +1 -1
  118. package/dist/components/cms/gate-validation.d.ts +21 -30
  119. package/dist/components/cms/gate-validation.js +11 -32
  120. package/dist/components/cms/gate-validation.js.map +1 -1
  121. package/dist/components/cms/object-array-field.js +4 -4
  122. package/dist/components/cms/object-array-field.js.map +1 -1
  123. package/dist/components/cms/prop-fields.d.ts +7 -2
  124. package/dist/components/cms/prop-fields.js +13 -6
  125. package/dist/components/cms/prop-fields.js.map +1 -1
  126. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  127. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  128. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  129. package/dist/components/cms/rich-text/insert.js +25 -17
  130. package/dist/components/cms/rich-text/insert.js.map +1 -1
  131. package/dist/components/cms/use-publish-controller.d.ts +1 -2
  132. package/dist/components/cms/use-publish-controller.js +7 -19
  133. package/dist/components/cms/use-publish-controller.js.map +1 -1
  134. package/dist/components/cms/versions-pane.js +8 -1
  135. package/dist/components/cms/versions-pane.js.map +1 -1
  136. package/dist/components/collection-view/cms-collection-view-source.js +3 -1
  137. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  138. package/dist/components/components/transfer-dialog.js +2 -1
  139. package/dist/components/components/transfer-dialog.js.map +1 -1
  140. package/dist/components/components/transfer.d.ts +10 -10
  141. package/dist/components/components/transfer.js +13 -12
  142. package/dist/components/components/transfer.js.map +1 -1
  143. package/dist/lib/ai/apply-content.js +56 -116
  144. package/dist/lib/ai/apply-content.js.map +1 -1
  145. package/dist/lib/ai/chat-context.d.ts +8 -0
  146. package/dist/lib/ai/chat-context.js +37 -6
  147. package/dist/lib/ai/chat-context.js.map +1 -1
  148. package/dist/lib/ai/chat-state.d.ts +6 -0
  149. package/dist/lib/ai/chat-state.js.map +1 -1
  150. package/dist/lib/ai/constraints.d.ts +20 -5
  151. package/dist/lib/ai/constraints.js +125 -33
  152. package/dist/lib/ai/constraints.js.map +1 -1
  153. package/dist/lib/ai/fake-provider.d.ts +16 -3
  154. package/dist/lib/ai/fake-provider.js +22 -6
  155. package/dist/lib/ai/fake-provider.js.map +1 -1
  156. package/dist/lib/ai/index.d.ts +2 -0
  157. package/dist/lib/ai/index.js +2 -0
  158. package/dist/lib/ai/index.js.map +1 -1
  159. package/dist/lib/ai/leash.d.ts +34 -0
  160. package/dist/lib/ai/leash.js +28 -0
  161. package/dist/lib/ai/leash.js.map +1 -0
  162. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  163. package/dist/lib/ai/rich-text-input.js +2 -1
  164. package/dist/lib/ai/rich-text-input.js.map +1 -1
  165. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  166. package/dist/lib/ai/rich-text-translate.js +28 -10
  167. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  168. package/dist/lib/ai/shape.d.ts +13 -13
  169. package/dist/lib/ai/shape.js +69 -93
  170. package/dist/lib/ai/shape.js.map +1 -1
  171. package/dist/lib/ai/tools.d.ts +36 -41
  172. package/dist/lib/ai/tools.js +83 -87
  173. package/dist/lib/ai/tools.js.map +1 -1
  174. package/dist/lib/ai/translate-document.d.ts +32 -4
  175. package/dist/lib/ai/translate-document.js +144 -46
  176. package/dist/lib/ai/translate-document.js.map +1 -1
  177. package/dist/lib/ai/types.d.ts +34 -23
  178. package/dist/lib/ai/types.js.map +1 -1
  179. package/dist/lib/ai/update-fields.d.ts +15 -12
  180. package/dist/lib/ai/update-fields.js +112 -188
  181. package/dist/lib/ai/update-fields.js.map +1 -1
  182. package/dist/lib/ai/writer-form.d.ts +116 -0
  183. package/dist/lib/ai/writer-form.js +429 -0
  184. package/dist/lib/ai/writer-form.js.map +1 -0
  185. package/dist/lib/assets/upload.d.ts +31 -2
  186. package/dist/lib/assets/upload.js +61 -6
  187. package/dist/lib/assets/upload.js.map +1 -1
  188. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  189. package/dist/lib/clipboard/os-clipboard.js +8 -4
  190. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  191. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  192. package/dist/lib/clipboard/paste-target.js +8 -7
  193. package/dist/lib/clipboard/paste-target.js.map +1 -1
  194. package/dist/lib/cms/field-patch.d.ts +0 -10
  195. package/dist/lib/cms/field-patch.js +4 -30
  196. package/dist/lib/cms/field-patch.js.map +1 -1
  197. package/dist/lib/cms-views/view-model.d.ts +3 -3
  198. package/dist/lib/collection-view/collection-view-source.d.ts +7 -3
  199. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  200. package/dist/lib/data/collection-content.js +3 -2
  201. package/dist/lib/data/collection-content.js.map +1 -1
  202. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  203. package/dist/lib/dnd/drop-rules.js +53 -59
  204. package/dist/lib/dnd/drop-rules.js.map +1 -1
  205. package/dist/lib/engine-version.d.ts +1 -1
  206. package/dist/lib/engine-version.js +1 -1
  207. package/dist/lib/engine-version.js.map +1 -1
  208. package/dist/lib/i18n/messages.de.js +40 -3
  209. package/dist/lib/i18n/messages.de.js.map +1 -1
  210. package/dist/lib/i18n/messages.en.d.ts +34 -2
  211. package/dist/lib/i18n/messages.en.js +43 -3
  212. package/dist/lib/i18n/messages.en.js.map +1 -1
  213. package/dist/lib/operations/project-runtime-config.d.ts +25 -6
  214. package/dist/lib/operations/project-runtime-config.js +37 -10
  215. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  216. package/dist/lib/operations/publish-with-revision.d.ts +33 -1
  217. package/dist/lib/operations/publish-with-revision.js +32 -0
  218. package/dist/lib/operations/publish-with-revision.js.map +1 -1
  219. package/dist/lib/operations/runtime-validator.d.ts +19 -68
  220. package/dist/lib/operations/runtime-validator.js +12 -138
  221. package/dist/lib/operations/runtime-validator.js.map +1 -1
  222. package/dist/lib/operations/studio-operations.d.ts +9 -2
  223. package/dist/lib/operations/studio-operations.js +13 -11
  224. package/dist/lib/operations/studio-operations.js.map +1 -1
  225. package/dist/lib/persistence/convex-adapter.js +6 -1
  226. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  227. package/dist/lib/persona/labels.js +3 -1
  228. package/dist/lib/persona/labels.js.map +1 -1
  229. package/dist/lib/workspace/index.d.ts +8 -11
  230. package/dist/lib/workspace/index.js +12 -24
  231. package/dist/lib/workspace/index.js.map +1 -1
  232. package/package.json +3 -3
  233. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  234. package/dist/components/cms/cms-composition-validators.js +0 -133
  235. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  236. package/dist/components/cms/publish-clean.d.ts +0 -28
  237. package/dist/components/cms/publish-clean.js +0 -38
  238. package/dist/components/cms/publish-clean.js.map +0 -1
  239. package/dist/lib/assets/asset-placements.d.ts +0 -28
  240. package/dist/lib/assets/asset-placements.js +0 -83
  241. package/dist/lib/assets/asset-placements.js.map +0 -1
@@ -1,5 +1,7 @@
1
- import type { ResolvedProject } from '@elytracms/core/studio-core';
2
- import type { LocaleConfig } from '@elytracms/core/cms-core';
1
+ import type { CollectionDef, LocaleConfig } from '@elytracms/core/cms-core';
2
+ import { ComponentRegistry } from '@elytracms/core/component-registry';
3
+ import { configProjectFor } from '../workspace/index.js';
4
+ export { configProjectFor };
3
5
  /**
4
6
  * EC-595 A2 — the config project an OPERATION runs for, by its project id (the slug),
5
7
  * whether or not it is the ACTIVE project.
@@ -11,10 +13,27 @@ import type { LocaleConfig } from '@elytracms/core/cms-core';
11
13
  * the operation's own `projectId`, never for whichever project the UI has active (A1
12
14
  * found-but-left #1: the manifest lookup read the active project's components).
13
15
  *
14
- * Reads the workspace state through its public reader only; zone B (EC-583) owns how the
15
- * resolved project is computed and may fold this lookup into its resolved-project
16
- * reader. `undefined` for an id no config project declares (env-only wiring, fixtures).
16
+ * The lookup itself is the workspace's ONE reader of "the config project with this slug"
17
+ * (`configProjectFor`, `lib/workspace` — EC-582 folded the copy that lived here into it);
18
+ * this module derives what an operation reads from it.
17
19
  */
18
- export declare function configProjectFor(projectId: string): ResolvedProject | undefined;
19
20
  /** The locale config of the operation's project (see {@link configProjectFor}). */
20
21
  export declare function projectLocaleConfig(projectId: string): LocaleConfig | undefined;
22
+ /**
23
+ * The collections of the operation's project (see {@link configProjectFor}); `[]` for an
24
+ * id no config project declares (env-only wiring, engine fixtures). EC-585: the ONE
25
+ * reader of "the config project with this slug" — it replaced `lib/workspace`'s
26
+ * `resolveProjectCollections`, a second copy of the same lookup (EC-595 A2 found-but-left).
27
+ */
28
+ export declare function projectCollections(projectId: string): CollectionDef[];
29
+ /**
30
+ * EC-582 — the component registry of the operation's project (see
31
+ * {@link configProjectFor}): its config component entries' MANIFESTS, registered once per
32
+ * list (the implementations are the renderer's, not the validator's — this stays off
33
+ * `runtime-renderer`, like the manifest lookup). What the publish gate's canonical
34
+ * validator judges compositions and props with — never the ACTIVE project's registry
35
+ * (critic row 13: on a deployment shared by projects with different component lists the
36
+ * gate judged an inactive project's props with the wrong manifests). `undefined` for a
37
+ * project that lists no components (or an id no config declares).
38
+ */
39
+ export declare function projectComponentRegistry(projectId: string): ComponentRegistry | undefined;
@@ -1,4 +1,6 @@
1
- import { getWorkspaceState } from '../workspace/index.js';
1
+ import { ComponentRegistry } from '@elytracms/core/component-registry';
2
+ import { configProjectFor } from '../workspace/index.js';
3
+ export { configProjectFor };
2
4
  /**
3
5
  * EC-595 A2 — the config project an OPERATION runs for, by its project id (the slug),
4
6
  * whether or not it is the ACTIVE project.
@@ -10,18 +12,43 @@ import { getWorkspaceState } from '../workspace/index.js';
10
12
  * the operation's own `projectId`, never for whichever project the UI has active (A1
11
13
  * found-but-left #1: the manifest lookup read the active project's components).
12
14
  *
13
- * Reads the workspace state through its public reader only; zone B (EC-583) owns how the
14
- * resolved project is computed and may fold this lookup into its resolved-project
15
- * reader. `undefined` for an id no config project declares (env-only wiring, fixtures).
15
+ * The lookup itself is the workspace's ONE reader of "the config project with this slug"
16
+ * (`configProjectFor`, `lib/workspace` — EC-582 folded the copy that lived here into it);
17
+ * this module derives what an operation reads from it.
16
18
  */
17
- export function configProjectFor(projectId) {
18
- const state = getWorkspaceState();
19
- if (state.kind !== 'config')
20
- return undefined;
21
- return state.projects.find((project) => project.slug === projectId);
22
- }
23
19
  /** The locale config of the operation's project (see {@link configProjectFor}). */
24
20
  export function projectLocaleConfig(projectId) {
25
21
  return configProjectFor(projectId)?.locales;
26
22
  }
23
+ /**
24
+ * The collections of the operation's project (see {@link configProjectFor}); `[]` for an
25
+ * id no config project declares (env-only wiring, engine fixtures). EC-585: the ONE
26
+ * reader of "the config project with this slug" — it replaced `lib/workspace`'s
27
+ * `resolveProjectCollections`, a second copy of the same lookup (EC-595 A2 found-but-left).
28
+ */
29
+ export function projectCollections(projectId) {
30
+ return configProjectFor(projectId)?.collections ?? [];
31
+ }
32
+ const registries = new WeakMap();
33
+ /**
34
+ * EC-582 — the component registry of the operation's project (see
35
+ * {@link configProjectFor}): its config component entries' MANIFESTS, registered once per
36
+ * list (the implementations are the renderer's, not the validator's — this stays off
37
+ * `runtime-renderer`, like the manifest lookup). What the publish gate's canonical
38
+ * validator judges compositions and props with — never the ACTIVE project's registry
39
+ * (critic row 13: on a deployment shared by projects with different component lists the
40
+ * gate judged an inactive project's props with the wrong manifests). `undefined` for a
41
+ * project that lists no components (or an id no config declares).
42
+ */
43
+ export function projectComponentRegistry(projectId) {
44
+ const entries = configProjectFor(projectId)?.components;
45
+ if (!entries)
46
+ return undefined;
47
+ let registry = registries.get(entries);
48
+ if (!registry) {
49
+ registry = new ComponentRegistry(entries.map((entry) => entry.manifest));
50
+ registries.set(entries, registry);
51
+ }
52
+ return registry;
53
+ }
27
54
  //# sourceMappingURL=project-runtime-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-runtime-config.js","sourceRoot":"","sources":["../../../src/lib/operations/project-runtime-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEhD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAA;AACrE,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACnD,OAAO,gBAAgB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;AAC7C,CAAC","sourcesContent":["import type { ResolvedProject } from '@elytracms/core/studio-core'\nimport type { LocaleConfig } from '@elytracms/core/cms-core'\nimport { getWorkspaceState } from '../workspace'\n\n/**\n * EC-595 A2 — the config project an OPERATION runs for, by its project id (the slug),\n * whether or not it is the ACTIVE project.\n *\n * One operations bundle is cached per deployment KEY (EC-179) and a deployment may\n * serve several config projects, so everything an operation reads from config — its\n * collections (A1's `resolveProjectCollections`), its component manifests (the write\n * canonicaliser), its locales (the publish gate's runtime validator) — is resolved for\n * the operation's own `projectId`, never for whichever project the UI has active (A1\n * found-but-left #1: the manifest lookup read the active project's components).\n *\n * Reads the workspace state through its public reader only; zone B (EC-583) owns how the\n * resolved project is computed and may fold this lookup into its resolved-project\n * reader. `undefined` for an id no config project declares (env-only wiring, fixtures).\n */\nexport function configProjectFor(projectId: string): ResolvedProject | undefined {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return undefined\n return state.projects.find((project) => project.slug === projectId)\n}\n\n/** The locale config of the operation's project (see {@link configProjectFor}). */\nexport function projectLocaleConfig(projectId: string): LocaleConfig | undefined {\n return configProjectFor(projectId)?.locales\n}\n"]}
1
+ {"version":3,"file":"project-runtime-config.js","sourceRoot":"","sources":["../../../src/lib/operations/project-runtime-config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAA;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE/C,OAAO,EAAE,gBAAgB,EAAE,CAAA;AAE3B;;;;;;;;;;;;;;GAcG;AAEH,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACnD,OAAO,gBAAgB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;AAC7C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,OAAO,gBAAgB,CAAC,SAAS,CAAC,EAAE,WAAW,IAAI,EAAE,CAAA;AACvD,CAAC;AAED,MAAM,UAAU,GAAG,IAAI,OAAO,EAA6B,CAAA;AAE3D;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,SAAiB;IACxD,MAAM,OAAO,GAAG,gBAAgB,CAAC,SAAS,CAAC,EAAE,UAAU,CAAA;IACvD,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAA;IAC9B,IAAI,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACtC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,QAAQ,GAAG,IAAI,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAA;QACxE,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC","sourcesContent":["import type { CollectionDef, LocaleConfig } from '@elytracms/core/cms-core'\nimport { ComponentRegistry } from '@elytracms/core/component-registry'\nimport { configProjectFor } from '../workspace'\n\nexport { configProjectFor }\n\n/**\n * EC-595 A2 — the config project an OPERATION runs for, by its project id (the slug),\n * whether or not it is the ACTIVE project.\n *\n * One operations bundle is cached per deployment KEY (EC-179) and a deployment may\n * serve several config projects, so everything an operation reads from config — its\n * collections (A1's `resolveProjectCollections`), its component manifests (the write\n * canonicaliser), its locales (the publish gate's runtime validator) — is resolved for\n * the operation's own `projectId`, never for whichever project the UI has active (A1\n * found-but-left #1: the manifest lookup read the active project's components).\n *\n * The lookup itself is the workspace's ONE reader of \"the config project with this slug\"\n * (`configProjectFor`, `lib/workspace` — EC-582 folded the copy that lived here into it);\n * this module derives what an operation reads from it.\n */\n\n/** The locale config of the operation's project (see {@link configProjectFor}). */\nexport function projectLocaleConfig(projectId: string): LocaleConfig | undefined {\n return configProjectFor(projectId)?.locales\n}\n\n/**\n * The collections of the operation's project (see {@link configProjectFor}); `[]` for an\n * id no config project declares (env-only wiring, engine fixtures). EC-585: the ONE\n * reader of \"the config project with this slug\" — it replaced `lib/workspace`'s\n * `resolveProjectCollections`, a second copy of the same lookup (EC-595 A2 found-but-left).\n */\nexport function projectCollections(projectId: string): CollectionDef[] {\n return configProjectFor(projectId)?.collections ?? []\n}\n\nconst registries = new WeakMap<object, ComponentRegistry>()\n\n/**\n * EC-582 — the component registry of the operation's project (see\n * {@link configProjectFor}): its config component entries' MANIFESTS, registered once per\n * list (the implementations are the renderer's, not the validator's — this stays off\n * `runtime-renderer`, like the manifest lookup). What the publish gate's canonical\n * validator judges compositions and props with — never the ACTIVE project's registry\n * (critic row 13: on a deployment shared by projects with different component lists the\n * gate judged an inactive project's props with the wrong manifests). `undefined` for a\n * project that lists no components (or an id no config declares).\n */\nexport function projectComponentRegistry(projectId: string): ComponentRegistry | undefined {\n const entries = configProjectFor(projectId)?.components\n if (!entries) return undefined\n let registry = registries.get(entries)\n if (!registry) {\n registry = new ComponentRegistry(entries.map((entry) => entry.manifest))\n registries.set(entries, registry)\n }\n return registry\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import type { CmsDocument, CmsValidationIssue, DocumentRef } from '@elytracms/core/cms-core';
2
2
  import type { CmsDocumentRecord } from '@elytracms/core/persistence';
3
- import type { OperationResult } from './operations-package.js';
3
+ import type { OperationError, OperationResult } from './operations-package.js';
4
4
  /** EC-595 point 6: a publish refused because the draft moved is re-validated and retried this often. */
5
5
  export declare const PUBLISH_ATTEMPTS = 3;
6
6
  export interface PublishWithRevisionDeps {
@@ -30,3 +30,35 @@ export interface PublishWithRevisionDeps {
30
30
  * nothing unvalidated is published). Any other failure is returned as is.
31
31
  */
32
32
  export declare function publishWithRevision(ref: DocumentRef, deps: PublishWithRevisionDeps): Promise<OperationResult<CmsDocumentRecord>>;
33
+ /**
34
+ * EC-585 — a publish the operation REFUSED (validation, a strict target, a conflict that
35
+ * did not settle, an unready asset library…), raised to the caller instead of resolving
36
+ * as if it had landed. Before this, the session's `publishDocument` recorded the error
37
+ * on the persist banner and RESOLVED, so the editor's controller set "Published" and
38
+ * promote toasted success on a refusal (read on the base: `cms-store.tsx:1065-1075`,
39
+ * `use-publish-controller.ts:94-96`). `message` is the localized sentence for a toast;
40
+ * `error` carries the full refusal (issues included).
41
+ */
42
+ export declare class PublishRefusedError extends Error {
43
+ readonly error: OperationError;
44
+ constructor(error: OperationError, message: string);
45
+ }
46
+ export interface PublishInSessionDeps {
47
+ /** The optimistic flip of the session's published set (reconciled below). */
48
+ markPublished(ref: DocumentRef): void;
49
+ /** {@link publishWithRevision} for this session. */
50
+ publish(ref: DocumentRef): Promise<OperationResult<unknown>>;
51
+ /** Re-read the authoritative record (the pin, the flags) — after success AND refusal. */
52
+ reconcile(ref: DocumentRef): Promise<void>;
53
+ /** Surface the refusal on the session (the persist banner). */
54
+ report(error: OperationError): void;
55
+ /** The localized sentence for a refusal. */
56
+ describe(error: OperationError): string;
57
+ }
58
+ /**
59
+ * EC-585 — the session's publish of one document (pure, deps injected): optimistic flip
60
+ * → the one publish operation → reconcile against the backend record — and on a refusal,
61
+ * report it AND reject with {@link PublishRefusedError}, so no caller (the editor's
62
+ * Publish, the versions pane, promote) can show success for a publish that did not land.
63
+ */
64
+ export declare function publishInSession(ref: DocumentRef, deps: PublishInSessionDeps): Promise<void>;
@@ -31,4 +31,36 @@ export async function publishWithRevision(ref, deps) {
31
31
  }
32
32
  return result;
33
33
  }
34
+ /**
35
+ * EC-585 — a publish the operation REFUSED (validation, a strict target, a conflict that
36
+ * did not settle, an unready asset library…), raised to the caller instead of resolving
37
+ * as if it had landed. Before this, the session's `publishDocument` recorded the error
38
+ * on the persist banner and RESOLVED, so the editor's controller set "Published" and
39
+ * promote toasted success on a refusal (read on the base: `cms-store.tsx:1065-1075`,
40
+ * `use-publish-controller.ts:94-96`). `message` is the localized sentence for a toast;
41
+ * `error` carries the full refusal (issues included).
42
+ */
43
+ export class PublishRefusedError extends Error {
44
+ error;
45
+ constructor(error, message) {
46
+ super(message);
47
+ this.error = error;
48
+ this.name = 'PublishRefusedError';
49
+ }
50
+ }
51
+ /**
52
+ * EC-585 — the session's publish of one document (pure, deps injected): optimistic flip
53
+ * → the one publish operation → reconcile against the backend record — and on a refusal,
54
+ * report it AND reject with {@link PublishRefusedError}, so no caller (the editor's
55
+ * Publish, the versions pane, promote) can show success for a publish that did not land.
56
+ */
57
+ export async function publishInSession(ref, deps) {
58
+ deps.markPublished(ref);
59
+ const result = await deps.publish(ref);
60
+ await deps.reconcile(ref);
61
+ if (!result.ok) {
62
+ deps.report(result.error);
63
+ throw new PublishRefusedError(result.error, deps.describe(result.error));
64
+ }
65
+ }
34
66
  //# sourceMappingURL=publish-with-revision.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"publish-with-revision.js","sourceRoot":"","sources":["../../../src/lib/operations/publish-with-revision.ts"],"names":[],"mappings":"AAIA,wGAAwG;AACxG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAA;AAkBjC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAgB,EAChB,IAA6B;IAE7B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,gBAAgB,CAAA;IAClD,IAAI,MAAsD,CAAA;IAC1D,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACrB,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;QACtD,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACxB,OAAO,MAAM,CAAA;QACf,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;QAC1B,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO,MAAM,CAAA;QACvF,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAA;QAC3D,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC;YAAE,OAAO,MAAM,CAAA;IACpF,CAAC;IACD,OAAO,MAAO,CAAA;AAChB,CAAC","sourcesContent":["import type { CmsDocument, CmsValidationIssue, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CmsDocumentRecord } from '@elytracms/core/persistence'\nimport type { OperationResult } from './operations-package'\n\n/** EC-595 point 6: a publish refused because the draft moved is re-validated and retried this often. */\nexport const PUBLISH_ATTEMPTS = 3\n\nexport interface PublishWithRevisionDeps {\n /** The FORCED FLUSH: every queued patch of the document lands (the session adopts the committed draft). */\n flush(ref: DocumentRef): Promise<void>\n /** The revision the session's view of the document is at. */\n revisionOf(ref: DocumentRef): number | undefined\n /** `documents.publish({ ref, expectedRevision })`. */\n publish(ref: DocumentRef, expectedRevision: number | undefined): Promise<OperationResult<CmsDocumentRecord>>\n /** Adopt a server row (the publish result, or a refusal's `current`). */\n adopt(record: CmsDocumentRecord): void\n /** The session's view of the document after an adoption. */\n documentOf(ref: DocumentRef): CmsDocument | undefined\n /** The session's (display) validation — re-run on the moved draft before a retry. */\n validate(document: CmsDocument): readonly CmsValidationIssue[]\n attempts?: number\n}\n\n/**\n * EC-595 binding point 6 — publish the revision the client validated (pure, deps\n * injected — unit-tested without a React tree).\n *\n * FORCED FLUSH, then `documents.publish({ expectedRevision })`: the store refuses when\n * the draft moved since (a colleague's save landed between the flush and the publish —\n * Codex P05). On that refusal the session adopts the current row, RE-VALIDATES what\n * would now be published, and retries — bounded by {@link PUBLISH_ATTEMPTS}, and never\n * past a blocking issue the moved draft carries (the refusal is returned instead, so\n * nothing unvalidated is published). Any other failure is returned as is.\n */\nexport async function publishWithRevision(\n ref: DocumentRef,\n deps: PublishWithRevisionDeps,\n): Promise<OperationResult<CmsDocumentRecord>> {\n const attempts = deps.attempts ?? PUBLISH_ATTEMPTS\n let result: OperationResult<CmsDocumentRecord> | undefined\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n await deps.flush(ref)\n result = await deps.publish(ref, deps.revisionOf(ref))\n if (result.ok) {\n deps.adopt(result.value)\n return result\n }\n const error = result.error\n if (error.code !== 'conflict' || !('current' in error) || !error.current) return result\n deps.adopt(error.current)\n const view = deps.documentOf(ref) ?? error.current.document\n if (deps.validate(view).some((issue) => issue.severity === 'error')) return result\n }\n return result!\n}\n"]}
1
+ {"version":3,"file":"publish-with-revision.js","sourceRoot":"","sources":["../../../src/lib/operations/publish-with-revision.ts"],"names":[],"mappings":"AAIA,wGAAwG;AACxG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAA;AAkBjC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAgB,EAChB,IAA6B;IAE7B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,gBAAgB,CAAA;IAClD,IAAI,MAAsD,CAAA;IAC1D,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACrB,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;QACtD,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACxB,OAAO,MAAM,CAAA;QACf,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;QAC1B,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO,MAAM,CAAA;QACvF,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAA;QAC3D,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC;YAAE,OAAO,MAAM,CAAA;IACpF,CAAC;IACD,OAAO,MAAO,CAAA;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAEjC;IADX,YACW,KAAqB,EAC9B,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAA;QAHL,UAAK,GAAL,KAAK,CAAgB;QAI9B,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF;AAeD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAgB,EAAE,IAA0B;IACjF,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;IACvB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;IACzB,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACzB,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IAC1E,CAAC;AACH,CAAC","sourcesContent":["import type { CmsDocument, CmsValidationIssue, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CmsDocumentRecord } from '@elytracms/core/persistence'\nimport type { OperationError, OperationResult } from './operations-package'\n\n/** EC-595 point 6: a publish refused because the draft moved is re-validated and retried this often. */\nexport const PUBLISH_ATTEMPTS = 3\n\nexport interface PublishWithRevisionDeps {\n /** The FORCED FLUSH: every queued patch of the document lands (the session adopts the committed draft). */\n flush(ref: DocumentRef): Promise<void>\n /** The revision the session's view of the document is at. */\n revisionOf(ref: DocumentRef): number | undefined\n /** `documents.publish({ ref, expectedRevision })`. */\n publish(ref: DocumentRef, expectedRevision: number | undefined): Promise<OperationResult<CmsDocumentRecord>>\n /** Adopt a server row (the publish result, or a refusal's `current`). */\n adopt(record: CmsDocumentRecord): void\n /** The session's view of the document after an adoption. */\n documentOf(ref: DocumentRef): CmsDocument | undefined\n /** The session's (display) validation — re-run on the moved draft before a retry. */\n validate(document: CmsDocument): readonly CmsValidationIssue[]\n attempts?: number\n}\n\n/**\n * EC-595 binding point 6 — publish the revision the client validated (pure, deps\n * injected — unit-tested without a React tree).\n *\n * FORCED FLUSH, then `documents.publish({ expectedRevision })`: the store refuses when\n * the draft moved since (a colleague's save landed between the flush and the publish —\n * Codex P05). On that refusal the session adopts the current row, RE-VALIDATES what\n * would now be published, and retries — bounded by {@link PUBLISH_ATTEMPTS}, and never\n * past a blocking issue the moved draft carries (the refusal is returned instead, so\n * nothing unvalidated is published). Any other failure is returned as is.\n */\nexport async function publishWithRevision(\n ref: DocumentRef,\n deps: PublishWithRevisionDeps,\n): Promise<OperationResult<CmsDocumentRecord>> {\n const attempts = deps.attempts ?? PUBLISH_ATTEMPTS\n let result: OperationResult<CmsDocumentRecord> | undefined\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n await deps.flush(ref)\n result = await deps.publish(ref, deps.revisionOf(ref))\n if (result.ok) {\n deps.adopt(result.value)\n return result\n }\n const error = result.error\n if (error.code !== 'conflict' || !('current' in error) || !error.current) return result\n deps.adopt(error.current)\n const view = deps.documentOf(ref) ?? error.current.document\n if (deps.validate(view).some((issue) => issue.severity === 'error')) return result\n }\n return result!\n}\n\n/**\n * EC-585 — a publish the operation REFUSED (validation, a strict target, a conflict that\n * did not settle, an unready asset library…), raised to the caller instead of resolving\n * as if it had landed. Before this, the session's `publishDocument` recorded the error\n * on the persist banner and RESOLVED, so the editor's controller set \"Published\" and\n * promote toasted success on a refusal (read on the base: `cms-store.tsx:1065-1075`,\n * `use-publish-controller.ts:94-96`). `message` is the localized sentence for a toast;\n * `error` carries the full refusal (issues included).\n */\nexport class PublishRefusedError extends Error {\n constructor(\n readonly error: OperationError,\n message: string,\n ) {\n super(message)\n this.name = 'PublishRefusedError'\n }\n}\n\nexport interface PublishInSessionDeps {\n /** The optimistic flip of the session's published set (reconciled below). */\n markPublished(ref: DocumentRef): void\n /** {@link publishWithRevision} for this session. */\n publish(ref: DocumentRef): Promise<OperationResult<unknown>>\n /** Re-read the authoritative record (the pin, the flags) — after success AND refusal. */\n reconcile(ref: DocumentRef): Promise<void>\n /** Surface the refusal on the session (the persist banner). */\n report(error: OperationError): void\n /** The localized sentence for a refusal. */\n describe(error: OperationError): string\n}\n\n/**\n * EC-585 — the session's publish of one document (pure, deps injected): optimistic flip\n * → the one publish operation → reconcile against the backend record — and on a refusal,\n * report it AND reject with {@link PublishRefusedError}, so no caller (the editor's\n * Publish, the versions pane, promote) can show success for a publish that did not land.\n */\nexport async function publishInSession(ref: DocumentRef, deps: PublishInSessionDeps): Promise<void> {\n deps.markPublished(ref)\n const result = await deps.publish(ref)\n await deps.reconcile(ref)\n if (!result.ok) {\n deps.report(result.error)\n throw new PublishRefusedError(result.error, deps.describe(result.error))\n }\n}\n"]}
@@ -1,81 +1,32 @@
1
- import type { CmsDocument, CmsValidationIssue, LocaleConfig, ValidateDocumentOptions } from '@elytracms/core/cms-core';
2
- import type { AssetRecord, CmsDocumentRecord, PersistenceAdapter } from '@elytracms/core/persistence';
3
- import type { GateValidationInput } from '../../components/cms/gate-validation.js';
1
+ import type { LocaleConfig } from '@elytracms/core/cms-core';
2
+ import type { ComponentRegistry } from '@elytracms/core/component-registry';
4
3
  import type { CollectionsResolver, DocumentValidator } from './operations-package.js';
5
4
  /**
6
- * EC-595 A2 — THE publish gate's validator, bound to the OPERATIONS RUNTIME (Codex F23,
7
- * ruled EC-565 1f): built once per operations bundle from the resolved config, never
8
- * read from a React mount.
5
+ * EC-595 A2 → EC-582 — THE studio publish gate's validator, bound to the OPERATIONS
6
+ * RUNTIME (Codex F23, ruled EC-565 1f): built once per operations bundle, never read from
7
+ * a React mount. Since EC-582 it is core's `createDocumentValidator`
8
+ * (`@elytracms/core/validation` — the same gate a headless embedder and the CLI build):
9
+ * per call it resolves the OPERATION's project — its collections, locales and component
10
+ * registry (`registry(projectId)`: THAT project's manifests, never the active project's —
11
+ * a deployment may serve several projects with different component lists) — prefetches
12
+ * the oracles through the operation's adapter and runs the canonical validator.
9
13
  *
10
- * Before this module the gate read a module-global stack (`lib/cms/document-gate.ts`)
11
- * that every mounted `CmsProvider` pushed its session validator onto, innermost wins —
12
- * so the transfer dialog's second provider changed the verdict for the whole app, a
13
- * publish with no session mounted had nothing to ask, and an undeclared collection
14
- * answered `[]` (a pass). Measured red on the base (EC-595 Done, part A2).
15
- *
16
- * Per call (`documents.publish` → `validateDocument(document, { projectId, adapter })`):
17
- *
18
- * 1. **The project's config** — its collections (`collections(projectId)`) and locales.
19
- * A collection the project does not declare is a BLOCKING issue: there are no field
20
- * rules to judge it by, and passing it unjudged is the defect.
21
- * 2. **Oracles prefetched through the adapter** the operation runs on (the scratch
22
- * adapter in a changeset dry run, the real one on apply): every document the draft
23
- * references at every position THE reference walk reaches (`walkReferences`, EC-589 —
24
- * existence + published-ness from the record's version pointer, what the
25
- * validator's relation-target pass reads), the document's own collection (siblings — `validation.unique` and
26
- * hierarchy cycles need the collection), and every asset it references
27
- * (`assets.resolveMany` — existence + the field's `accept`).
28
- * 3. **An unready asset library refuses visibly** (EC-588 found-but-left, ruled 1f):
29
- * when the asset read fails, a document that references assets is refused with a
30
- * localised issue — never passed with the asset checks silently off.
31
- * 4. **The rules** — the ONE option bag (`gateValidateOptions`, EC-497: the same bag the
32
- * editor's live validation builds over the session) over the prefetched world, so
33
- * the sync `validateDocument` runs over data, not over the mount.
34
- *
35
- * The bag builder pulls in the composition leashes, which read the studio component
36
- * registry (→ `runtime-renderer`). `loadBag` is therefore imported DYNAMICALLY inside
37
- * the async validator, so the operations bootstrap's static module graph does not grow
38
- * by the registry (measured before/after, EC-595 Done part A2).
14
+ * `@elytracms/core/validation` (the composition leash pulls in the rich-text parser) is
15
+ * imported DYNAMICALLY inside the async validator, so the operations bootstrap's static
16
+ * module graph does not grow by it (measured before/after, EC-595 Done part A2 and EC-582
17
+ * Done).
39
18
  *
40
19
  * The editor's live validation keeps reading the session (that is display); the GATE
41
- * reads this.
20
+ * reads this. The refusals and their catalog keys (`GATE_*_MESSAGE_KEY`) are declared
21
+ * once, in core.
42
22
  */
43
- /** The catalog key of the unknown-collection refusal. */
44
- export declare const GATE_UNKNOWN_COLLECTION_MESSAGE_KEY = "cms.issueMessage.gateUnknownCollection";
45
- /** The catalog key of the unready-asset-library refusal. */
46
- export declare const GATE_ASSET_LIBRARY_UNREADY_MESSAGE_KEY = "cms.issueMessage.gateAssetLibraryUnready";
47
- /** The catalog key of the no-locales refusal. */
48
- export declare const GATE_NO_LOCALES_MESSAGE_KEY = "cms.issueMessage.gateNoLocales";
49
- /** The bag builder's signature (`components/cms/gate-validation.ts`), loaded lazily. */
50
- export type GateBagBuilder = (input: GateValidationInput) => ValidateDocumentOptions;
51
23
  export interface RuntimeValidatorOptions {
52
24
  /** The resolved config's collections, per project (the operations context's resolver). */
53
25
  readonly collections: CollectionsResolver;
54
- /** The project's locale config; `undefined` → the project declares none (a blocking config issue elsewhere). */
26
+ /** The project's locale config; `undefined` → the project declares none (a blocking refusal). */
55
27
  readonly localeConfig: (projectId: string) => LocaleConfig | undefined;
56
- /** THE option-bag builder, loaded on first use (see the module doc for why it is lazy). */
57
- readonly loadBag: () => Promise<GateBagBuilder>;
28
+ /** The project's component registry (its manifests); `undefined` → it lists no components. */
29
+ readonly registry: (projectId: string) => ComponentRegistry | undefined;
58
30
  }
59
- /** The prefetched world one document is judged over. */
60
- export interface PrefetchedOracles {
61
- /** The referenced documents + the document's own collection, as stored (drafts). */
62
- readonly records: readonly CmsDocumentRecord[];
63
- /** The referenced assets by id, or `'unready'` when the library could not be read. */
64
- readonly assets: ReadonlyMap<string, AssetRecord> | 'unready';
65
- /** Whether the document references any asset at all. */
66
- readonly referencesAssets: boolean;
67
- }
68
- /**
69
- * Prefetch, through the adapter, exactly what `document` needs to be judged: the
70
- * documents it references, its own collection's documents, and the assets it
71
- * references. Exported for tests.
72
- */
73
- export declare function prefetchGateOracles(adapter: PersistenceAdapter, projectId: string, document: CmsDocument): Promise<PrefetchedOracles>;
74
- /** The unknown-collection refusal (exported for the catalog corpus test). */
75
- export declare function unknownCollectionRefusal(document: CmsDocument): CmsValidationIssue;
76
- /** The unready-asset-library refusal (exported for the catalog corpus test). */
77
- export declare function assetLibraryUnreadyRefusal(document: CmsDocument): CmsValidationIssue;
78
- /** The no-locales refusal (exported for the catalog corpus test). */
79
- export declare function noLocalesRefusal(document: CmsDocument): CmsValidationIssue;
80
31
  /** Build THE gate validator for an operations bundle. */
81
32
  export declare function createRuntimeDocumentValidator(options: RuntimeValidatorOptions): DocumentValidator;
@@ -1,144 +1,18 @@
1
- import { createCollectionRegistry, createDocumentStore, documentKey, validateDocument, walkReferences, } from '@elytracms/core/cms-core';
2
- /**
3
- * EC-595 A2 — THE publish gate's validator, bound to the OPERATIONS RUNTIME (Codex F23,
4
- * ruled EC-565 1f): built once per operations bundle from the resolved config, never
5
- * read from a React mount.
6
- *
7
- * Before this module the gate read a module-global stack (`lib/cms/document-gate.ts`)
8
- * that every mounted `CmsProvider` pushed its session validator onto, innermost wins —
9
- * so the transfer dialog's second provider changed the verdict for the whole app, a
10
- * publish with no session mounted had nothing to ask, and an undeclared collection
11
- * answered `[]` (a pass). Measured red on the base (EC-595 Done, part A2).
12
- *
13
- * Per call (`documents.publish` → `validateDocument(document, { projectId, adapter })`):
14
- *
15
- * 1. **The project's config** — its collections (`collections(projectId)`) and locales.
16
- * A collection the project does not declare is a BLOCKING issue: there are no field
17
- * rules to judge it by, and passing it unjudged is the defect.
18
- * 2. **Oracles prefetched through the adapter** the operation runs on (the scratch
19
- * adapter in a changeset dry run, the real one on apply): every document the draft
20
- * references at every position THE reference walk reaches (`walkReferences`, EC-589 —
21
- * existence + published-ness from the record's version pointer, what the
22
- * validator's relation-target pass reads), the document's own collection (siblings — `validation.unique` and
23
- * hierarchy cycles need the collection), and every asset it references
24
- * (`assets.resolveMany` — existence + the field's `accept`).
25
- * 3. **An unready asset library refuses visibly** (EC-588 found-but-left, ruled 1f):
26
- * when the asset read fails, a document that references assets is refused with a
27
- * localised issue — never passed with the asset checks silently off.
28
- * 4. **The rules** — the ONE option bag (`gateValidateOptions`, EC-497: the same bag the
29
- * editor's live validation builds over the session) over the prefetched world, so
30
- * the sync `validateDocument` runs over data, not over the mount.
31
- *
32
- * The bag builder pulls in the composition leashes, which read the studio component
33
- * registry (→ `runtime-renderer`). `loadBag` is therefore imported DYNAMICALLY inside
34
- * the async validator, so the operations bootstrap's static module graph does not grow
35
- * by the registry (measured before/after, EC-595 Done part A2).
36
- *
37
- * The editor's live validation keeps reading the session (that is display); the GATE
38
- * reads this.
39
- */
40
- /** The catalog key of the unknown-collection refusal. */
41
- export const GATE_UNKNOWN_COLLECTION_MESSAGE_KEY = 'cms.issueMessage.gateUnknownCollection';
42
- /** The catalog key of the unready-asset-library refusal. */
43
- export const GATE_ASSET_LIBRARY_UNREADY_MESSAGE_KEY = 'cms.issueMessage.gateAssetLibraryUnready';
44
- /** The catalog key of the no-locales refusal. */
45
- export const GATE_NO_LOCALES_MESSAGE_KEY = 'cms.issueMessage.gateNoLocales';
46
- /**
47
- * Prefetch, through the adapter, exactly what `document` needs to be judged: the
48
- * documents it references, its own collection's documents, and the assets it
49
- * references. Exported for tests.
50
- */
51
- export async function prefetchGateOracles(adapter, projectId, document) {
52
- // THE reference walk (EC-589 `walkReferences`) — the one every reference reader calls,
53
- // the validator's relation-target pass included: every position it reaches is a
54
- // target whose existence and published-ness the pass reads, so it is exactly what is
55
- // prefetched (a hidden position is a subset — the pass judges the publish strip).
56
- const targetsByKey = new Map();
57
- const assetIds = new Set();
58
- walkReferences(document, (hit) => {
59
- if (hit.target.type === 'document')
60
- targetsByKey.set(documentKey(hit.target.ref), hit.target.ref);
61
- else if (hit.target.type === 'asset')
62
- assetIds.add(hit.target.id);
63
- });
64
- const siblings = adapter.cms.listDocuments(projectId, document.collection);
65
- const targets = Promise.all([...targetsByKey.values()].map(async (ref) =>
66
- // The document's own collection comes whole with the siblings (unique, hierarchy).
67
- ref.collection === document.collection ? undefined : adapter.cms.findDocument(projectId, ref)));
68
- let assets = new Map();
69
- if (assetIds.size > 0) {
70
- try {
71
- assets = await adapter.assets.resolveMany(projectId, [...assetIds]);
72
- }
73
- catch {
74
- assets = 'unready';
75
- }
76
- }
77
- const [own, others] = await Promise.all([siblings, targets]);
78
- return {
79
- records: [...own, ...others.filter((record) => record !== undefined)],
80
- assets,
81
- referencesAssets: assetIds.size > 0,
82
- };
83
- }
84
- function gateIssue(document, message, key, vars) {
85
- return {
86
- code: 'custom-validation',
87
- severity: 'error',
88
- message,
89
- messageRef: { key, ...(vars ? { vars } : {}) },
90
- path: [],
91
- collectionId: document.collection,
92
- documentId: document.id,
93
- };
94
- }
95
- /** The unknown-collection refusal (exported for the catalog corpus test). */
96
- export function unknownCollectionRefusal(document) {
97
- return gateIssue(document, `The project declares no collection “${document.collection}” — there are no rules to publish it by.`, GATE_UNKNOWN_COLLECTION_MESSAGE_KEY, { collection: document.collection });
98
- }
99
- /** The unready-asset-library refusal (exported for the catalog corpus test). */
100
- export function assetLibraryUnreadyRefusal(document) {
101
- return gateIssue(document, 'The asset library could not be read — refusing to publish unverified asset references. Try again.', GATE_ASSET_LIBRARY_UNREADY_MESSAGE_KEY);
102
- }
103
- /** The no-locales refusal (exported for the catalog corpus test). */
104
- export function noLocalesRefusal(document) {
105
- return gateIssue(document, 'The project declares no locales — refusing to publish without them.', GATE_NO_LOCALES_MESSAGE_KEY);
106
- }
107
1
  /** Build THE gate validator for an operations bundle. */
108
2
  export function createRuntimeDocumentValidator(options) {
3
+ let gate;
109
4
  return async (document, context) => {
110
- const collections = options.collections(context.projectId);
111
- const collection = collections.find((candidate) => candidate.id === document.collection);
112
- if (!collection)
113
- return [unknownCollectionRefusal(document)];
114
- // EC-479: a project without locales has no storage key for a localized value; the
115
- // gate refuses rather than inventing one.
116
- const localeConfig = options.localeConfig(context.projectId);
117
- if (!localeConfig)
118
- return [noLocalesRefusal(document)];
119
- const oracles = await prefetchGateOracles(context.adapter, context.projectId, document);
120
- if (oracles.assets === 'unready' && oracles.referencesAssets) {
121
- return [assetLibraryUnreadyRefusal(document)];
122
- }
123
- const published = new Set(oracles.records
124
- .filter((record) => record.publishedVersion !== undefined)
125
- .map((record) => documentKey(record.document)));
126
- const assets = oracles.assets === 'unready' ? new Map() : oracles.assets;
127
- const bag = await options.loadBag();
128
- // The document under judgement is swapped into the store (the session bag's contract).
129
- const store = createDocumentStore([
130
- ...oracles.records
131
- .map((record) => record.document)
132
- .filter((doc) => !(doc.collection === document.collection && doc.id === document.id)),
133
- document,
134
- ]);
135
- const validateOptions = bag({
136
- store,
137
- localeConfig,
138
- isPublished: (ref) => published.has(documentKey(ref)),
139
- findAsset: (id) => assets.get(id),
140
- });
141
- return validateDocument(createCollectionRegistry(collections), document, validateOptions);
5
+ gate ??= import('@elytracms/core/validation').then(({ createDocumentValidator }) => createDocumentValidator({
6
+ project: (projectId) => {
7
+ const registry = options.registry(projectId);
8
+ return {
9
+ collections: options.collections(projectId),
10
+ locales: options.localeConfig(projectId),
11
+ ...(registry ? { registry } : {}),
12
+ };
13
+ },
14
+ }));
15
+ return (await gate)(document, context);
142
16
  };
143
17
  }
144
18
  //# sourceMappingURL=runtime-validator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-validator.js","sourceRoot":"","sources":["../../../src/lib/operations/runtime-validator.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createCollectionRegistry,\n createDocumentStore,\n documentKey,\n validateDocument,\n walkReferences,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n DocumentRef,\n LocaleConfig,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord, CmsDocumentRecord, PersistenceAdapter } from '@elytracms/core/persistence'\nimport type { GateValidationInput } from '../../components/cms/gate-validation'\nimport type { CollectionsResolver, DocumentValidator } from './operations-package'\n\n/**\n * EC-595 A2 — THE publish gate's validator, bound to the OPERATIONS RUNTIME (Codex F23,\n * ruled EC-565 1f): built once per operations bundle from the resolved config, never\n * read from a React mount.\n *\n * Before this module the gate read a module-global stack (`lib/cms/document-gate.ts`)\n * that every mounted `CmsProvider` pushed its session validator onto, innermost wins —\n * so the transfer dialog's second provider changed the verdict for the whole app, a\n * publish with no session mounted had nothing to ask, and an undeclared collection\n * answered `[]` (a pass). Measured red on the base (EC-595 Done, part A2).\n *\n * Per call (`documents.publish` → `validateDocument(document, { projectId, adapter })`):\n *\n * 1. **The project's config** — its collections (`collections(projectId)`) and locales.\n * A collection the project does not declare is a BLOCKING issue: there are no field\n * rules to judge it by, and passing it unjudged is the defect.\n * 2. **Oracles prefetched through the adapter** the operation runs on (the scratch\n * adapter in a changeset dry run, the real one on apply): every document the draft\n * references at every position THE reference walk reaches (`walkReferences`, EC-589 —\n * existence + published-ness from the record's version pointer, what the\n * validator's relation-target pass reads), the document's own collection (siblings — `validation.unique` and\n * hierarchy cycles need the collection), and every asset it references\n * (`assets.resolveMany` — existence + the field's `accept`).\n * 3. **An unready asset library refuses visibly** (EC-588 found-but-left, ruled 1f):\n * when the asset read fails, a document that references assets is refused with a\n * localised issue — never passed with the asset checks silently off.\n * 4. **The rules** — the ONE option bag (`gateValidateOptions`, EC-497: the same bag the\n * editor's live validation builds over the session) over the prefetched world, so\n * the sync `validateDocument` runs over data, not over the mount.\n *\n * The bag builder pulls in the composition leashes, which read the studio component\n * registry (→ `runtime-renderer`). `loadBag` is therefore imported DYNAMICALLY inside\n * the async validator, so the operations bootstrap's static module graph does not grow\n * by the registry (measured before/after, EC-595 Done part A2).\n *\n * The editor's live validation keeps reading the session (that is display); the GATE\n * reads this.\n */\n\n/** The catalog key of the unknown-collection refusal. */\nexport const GATE_UNKNOWN_COLLECTION_MESSAGE_KEY = 'cms.issueMessage.gateUnknownCollection'\n/** The catalog key of the unready-asset-library refusal. */\nexport const GATE_ASSET_LIBRARY_UNREADY_MESSAGE_KEY = 'cms.issueMessage.gateAssetLibraryUnready'\n\n/** The catalog key of the no-locales refusal. */\nexport const GATE_NO_LOCALES_MESSAGE_KEY = 'cms.issueMessage.gateNoLocales'\n\n/** The bag builder's signature (`components/cms/gate-validation.ts`), loaded lazily. */\nexport type GateBagBuilder = (input: GateValidationInput) => ValidateDocumentOptions\n\nexport interface RuntimeValidatorOptions {\n /** The resolved config's collections, per project (the operations context's resolver). */\n readonly collections: CollectionsResolver\n /** The project's locale config; `undefined` → the project declares none (a blocking config issue elsewhere). */\n readonly localeConfig: (projectId: string) => LocaleConfig | undefined\n /** THE option-bag builder, loaded on first use (see the module doc for why it is lazy). */\n readonly loadBag: () => Promise<GateBagBuilder>\n}\n\n/** The prefetched world one document is judged over. */\nexport interface PrefetchedOracles {\n /** The referenced documents + the document's own collection, as stored (drafts). */\n readonly records: readonly CmsDocumentRecord[]\n /** The referenced assets by id, or `'unready'` when the library could not be read. */\n readonly assets: ReadonlyMap<string, AssetRecord> | 'unready'\n /** Whether the document references any asset at all. */\n readonly referencesAssets: boolean\n}\n\n/**\n * Prefetch, through the adapter, exactly what `document` needs to be judged: the\n * documents it references, its own collection's documents, and the assets it\n * references. Exported for tests.\n */\nexport async function prefetchGateOracles(\n adapter: PersistenceAdapter,\n projectId: string,\n document: CmsDocument,\n): Promise<PrefetchedOracles> {\n // THE reference walk (EC-589 `walkReferences`) — the one every reference reader calls,\n // the validator's relation-target pass included: every position it reaches is a\n // target whose existence and published-ness the pass reads, so it is exactly what is\n // prefetched (a hidden position is a subset — the pass judges the publish strip).\n const targetsByKey = new Map<string, DocumentRef>()\n const assetIds = new Set<string>()\n walkReferences(document, (hit) => {\n if (hit.target.type === 'document') targetsByKey.set(documentKey(hit.target.ref), hit.target.ref)\n else if (hit.target.type === 'asset') assetIds.add(hit.target.id)\n })\n const siblings = adapter.cms.listDocuments(projectId, document.collection)\n const targets = Promise.all(\n [...targetsByKey.values()].map(async (ref) =>\n // The document's own collection comes whole with the siblings (unique, hierarchy).\n ref.collection === document.collection ? undefined : adapter.cms.findDocument(projectId, ref),\n ),\n )\n let assets: PrefetchedOracles['assets'] = new Map()\n if (assetIds.size > 0) {\n try {\n assets = await adapter.assets.resolveMany(projectId, [...assetIds])\n } catch {\n assets = 'unready'\n }\n }\n const [own, others] = await Promise.all([siblings, targets])\n return {\n records: [...own, ...others.filter((record): record is CmsDocumentRecord => record !== undefined)],\n assets,\n referencesAssets: assetIds.size > 0,\n }\n}\n\nfunction gateIssue(document: CmsDocument, message: string, key: string, vars?: Record<string, string>): CmsValidationIssue {\n return {\n code: 'custom-validation',\n severity: 'error',\n message,\n messageRef: { key, ...(vars ? { vars } : {}) },\n path: [],\n collectionId: document.collection,\n documentId: document.id,\n }\n}\n\n/** The unknown-collection refusal (exported for the catalog corpus test). */\nexport function unknownCollectionRefusal(document: CmsDocument): CmsValidationIssue {\n return gateIssue(\n document,\n `The project declares no collection “${document.collection}” — there are no rules to publish it by.`,\n GATE_UNKNOWN_COLLECTION_MESSAGE_KEY,\n { collection: document.collection },\n )\n}\n\n/** The unready-asset-library refusal (exported for the catalog corpus test). */\nexport function assetLibraryUnreadyRefusal(document: CmsDocument): CmsValidationIssue {\n return gateIssue(\n document,\n 'The asset library could not be read — refusing to publish unverified asset references. Try again.',\n GATE_ASSET_LIBRARY_UNREADY_MESSAGE_KEY,\n )\n}\n\n/** The no-locales refusal (exported for the catalog corpus test). */\nexport function noLocalesRefusal(document: CmsDocument): CmsValidationIssue {\n return gateIssue(\n document,\n 'The project declares no locales — refusing to publish without them.',\n GATE_NO_LOCALES_MESSAGE_KEY,\n )\n}\n\n/** Build THE gate validator for an operations bundle. */\nexport function createRuntimeDocumentValidator(options: RuntimeValidatorOptions): DocumentValidator {\n return async (document, context) => {\n const collections = options.collections(context.projectId)\n const collection = collections.find((candidate) => candidate.id === document.collection)\n if (!collection) return [unknownCollectionRefusal(document)]\n // EC-479: a project without locales has no storage key for a localized value; the\n // gate refuses rather than inventing one.\n const localeConfig = options.localeConfig(context.projectId)\n if (!localeConfig) return [noLocalesRefusal(document)]\n const oracles = await prefetchGateOracles(context.adapter, context.projectId, document)\n if (oracles.assets === 'unready' && oracles.referencesAssets) {\n return [assetLibraryUnreadyRefusal(document)]\n }\n const published = new Set(\n oracles.records\n .filter((record) => record.publishedVersion !== undefined)\n .map((record) => documentKey(record.document)),\n )\n const assets = oracles.assets === 'unready' ? new Map<string, AssetRecord>() : oracles.assets\n const bag = await options.loadBag()\n // The document under judgement is swapped into the store (the session bag's contract).\n const store = createDocumentStore([\n ...oracles.records\n .map((record) => record.document)\n .filter((doc) => !(doc.collection === document.collection && doc.id === document.id)),\n document,\n ])\n const validateOptions = bag({\n store,\n localeConfig,\n isPublished: (ref) => published.has(documentKey(ref)),\n findAsset: (id) => assets.get(id),\n })\n return validateDocument(createCollectionRegistry(collections), document, validateOptions)\n }\n}\n"]}
1
+ {"version":3,"file":"runtime-validator.js","sourceRoot":"","sources":["../../../src/lib/operations/runtime-validator.ts"],"names":[],"mappings":"AAiCA,yDAAyD;AACzD,MAAM,UAAU,8BAA8B,CAAC,OAAgC;IAC7E,IAAI,IAA8H,CAAA;IAClI,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;QACjC,IAAI,KAAK,MAAM,CAAC,4BAA4B,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,uBAAuB,EAAE,EAAE,EAAE,CACjF,uBAAuB,CAAC;YACtB,OAAO,EAAE,CAAC,SAAS,EAAE,EAAE;gBACrB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;gBAC5C,OAAO;oBACL,WAAW,EAAE,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC;oBAC3C,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC;oBACxC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAClC,CAAA;YACH,CAAC;SACF,CAAC,CACH,CAAA;QACD,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IACxC,CAAC,CAAA;AACH,CAAC","sourcesContent":["import type { CmsDocument, CmsValidationIssue, LocaleConfig } from '@elytracms/core/cms-core'\nimport type { ComponentRegistry } from '@elytracms/core/component-registry'\nimport type { CollectionsResolver, DocumentValidator } from './operations-package'\n\n/**\n * EC-595 A2 → EC-582 — THE studio publish gate's validator, bound to the OPERATIONS\n * RUNTIME (Codex F23, ruled EC-565 1f): built once per operations bundle, never read from\n * a React mount. Since EC-582 it is core's `createDocumentValidator`\n * (`@elytracms/core/validation` — the same gate a headless embedder and the CLI build):\n * per call it resolves the OPERATION's project — its collections, locales and component\n * registry (`registry(projectId)`: THAT project's manifests, never the active project's —\n * a deployment may serve several projects with different component lists) — prefetches\n * the oracles through the operation's adapter and runs the canonical validator.\n *\n * `@elytracms/core/validation` (the composition leash pulls in the rich-text parser) is\n * imported DYNAMICALLY inside the async validator, so the operations bootstrap's static\n * module graph does not grow by it (measured before/after, EC-595 Done part A2 and EC-582\n * Done).\n *\n * The editor's live validation keeps reading the session (that is display); the GATE\n * reads this. The refusals and their catalog keys (`GATE_*_MESSAGE_KEY`) are declared\n * once, in core.\n */\n\nexport interface RuntimeValidatorOptions {\n /** The resolved config's collections, per project (the operations context's resolver). */\n readonly collections: CollectionsResolver\n /** The project's locale config; `undefined` → the project declares none (a blocking refusal). */\n readonly localeConfig: (projectId: string) => LocaleConfig | undefined\n /** The project's component registry (its manifests); `undefined` → it lists no components. */\n readonly registry: (projectId: string) => ComponentRegistry | undefined\n}\n\n/** Build THE gate validator for an operations bundle. */\nexport function createRuntimeDocumentValidator(options: RuntimeValidatorOptions): DocumentValidator {\n let gate: Promise<(document: CmsDocument, context: Parameters<DocumentValidator>[1]) => Promise<CmsValidationIssue[]>> | undefined\n return async (document, context) => {\n gate ??= import('@elytracms/core/validation').then(({ createDocumentValidator }) =>\n createDocumentValidator({\n project: (projectId) => {\n const registry = options.registry(projectId)\n return {\n collections: options.collections(projectId),\n locales: options.localeConfig(projectId),\n ...(registry ? { registry } : {}),\n }\n },\n }),\n )\n return (await gate)(document, context)\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import type { DocumentListener, IdFactory, PersistenceAdapter, PresenceInput } from '@elytracms/core/persistence';
2
2
  import type { DocumentRef } from '@elytracms/core/cms-core';
3
- import type { ChangesetOperations, CollectionsResolver, DocumentValidator, ManifestLookupResolver, OperationsClient, Principal, ReferenceIndexAttachment } from './operations-package.js';
3
+ import type { ChangesetOperations, CollectionsResolver, DocumentValidator, LocalesResolver, ManifestLookupResolver, OperationsClient, Principal, ReferenceIndexAttachment } from './operations-package.js';
4
4
  /**
5
5
  * The studio's single operations bootstrap (EC-141, vision AD-7).
6
6
  *
@@ -56,7 +56,7 @@ export interface CreateStudioOperationsOptions {
56
56
  /**
57
57
  * EC-583 / EC-559 — the collection definitions on the operations context, resolved for
58
58
  * each operation's OWN project id. Defaults to the workspace config's project with that
59
- * slug (`resolveProjectCollections` — the resolved `elytra.config.ts`, functions
59
+ * slug (`projectCollections` — the resolved `elytra.config.ts`, functions
60
60
  * included), read on every call: the bundle is cached per deployment KEY (EC-179) and
61
61
  * one deployment may serve several config projects, so capturing one project's
62
62
  * collections at build time would canonicalise another project's documents against
@@ -81,6 +81,13 @@ export interface CreateStudioOperationsOptions {
81
81
  * through a mount.
82
82
  */
83
83
  validateDocument?: DocumentValidator;
84
+ /**
85
+ * EC-585 — the locale config on the operations context, per project: the view
86
+ * `documents.publish` runs its hidden-clear in. Defaults to that config project's
87
+ * `locales` (`projectLocaleConfig`) — the same project, read the same way, as the
88
+ * collections, the manifests and the gate's validator.
89
+ */
90
+ locales?: LocalesResolver;
84
91
  }
85
92
  /** Build a studio operations bundle over an adapter (exported for tests). */
86
93
  export declare function createStudioOperations(adapter: PersistenceAdapter, options?: CreateStudioOperationsOptions): StudioOperations;