@elytracms/studio 0.0.80 → 0.0.82

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 (196) hide show
  1. package/convex/contract-parity.test.ts +1 -1
  2. package/convex/functions.ts +13 -3
  3. package/convex/http-routes.test.ts +143 -0
  4. package/convex/http.ts +19 -9
  5. package/convex/redirect-revalidate.test.ts +130 -0
  6. package/convex/revalidate.ts +9 -7
  7. package/convex/schema.ts +0 -19
  8. package/convex/triggers.ts +33 -3
  9. package/convex/users.ts +1 -1
  10. package/dist/components/ai/ai-chat-panel.js +26 -4
  11. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  12. package/dist/components/ai/proposal-card.d.ts +27 -0
  13. package/dist/components/ai/proposal-card.js +31 -3
  14. package/dist/components/ai/proposal-card.js.map +1 -1
  15. package/dist/components/ai/proposal-issues.d.ts +55 -0
  16. package/dist/components/ai/proposal-issues.js +69 -0
  17. package/dist/components/ai/proposal-issues.js.map +1 -0
  18. package/dist/components/ai/translate-document-dialog.js +38 -3
  19. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  20. package/dist/components/app-views/default-modules.d.ts +141 -44
  21. package/dist/components/app-views/default-modules.js +229 -65
  22. package/dist/components/app-views/default-modules.js.map +1 -1
  23. package/dist/components/app-views/module.d.ts +38 -0
  24. package/dist/components/app-views/module.js.map +1 -1
  25. package/dist/components/app-views/sidebar.d.ts +5 -2
  26. package/dist/components/app-views/sidebar.js +16 -5
  27. package/dist/components/app-views/sidebar.js.map +1 -1
  28. package/dist/components/app-views/studio-config.d.ts +28 -1
  29. package/dist/components/app-views/studio-config.js +1 -1
  30. package/dist/components/app-views/studio-config.js.map +1 -1
  31. package/dist/components/builder/canvas-render-context.js +10 -2
  32. package/dist/components/builder/canvas-render-context.js.map +1 -1
  33. package/dist/components/builder/clipboard-actions.js +5 -3
  34. package/dist/components/builder/clipboard-actions.js.map +1 -1
  35. package/dist/components/builder/reuse-context.js +7 -1
  36. package/dist/components/builder/reuse-context.js.map +1 -1
  37. package/dist/components/builder/reuse.d.ts +94 -16
  38. package/dist/components/builder/reuse.js +201 -19
  39. package/dist/components/builder/reuse.js.map +1 -1
  40. package/dist/components/cms/bulk-actions.js +10 -5
  41. package/dist/components/cms/bulk-actions.js.map +1 -1
  42. package/dist/components/cms/cell-edit.d.ts +18 -3
  43. package/dist/components/cms/cell-edit.js +29 -7
  44. package/dist/components/cms/cell-edit.js.map +1 -1
  45. package/dist/components/cms/cms-store.d.ts +54 -14
  46. package/dist/components/cms/cms-store.js +142 -45
  47. package/dist/components/cms/cms-store.js.map +1 -1
  48. package/dist/components/cms/collection-table.js +2 -2
  49. package/dist/components/cms/collection-table.js.map +1 -1
  50. package/dist/components/cms/detail-editor.js +15 -14
  51. package/dist/components/cms/detail-editor.js.map +1 -1
  52. package/dist/components/cms/document-editor-context.d.ts +12 -1
  53. package/dist/components/cms/document-editor-context.js.map +1 -1
  54. package/dist/components/cms/field-control.js +22 -8
  55. package/dist/components/cms/field-control.js.map +1 -1
  56. package/dist/components/cms/field-renderer.js +8 -1
  57. package/dist/components/cms/field-renderer.js.map +1 -1
  58. package/dist/components/cms/gate-validation.d.ts +36 -0
  59. package/dist/components/cms/gate-validation.js +17 -0
  60. package/dist/components/cms/gate-validation.js.map +1 -0
  61. package/dist/components/cms/import-export.js +2 -1
  62. package/dist/components/cms/import-export.js.map +1 -1
  63. package/dist/components/cms/inline-field-errors.d.ts +8 -0
  64. package/dist/components/cms/inline-field-errors.js +52 -0
  65. package/dist/components/cms/inline-field-errors.js.map +1 -0
  66. package/dist/components/cms/issues-pane.d.ts +4 -2
  67. package/dist/components/cms/issues-pane.js +13 -7
  68. package/dist/components/cms/issues-pane.js.map +1 -1
  69. package/dist/components/cms/slug-collision-report.d.ts +41 -0
  70. package/dist/components/cms/slug-collision-report.js +52 -0
  71. package/dist/components/cms/slug-collision-report.js.map +1 -0
  72. package/dist/components/components/transfer-dialog.js +10 -12
  73. package/dist/components/components/transfer-dialog.js.map +1 -1
  74. package/dist/components/deployment-setup-gate.d.ts +24 -0
  75. package/dist/components/deployment-setup-gate.js +29 -4
  76. package/dist/components/deployment-setup-gate.js.map +1 -1
  77. package/dist/components/issue-severity.d.ts +73 -0
  78. package/dist/components/issue-severity.js +56 -0
  79. package/dist/components/issue-severity.js.map +1 -0
  80. package/dist/components/nav-issues.d.ts +68 -0
  81. package/dist/components/nav-issues.js +78 -0
  82. package/dist/components/nav-issues.js.map +1 -0
  83. package/dist/components/operation-error.js +3 -3
  84. package/dist/components/operation-error.js.map +1 -1
  85. package/dist/components/project-list.d.ts +9 -3
  86. package/dist/components/project-list.js +11 -5
  87. package/dist/components/project-list.js.map +1 -1
  88. package/dist/components/project-switcher.d.ts +3 -1
  89. package/dist/components/project-switcher.js +4 -2
  90. package/dist/components/project-switcher.js.map +1 -1
  91. package/dist/components/redirects/redirects-store.d.ts +21 -0
  92. package/dist/components/redirects/redirects-store.js +17 -4
  93. package/dist/components/redirects/redirects-store.js.map +1 -1
  94. package/dist/components/section-nav.js +75 -10
  95. package/dist/components/section-nav.js.map +1 -1
  96. package/dist/customization.d.ts +7 -0
  97. package/dist/customization.js.map +1 -1
  98. package/dist/lib/ai/apply-content.js +6 -7
  99. package/dist/lib/ai/apply-content.js.map +1 -1
  100. package/dist/lib/ai/chat-context.d.ts +32 -26
  101. package/dist/lib/ai/chat-context.js +81 -9
  102. package/dist/lib/ai/chat-context.js.map +1 -1
  103. package/dist/lib/ai/constraints.d.ts +42 -0
  104. package/dist/lib/ai/constraints.js +259 -0
  105. package/dist/lib/ai/constraints.js.map +1 -0
  106. package/dist/lib/ai/index.d.ts +2 -0
  107. package/dist/lib/ai/index.js +2 -0
  108. package/dist/lib/ai/index.js.map +1 -1
  109. package/dist/lib/ai/proposal-validation.d.ts +26 -0
  110. package/dist/lib/ai/proposal-validation.js +17 -0
  111. package/dist/lib/ai/proposal-validation.js.map +1 -0
  112. package/dist/lib/ai/shape.js +60 -13
  113. package/dist/lib/ai/shape.js.map +1 -1
  114. package/dist/lib/ai/tools.d.ts +8 -4
  115. package/dist/lib/ai/tools.js +43 -14
  116. package/dist/lib/ai/tools.js.map +1 -1
  117. package/dist/lib/ai/translate-document.d.ts +35 -1
  118. package/dist/lib/ai/translate-document.js +16 -1
  119. package/dist/lib/ai/translate-document.js.map +1 -1
  120. package/dist/lib/ai/types.d.ts +11 -6
  121. package/dist/lib/ai/types.js.map +1 -1
  122. package/dist/lib/ai/update-fields.d.ts +8 -6
  123. package/dist/lib/ai/update-fields.js +9 -22
  124. package/dist/lib/ai/update-fields.js.map +1 -1
  125. package/dist/lib/clipboard/paste-issues.d.ts +5 -0
  126. package/dist/lib/clipboard/paste-issues.js +16 -6
  127. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  128. package/dist/lib/cms/document-gate.d.ts +37 -0
  129. package/dist/lib/cms/document-gate.js +40 -0
  130. package/dist/lib/cms/document-gate.js.map +1 -0
  131. package/dist/lib/cms-views/view-model.d.ts +5 -0
  132. package/dist/lib/cms-views/view-model.js +10 -4
  133. package/dist/lib/cms-views/view-model.js.map +1 -1
  134. package/dist/lib/data/collection-content.d.ts +6 -1
  135. package/dist/lib/data/collection-content.js +51 -15
  136. package/dist/lib/data/collection-content.js.map +1 -1
  137. package/dist/lib/i18n/i18n.d.ts +51 -16
  138. package/dist/lib/i18n/i18n.js +119 -43
  139. package/dist/lib/i18n/i18n.js.map +1 -1
  140. package/dist/lib/i18n/index.d.ts +8 -1
  141. package/dist/lib/i18n/index.js +1 -1
  142. package/dist/lib/i18n/index.js.map +1 -1
  143. package/dist/lib/i18n/messages.de.js +200 -0
  144. package/dist/lib/i18n/messages.de.js.map +1 -1
  145. package/dist/lib/i18n/messages.en.d.ts +162 -0
  146. package/dist/lib/i18n/messages.en.js +226 -0
  147. package/dist/lib/i18n/messages.en.js.map +1 -1
  148. package/dist/lib/i18n/types.d.ts +15 -9
  149. package/dist/lib/i18n/types.js.map +1 -1
  150. package/dist/lib/locale/binding-fallback.d.ts +8 -1
  151. package/dist/lib/locale/binding-fallback.js +8 -1
  152. package/dist/lib/locale/binding-fallback.js.map +1 -1
  153. package/dist/lib/locale/locale-store.d.ts +41 -9
  154. package/dist/lib/locale/locale-store.js +34 -15
  155. package/dist/lib/locale/locale-store.js.map +1 -1
  156. package/dist/lib/locale/locale-switcher.d.ts +3 -1
  157. package/dist/lib/locale/locale-switcher.js +4 -2
  158. package/dist/lib/locale/locale-switcher.js.map +1 -1
  159. package/dist/lib/locale/use-studio-locale.d.ts +9 -4
  160. package/dist/lib/locale/use-studio-locale.js.map +1 -1
  161. package/dist/lib/operations/studio-operations.js +19 -2
  162. package/dist/lib/operations/studio-operations.js.map +1 -1
  163. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  164. package/dist/lib/persistence/convex-adapter.js +0 -55
  165. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  166. package/dist/lib/persistence/deployment-readiness.js +10 -0
  167. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  168. package/dist/lib/persona/labels.d.ts +85 -9
  169. package/dist/lib/persona/labels.js +198 -58
  170. package/dist/lib/persona/labels.js.map +1 -1
  171. package/dist/lib/routes/route-issues.d.ts +9 -0
  172. package/dist/lib/routes/route-issues.js +117 -0
  173. package/dist/lib/routes/route-issues.js.map +1 -0
  174. package/dist/lib/webhooks/dispatcher.d.ts +5 -2
  175. package/dist/lib/webhooks/dispatcher.js.map +1 -1
  176. package/dist/lib/webhooks/tags.js +150 -17
  177. package/dist/lib/webhooks/tags.js.map +1 -1
  178. package/dist/lib/workspace/config-issues.d.ts +56 -0
  179. package/dist/lib/workspace/config-issues.js +72 -0
  180. package/dist/lib/workspace/config-issues.js.map +1 -0
  181. package/dist/lib/workspace/index.d.ts +24 -8
  182. package/dist/lib/workspace/index.js +28 -18
  183. package/dist/lib/workspace/index.js.map +1 -1
  184. package/dist/lib/workspace/switcher-source.d.ts +20 -3
  185. package/dist/lib/workspace/switcher-source.js.map +1 -1
  186. package/dist/lib/workspace/workspace-source.js +27 -0
  187. package/dist/lib/workspace/workspace-source.js.map +1 -1
  188. package/dist/previews/manifest-format.d.ts +13 -6
  189. package/dist/previews/manifest-format.js +9 -2
  190. package/dist/previews/manifest-format.js.map +1 -1
  191. package/dist/router/root.d.ts +36 -1
  192. package/dist/router/root.js +54 -7
  193. package/dist/router/root.js.map +1 -1
  194. package/package.json +2 -2
  195. package/convex/cliTokens.ts +0 -63
  196. package/convex/sync.ts +0 -297
@@ -1,4 +1,4 @@
1
- import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, FieldDef, Locale, LocaleConfig } from '@elytracms/core/cms-core';
1
+ import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig } from '@elytracms/core/cms-core';
2
2
  import type { OperationError, OperationResult } from '@elytracms/studio/lib/operations';
3
3
  /**
4
4
  * Inline cell editing for the collection table (EC-168) — the pure half.
@@ -42,8 +42,14 @@ export declare function applyCellValue(collection: CollectionDef, doc: CmsDocume
42
42
  * Error-severity issues that the edited cell itself caused. Localized fields
43
43
  * only reject on issues for the edited locale — a missing translation in some
44
44
  * *other* locale must not block this commit.
45
+ *
46
+ * EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +
47
+ * `localeConfig` subset it used to assemble here. A cell edit is not a weaker
48
+ * KIND of validation — it is the same validation, FILTERED to the cell being
49
+ * committed, which is the whole option-set contract in one function: surfaces
50
+ * differ in what they do with the issues, never in what they validated with.
45
51
  */
46
- export declare function cellEditIssues(registry: CollectionRegistry, documents: readonly CmsDocument[], localeConfig: LocaleConfig, collection: CollectionDef, next: CmsDocument, fieldName: string, locale: Locale): CmsValidationIssue[];
52
+ export declare function cellEditIssues(registry: CollectionRegistry, documents: readonly CmsDocument[], localeConfig: LocaleConfig, collection: CollectionDef, next: CmsDocument, fieldName: string, locale: Locale, oracles?: CellEditOracles): CmsValidationIssue[];
47
53
  /** Outcome of one cell commit: the new document, or a structured rejection. */
48
54
  export type CellCommitOutcome = {
49
55
  ok: true;
@@ -62,7 +68,16 @@ export interface CellCommitInput {
62
68
  /** The committed draft value (`undefined` clears the field). */
63
69
  value: unknown;
64
70
  }
65
- export interface CellCommitContext {
71
+ /**
72
+ * The session oracles the canonical option bag needs beyond the store + locales
73
+ * (EC-497). Optional so a fixture-only caller still gets the rest of the bag;
74
+ * absent `isPublished` means "nothing is published", the engine's own default.
75
+ */
76
+ export interface CellEditOracles {
77
+ isPublished?: (ref: DocumentRef) => boolean;
78
+ assetExists?: (id: string) => boolean;
79
+ }
80
+ export interface CellCommitContext extends CellEditOracles {
66
81
  registry: CollectionRegistry;
67
82
  /** Current session documents (relation/asset target resolution). */
68
83
  documents: readonly CmsDocument[];
@@ -1,5 +1,7 @@
1
1
  import { createDocumentStore, isFieldLocalized, validateDocument } from '@elytracms/core/cms-core';
2
+ import { gateValidateOptions } from './gate-validation.js';
2
3
  import { replaceDoc } from './document-ref-utils.js';
4
+ import { cmsValidationIssueMessage, operationErrorMessage, operationIssueMessage, } from '@elytracms/studio/lib/persona/labels';
3
5
  /** Stable key for a cell (state maps, DOM lookups). */
4
6
  export function cellKey(coord) {
5
7
  return `${coord.docId}::${coord.field}`;
@@ -70,12 +72,23 @@ export function applyCellValue(collection, doc, fieldName, locale, value) {
70
72
  * Error-severity issues that the edited cell itself caused. Localized fields
71
73
  * only reject on issues for the edited locale — a missing translation in some
72
74
  * *other* locale must not block this commit.
75
+ *
76
+ * EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +
77
+ * `localeConfig` subset it used to assemble here. A cell edit is not a weaker
78
+ * KIND of validation — it is the same validation, FILTERED to the cell being
79
+ * committed, which is the whole option-set contract in one function: surfaces
80
+ * differ in what they do with the issues, never in what they validated with.
73
81
  */
74
- export function cellEditIssues(registry, documents, localeConfig, collection, next, fieldName, locale) {
82
+ export function cellEditIssues(registry, documents, localeConfig, collection, next, fieldName, locale, oracles = {}) {
75
83
  // Validate against the session with the edited document swapped in, so
76
84
  // self-relations and same-document targets resolve.
77
85
  const store = createDocumentStore(replaceDoc(documents, next));
78
- const issues = validateDocument(registry, next, { store, localeConfig });
86
+ const issues = validateDocument(registry, next, gateValidateOptions({
87
+ store,
88
+ localeConfig,
89
+ isPublished: oracles.isPublished ?? (() => false),
90
+ ...(oracles.assetExists ? { assetExists: oracles.assetExists } : {}),
91
+ }));
79
92
  const localized = isFieldLocalized(collection, fieldName);
80
93
  return issues.filter((issue) => {
81
94
  if (issue.severity !== 'error')
@@ -99,16 +112,22 @@ export function cellEditIssues(registry, documents, localeConfig, collection, ne
99
112
  */
100
113
  export async function commitCellEdit(context, input) {
101
114
  const next = applyCellValue(input.collection, input.doc, input.field.name, input.locale, input.value);
102
- const issues = cellEditIssues(context.registry, context.documents, context.localeConfig, input.collection, next, input.field.name, input.locale);
115
+ const issues = cellEditIssues(context.registry, context.documents, context.localeConfig, input.collection, next, input.field.name, input.locale, {
116
+ ...(context.isPublished ? { isPublished: context.isPublished } : {}),
117
+ ...(context.assetExists ? { assetExists: context.assetExists } : {}),
118
+ });
103
119
  if (issues.length > 0) {
104
120
  return {
105
121
  ok: false,
106
122
  error: {
107
123
  code: 'validation',
108
- message: issues[0].message,
124
+ // EC-487/EC-494: this producer is CLIENT-side and already holds the structured
125
+ // issue, so it resolves the message here. (Since EC-494 the wire also carries
126
+ // the ref, which is what makes the SERVER-side publish gate localize.)
127
+ message: cmsValidationIssueMessage(issues[0]),
109
128
  issues: issues.map((issue) => ({
110
129
  code: issue.code,
111
- message: issue.message,
130
+ message: cmsValidationIssueMessage(issue),
112
131
  path: [...issue.path],
113
132
  domain: 'cms',
114
133
  })),
@@ -138,8 +157,11 @@ export async function commitCellEdit(context, input) {
138
157
  }
139
158
  /** The message the cell renders for a rejection: the first issue beats the envelope. */
140
159
  export function cellErrorMessage(error) {
160
+ // EC-494: an OperationIssue now carries its catalog ref across the wire, so a
161
+ // rejection built SERVER-SIDE (the publish gate) localizes here too — not only the
162
+ // studio-built ones above.
141
163
  if (error.code === 'validation' && error.issues.length > 0)
142
- return error.issues[0].message;
143
- return error.message;
164
+ return operationIssueMessage(error.issues[0]);
165
+ return operationErrorMessage(error);
144
166
  }
145
167
  //# sourceMappingURL=cell-edit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/cell-edit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAWlG,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAoBjD,uDAAuD;AACvD,MAAM,UAAU,OAAO,CAAC,KAAgB;IACtC,OAAO,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAA;AACzC,CAAC;AAOD,MAAM,YAAY,GAAkC,IAAI,GAAG,CAAC;IAC1D,MAAM;IACN,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,MAAM;CACP,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAe,EAAE,QAAiB;IAC7D,IAAI,QAAQ;QAAE,OAAO,MAAM,CAAA;IAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,QAAQ,CAAA;IACxE,OAAO,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAA;AACzD,CAAC;AAOD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAyB,EACzB,MAAyB,EACzB,OAAkB,EAClB,SAA2B;IAE3B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACzC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACzC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1D,IAAI,SAAS,KAAK,MAAM,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;QAC/B,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAA;IAChF,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACxE,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAClE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAE,CAAA;IACvD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAE,EAAE,CAAA;AACxD,CAAC;AAED,kFAAkF;AAElF;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,UAAyB,EACzB,GAAgB,EAChB,SAAiB,EACjB,MAAc,EACd,KAAc;IAEd,IAAI,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;QACxD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC,SAAS,CAAC,CAAA;;YAC/C,SAAS,CAAC,SAAS,CAAC,GAAG,KAAK,CAAA;QACjC,OAAO,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,GAAG,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,CAAA;IACzE,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,CAAA;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC,CAAA;;QAC5C,MAAM,CAAC,SAAS,CAAC,GAAG,KAAK,CAAA;IAC9B,OAAO,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,CAAA;AAC3B,CAAC;AAED,kFAAkF;AAElF;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,QAA4B,EAC5B,SAAiC,EACjC,YAA0B,EAC1B,UAAyB,EACzB,IAAiB,EACjB,SAAiB,EACjB,MAAc;IAEd,uEAAuE;IACvE,oDAAoD;IACpD,MAAM,KAAK,GAAG,mBAAmB,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;IACxE,MAAM,SAAS,GAAG,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IACzD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QAC7B,IAAI,KAAK,CAAC,QAAQ,KAAK,OAAO;YAAE,OAAO,KAAK,CAAA;QAC5C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACjD,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAA;QAC3B,mFAAmF;QACnF,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAC1C,OAAO,EAAE,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,MAAM,CAAA;IAChD,CAAC,CAAC,CAAA;AACJ,CAAC;AAqCD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,OAA0B,EAC1B,KAAsB;IAEtB,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;IAErG,MAAM,MAAM,GAAG,cAAc,CAC3B,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,YAAY,EACpB,KAAK,CAAC,UAAU,EAChB,IAAI,EACJ,KAAK,CAAC,KAAK,CAAC,IAAI,EAChB,KAAK,CAAC,MAAM,CACb,CAAA;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAE,CAAC,OAAO;gBAC3B,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC;oBACrB,MAAM,EAAE,KAAK;iBACd,CAAC,CAAC;aACJ;SACF,CAAA;IACH,CAAC;IAED,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IAC1B,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,IAAI,MAAgC,CAAA;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,wEAAwE;YACxE,4DAA4D;YAC5D,MAAM,GAAG;gBACP,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;aAC5F,CAAA;QACH,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAClC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;QAC3C,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;AAChC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,gBAAgB,CAAC,KAAqB;IACpD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,OAAO,CAAA;IAC3F,OAAO,KAAK,CAAC,OAAO,CAAA;AACtB,CAAC","sourcesContent":["import { createDocumentStore, isFieldLocalized, validateDocument } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { replaceDoc } from './document-ref-utils'\n\n/**\n * Inline cell editing for the collection table (EC-168) — the pure half.\n *\n * Everything here is plain data + functions so the cell-edit state machine is\n * testable without a DOM: which cells are editable, how Tab/Enter advance,\n * how a draft value is folded into a document (active locale only), and how a\n * commit flows through exactly one `documents.upsert` operations command with\n * optimistic apply + rollback on rejection.\n */\n\n// --- Cell identity -----------------------------------------------------------\n\n/** A cell in the collection table: one document × one field column. */\nexport interface CellCoord {\n docId: string\n field: string\n}\n\n/** Stable key for a cell (state maps, DOM lookups). */\nexport function cellKey(coord: CellCoord): string {\n return `${coord.docId}::${coord.field}`\n}\n\n// --- Editability -------------------------------------------------------------\n\n/** How a cell edits: inline control, anchored picker dialog, or not at all. */\nexport type CellEditKind = 'inline' | 'picker' | 'none'\n\nconst INLINE_TYPES: ReadonlySet<FieldDef['type']> = new Set([\n 'text',\n 'number',\n 'boolean',\n 'select',\n 'date',\n])\n\n/**\n * Classify a field's in-table editing surface. Rich text is deliberately not\n * inline-editable (the detail editor owns it); read-only fields never edit.\n */\nexport function cellEditKind(field: FieldDef, readOnly: boolean): CellEditKind {\n if (readOnly) return 'none'\n if (field.type === 'relation' || field.type === 'asset') return 'picker'\n return INLINE_TYPES.has(field.type) ? 'inline' : 'none'\n}\n\n// --- Keyboard advance --------------------------------------------------------\n\n/** Where a committed edit moves next: Tab → next, Shift-Tab → prev, Enter → down. */\nexport type AdvanceDirection = 'next' | 'prev' | 'down'\n\n/**\n * The next editable cell in the table grid, or `null` at a boundary.\n * `docIds` is the rendered row order; `fields` the editable column order.\n * 'next'/'prev' walk row-major (wrapping across rows); 'down' stays in the\n * same column.\n */\nexport function nextEditableCell(\n docIds: readonly string[],\n fields: readonly string[],\n current: CellCoord,\n direction: AdvanceDirection,\n): CellCoord | null {\n const row = docIds.indexOf(current.docId)\n const col = fields.indexOf(current.field)\n if (row < 0 || col < 0 || fields.length === 0) return null\n if (direction === 'down') {\n const nextDoc = docIds[row + 1]\n return nextDoc === undefined ? null : { docId: nextDoc, field: current.field }\n }\n const flat = row * fields.length + col + (direction === 'next' ? 1 : -1)\n if (flat < 0 || flat >= docIds.length * fields.length) return null\n const docId = docIds[Math.floor(flat / fields.length)]!\n return { docId, field: fields[flat % fields.length]! }\n}\n\n// --- Applying a draft value ----------------------------------------------------\n\n/**\n * Fold one field value into a document. Localized fields write ONLY the given\n * locale's bag (other locale variants untouched, EC-018); non-localized fields\n * write the shared `values` bag. `undefined` clears the entry.\n */\nexport function applyCellValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n value: unknown,\n): CmsDocument {\n if (isFieldLocalized(collection, fieldName)) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n// --- Validation gate -----------------------------------------------------------\n\n/**\n * Error-severity issues that the edited cell itself caused. Localized fields\n * only reject on issues for the edited locale — a missing translation in some\n * *other* locale must not block this commit.\n */\nexport function cellEditIssues(\n registry: CollectionRegistry,\n documents: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n collection: CollectionDef,\n next: CmsDocument,\n fieldName: string,\n locale: Locale,\n): CmsValidationIssue[] {\n // Validate against the session with the edited document swapped in, so\n // self-relations and same-document targets resolve.\n const store = createDocumentStore(replaceDoc(documents, next))\n const issues = validateDocument(registry, next, { store, localeConfig })\n const localized = isFieldLocalized(collection, fieldName)\n return issues.filter((issue) => {\n if (issue.severity !== 'error') return false\n if (issue.meta?.field !== fieldName) return false\n if (!localized) return true\n // Localized field paths are ['documents', key, 'localized', <locale>, <field>, …].\n const at = issue.path.indexOf('localized')\n return at < 0 || issue.path[at + 1] === locale\n })\n}\n\n// --- Commit pipeline -------------------------------------------------------------\n\n/** Outcome of one cell commit: the new document, or a structured rejection. */\nexport type CellCommitOutcome =\n | { ok: true; doc: CmsDocument }\n | { ok: false; error: OperationError }\n\nexport interface CellCommitInput {\n collection: CollectionDef\n /** The document as currently rendered (the rollback target). */\n doc: CmsDocument\n field: FieldDef\n /** The active locale at the point of edit (table locale resolution). */\n locale: Locale\n /** The committed draft value (`undefined` clears the field). */\n value: unknown\n}\n\nexport interface CellCommitContext {\n registry: CollectionRegistry\n /** Current session documents (relation/asset target resolution). */\n documents: readonly CmsDocument[]\n localeConfig: LocaleConfig\n /** Optimistically apply the edited document to the session. */\n applySession: (next: CmsDocument) => void\n /** Restore the pre-edit document after a rejected dispatch. */\n rollbackSession: (previous: CmsDocument) => void\n /**\n * Dispatch the ONE operations command for this commit\n * (`documents.upsert`, EC-141). Absent in fixture-only sessions\n * (no project scope) — the commit is session-only then.\n */\n dispatch?: (next: CmsDocument) => Promise<OperationResult<unknown>>\n}\n\n/**\n * Commit one cell edit:\n * 1. fold the value into the document (active locale only),\n * 2. gate on field validation — rejected edits dispatch NOTHING and report a\n * structured validation error for the cell,\n * 3. apply optimistically, dispatch exactly one operations command,\n * 4. roll the session back when the operation result is a rejection.\n */\nexport async function commitCellEdit(\n context: CellCommitContext,\n input: CellCommitInput,\n): Promise<CellCommitOutcome> {\n const next = applyCellValue(input.collection, input.doc, input.field.name, input.locale, input.value)\n\n const issues = cellEditIssues(\n context.registry,\n context.documents,\n context.localeConfig,\n input.collection,\n next,\n input.field.name,\n input.locale,\n )\n if (issues.length > 0) {\n return {\n ok: false,\n error: {\n code: 'validation',\n message: issues[0]!.message,\n issues: issues.map((issue) => ({\n code: issue.code,\n message: issue.message,\n path: [...issue.path],\n domain: 'cms',\n })),\n },\n }\n }\n\n context.applySession(next)\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(next)\n } catch (cause) {\n // Operations never throw by contract; treat a broken transport the same\n // as a failed command so the cell still rolls back visibly.\n result = {\n ok: false,\n error: { code: 'unknown', message: cause instanceof Error ? cause.message : String(cause) },\n }\n }\n if (!result.ok) {\n context.rollbackSession(input.doc)\n return { ok: false, error: result.error }\n }\n }\n return { ok: true, doc: next }\n}\n\n/** The message the cell renders for a rejection: the first issue beats the envelope. */\nexport function cellErrorMessage(error: OperationError): string {\n if (error.code === 'validation' && error.issues.length > 0) return error.issues[0]!.message\n return error.message\n}\n"]}
1
+ {"version":3,"file":"cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { createDocumentStore, isFieldLocalized, validateDocument } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { gateValidateOptions } from './gate-validation'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { replaceDoc } from './document-ref-utils'\nimport {\n cmsValidationIssueMessage,\n operationErrorMessage,\n operationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Inline cell editing for the collection table (EC-168) — the pure half.\n *\n * Everything here is plain data + functions so the cell-edit state machine is\n * testable without a DOM: which cells are editable, how Tab/Enter advance,\n * how a draft value is folded into a document (active locale only), and how a\n * commit flows through exactly one `documents.upsert` operations command with\n * optimistic apply + rollback on rejection.\n */\n\n// --- Cell identity -----------------------------------------------------------\n\n/** A cell in the collection table: one document × one field column. */\nexport interface CellCoord {\n docId: string\n field: string\n}\n\n/** Stable key for a cell (state maps, DOM lookups). */\nexport function cellKey(coord: CellCoord): string {\n return `${coord.docId}::${coord.field}`\n}\n\n// --- Editability -------------------------------------------------------------\n\n/** How a cell edits: inline control, anchored picker dialog, or not at all. */\nexport type CellEditKind = 'inline' | 'picker' | 'none'\n\nconst INLINE_TYPES: ReadonlySet<FieldDef['type']> = new Set([\n 'text',\n 'number',\n 'boolean',\n 'select',\n 'date',\n])\n\n/**\n * Classify a field's in-table editing surface. Rich text is deliberately not\n * inline-editable (the detail editor owns it); read-only fields never edit.\n */\nexport function cellEditKind(field: FieldDef, readOnly: boolean): CellEditKind {\n if (readOnly) return 'none'\n if (field.type === 'relation' || field.type === 'asset') return 'picker'\n return INLINE_TYPES.has(field.type) ? 'inline' : 'none'\n}\n\n// --- Keyboard advance --------------------------------------------------------\n\n/** Where a committed edit moves next: Tab → next, Shift-Tab → prev, Enter → down. */\nexport type AdvanceDirection = 'next' | 'prev' | 'down'\n\n/**\n * The next editable cell in the table grid, or `null` at a boundary.\n * `docIds` is the rendered row order; `fields` the editable column order.\n * 'next'/'prev' walk row-major (wrapping across rows); 'down' stays in the\n * same column.\n */\nexport function nextEditableCell(\n docIds: readonly string[],\n fields: readonly string[],\n current: CellCoord,\n direction: AdvanceDirection,\n): CellCoord | null {\n const row = docIds.indexOf(current.docId)\n const col = fields.indexOf(current.field)\n if (row < 0 || col < 0 || fields.length === 0) return null\n if (direction === 'down') {\n const nextDoc = docIds[row + 1]\n return nextDoc === undefined ? null : { docId: nextDoc, field: current.field }\n }\n const flat = row * fields.length + col + (direction === 'next' ? 1 : -1)\n if (flat < 0 || flat >= docIds.length * fields.length) return null\n const docId = docIds[Math.floor(flat / fields.length)]!\n return { docId, field: fields[flat % fields.length]! }\n}\n\n// --- Applying a draft value ----------------------------------------------------\n\n/**\n * Fold one field value into a document. Localized fields write ONLY the given\n * locale's bag (other locale variants untouched, EC-018); non-localized fields\n * write the shared `values` bag. `undefined` clears the entry.\n */\nexport function applyCellValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n value: unknown,\n): CmsDocument {\n if (isFieldLocalized(collection, fieldName)) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n// --- Validation gate -----------------------------------------------------------\n\n/**\n * Error-severity issues that the edited cell itself caused. Localized fields\n * only reject on issues for the edited locale — a missing translation in some\n * *other* locale must not block this commit.\n *\n * EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +\n * `localeConfig` subset it used to assemble here. A cell edit is not a weaker\n * KIND of validation — it is the same validation, FILTERED to the cell being\n * committed, which is the whole option-set contract in one function: surfaces\n * differ in what they do with the issues, never in what they validated with.\n */\nexport function cellEditIssues(\n registry: CollectionRegistry,\n documents: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n collection: CollectionDef,\n next: CmsDocument,\n fieldName: string,\n locale: Locale,\n oracles: CellEditOracles = {},\n): CmsValidationIssue[] {\n // Validate against the session with the edited document swapped in, so\n // self-relations and same-document targets resolve.\n const store = createDocumentStore(replaceDoc(documents, next))\n const issues = validateDocument(\n registry,\n next,\n gateValidateOptions({\n store,\n localeConfig,\n isPublished: oracles.isPublished ?? (() => false),\n ...(oracles.assetExists ? { assetExists: oracles.assetExists } : {}),\n }),\n )\n const localized = isFieldLocalized(collection, fieldName)\n return issues.filter((issue) => {\n if (issue.severity !== 'error') return false\n if (issue.meta?.field !== fieldName) return false\n if (!localized) return true\n // Localized field paths are ['documents', key, 'localized', <locale>, <field>, …].\n const at = issue.path.indexOf('localized')\n return at < 0 || issue.path[at + 1] === locale\n })\n}\n\n// --- Commit pipeline -------------------------------------------------------------\n\n/** Outcome of one cell commit: the new document, or a structured rejection. */\nexport type CellCommitOutcome =\n | { ok: true; doc: CmsDocument }\n | { ok: false; error: OperationError }\n\nexport interface CellCommitInput {\n collection: CollectionDef\n /** The document as currently rendered (the rollback target). */\n doc: CmsDocument\n field: FieldDef\n /** The active locale at the point of edit (table locale resolution). */\n locale: Locale\n /** The committed draft value (`undefined` clears the field). */\n value: unknown\n}\n\n/**\n * The session oracles the canonical option bag needs beyond the store + locales\n * (EC-497). Optional so a fixture-only caller still gets the rest of the bag;\n * absent `isPublished` means \"nothing is published\", the engine's own default.\n */\nexport interface CellEditOracles {\n isPublished?: (ref: DocumentRef) => boolean\n assetExists?: (id: string) => boolean\n}\n\nexport interface CellCommitContext extends CellEditOracles {\n registry: CollectionRegistry\n /** Current session documents (relation/asset target resolution). */\n documents: readonly CmsDocument[]\n localeConfig: LocaleConfig\n /** Optimistically apply the edited document to the session. */\n applySession: (next: CmsDocument) => void\n /** Restore the pre-edit document after a rejected dispatch. */\n rollbackSession: (previous: CmsDocument) => void\n /**\n * Dispatch the ONE operations command for this commit\n * (`documents.upsert`, EC-141). Absent in fixture-only sessions\n * (no project scope) — the commit is session-only then.\n */\n dispatch?: (next: CmsDocument) => Promise<OperationResult<unknown>>\n}\n\n/**\n * Commit one cell edit:\n * 1. fold the value into the document (active locale only),\n * 2. gate on field validation — rejected edits dispatch NOTHING and report a\n * structured validation error for the cell,\n * 3. apply optimistically, dispatch exactly one operations command,\n * 4. roll the session back when the operation result is a rejection.\n */\nexport async function commitCellEdit(\n context: CellCommitContext,\n input: CellCommitInput,\n): Promise<CellCommitOutcome> {\n const next = applyCellValue(input.collection, input.doc, input.field.name, input.locale, input.value)\n\n const issues = cellEditIssues(\n context.registry,\n context.documents,\n context.localeConfig,\n input.collection,\n next,\n input.field.name,\n input.locale,\n {\n ...(context.isPublished ? { isPublished: context.isPublished } : {}),\n ...(context.assetExists ? { assetExists: context.assetExists } : {}),\n },\n )\n if (issues.length > 0) {\n return {\n ok: false,\n error: {\n code: 'validation',\n // EC-487/EC-494: this producer is CLIENT-side and already holds the structured\n // issue, so it resolves the message here. (Since EC-494 the wire also carries\n // the ref, which is what makes the SERVER-side publish gate localize.)\n message: cmsValidationIssueMessage(issues[0]!),\n issues: issues.map((issue) => ({\n code: issue.code,\n message: cmsValidationIssueMessage(issue),\n path: [...issue.path],\n domain: 'cms',\n })),\n },\n }\n }\n\n context.applySession(next)\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(next)\n } catch (cause) {\n // Operations never throw by contract; treat a broken transport the same\n // as a failed command so the cell still rolls back visibly.\n result = {\n ok: false,\n error: { code: 'unknown', message: cause instanceof Error ? cause.message : String(cause) },\n }\n }\n if (!result.ok) {\n context.rollbackSession(input.doc)\n return { ok: false, error: result.error }\n }\n }\n return { ok: true, doc: next }\n}\n\n/** The message the cell renders for a rejection: the first issue beats the envelope. */\nexport function cellErrorMessage(error: OperationError): string {\n // EC-494: an OperationIssue now carries its catalog ref across the wire, so a\n // rejection built SERVER-SIDE (the publish gate) localizes here too — not only the\n // studio-built ones above.\n if (error.code === 'validation' && error.issues.length > 0)\n return operationIssueMessage(error.issues[0]!)\n return operationErrorMessage(error)\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope } from '@elytracms/core/cms-core';
2
+ import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope, ValidateDocumentOptions } from '@elytracms/core/cms-core';
3
3
  import type { DocumentVersionRecord } from '@elytracms/core/persistence';
4
4
  import type { OperationError } from '@elytracms/studio/lib/operations';
5
5
  import type { TableViewState } from '@elytracms/studio/lib/cms-views';
@@ -134,6 +134,19 @@ export interface CmsStore {
134
134
  restoreDocumentVersion(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>;
135
135
  importDocuments(collectionId: string, docs: unknown[]): ImportReport;
136
136
  validate(doc: CmsDocument): CmsValidationIssue[];
137
+ /**
138
+ * THE validation option bag {@link validate} uses (EC-497) — the session document
139
+ * store, the project locales, the composition + rich-text-embed leashes, the
140
+ * published-ness pointer and the real asset-library oracle, built once by
141
+ * `gateValidateOptions`.
142
+ *
143
+ * Exposed so a surface validating a document that is NOT yet in the session (the
144
+ * AI engines judge a proposal before it exists) passes the editor's rules WHOLE.
145
+ * EC-473 exposed the two store-dependent halves for that; handing over the bag
146
+ * instead removes the remaining way to get it wrong — assembling a subset at the
147
+ * call site, which is the defect EC-497 exists to end.
148
+ */
149
+ validateOptions: ValidateDocumentOptions;
137
150
  isFieldReadOnly(field: FieldDef): boolean;
138
151
  viewsOf(collectionId: string): SavedView[];
139
152
  activeViewId(collectionId: string): string;
@@ -211,14 +224,7 @@ export declare function buildNewDocument(collectionId: string, registry: Collect
211
224
  * can never materialize a second singleton document.
212
225
  */
213
226
  export declare function hasDocumentFor(docs: readonly CmsDocument[], collectionId: string): boolean;
214
- /**
215
- * The CMS session store (EC-141): documents and collections are optimistic,
216
- * synchronous session state (live validation while typing); every mutation —
217
- * create/save/delete/state transitions — is mirrored onto the operations
218
- * layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
219
- * Failed/denied commands surface as {@link CmsStore.persistError}.
220
- */
221
- export declare function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, initialLocaleConfig, seedReflectsWrites, }: {
227
+ export interface CmsProviderProps {
222
228
  children: ReactNode;
223
229
  /** Enables persistence through the shared operations client. */
224
230
  projectId?: string;
@@ -229,13 +235,23 @@ export declare function CmsProvider({ children, projectId, initialCollections, i
229
235
  * Omitted → the historical mode default (connected hydrates, playground may not).
230
236
  */
231
237
  seedReflectsWrites?: boolean;
232
- /** Seed collections (e.g. from an imported starter kit, EC-119); defaults to the samples. */
238
+ /**
239
+ * The session's collections — the project's own schema (hydration, a starter
240
+ * kit, or `elytra.config.ts`). Absent → **no collections** (EC-479): the store
241
+ * used to fall back to `cms-core`'s `sampleFilterableCollections`, so a studio
242
+ * whose config never arrived showed the engine's demo schema as if it were the
243
+ * project's. The absence is now reported at the rail (`projectConfigIssues`).
244
+ */
233
245
  initialCollections?: CollectionDef[];
246
+ /**
247
+ * The session's documents. Absent → **none** (EC-479; was `sampleDocuments`).
248
+ */
234
249
  initialDocuments?: CmsDocument[];
235
250
  /**
236
251
  * Published document keys from hydration (EC-224): the version pointer
237
- * (`publishedVersion` set). Absent (kit / fixture sessions) → the sample
238
- * fixtures' published set (`samplePublishedKeys`).
252
+ * (`publishedVersion` set). Absent (kit / fixture sessions) → none published
253
+ * (EC-479 retired the `samplePublishedKeys` fixture default, which only ever
254
+ * described the engine's own sample documents).
239
255
  */
240
256
  initialPublishedKeys?: readonly string[];
241
257
  /**
@@ -261,9 +277,33 @@ export declare function CmsProvider({ children, projectId, initialCollections, i
261
277
  * returned records. Absent (playground / kit) → empty set.
262
278
  */
263
279
  initialChangedKeys?: readonly string[];
264
- /** Locale config for the seeded content; defaults to the sample config. */
280
+ /**
281
+ * Locale config for this session — the PROJECT's own (config-declared locales,
282
+ * or its persisted settings; see `resolveCmsInitialLocaleConfig`). Absent → the
283
+ * active config project's locales are read directly, and failing that the
284
+ * session runs with no locale at all rather than the engine's `en/de`
285
+ * `sampleLocaleConfig` fixture (EC-479/EC-483: the default locale is the literal
286
+ * storage key for localized values, so inventing one is a future data migration).
287
+ */
265
288
  initialLocaleConfig?: LocaleConfig;
266
- }): import("react").JSX.Element;
289
+ }
290
+ /**
291
+ * The CMS session store (EC-141): documents and collections are optimistic,
292
+ * synchronous session state (live validation while typing); every mutation —
293
+ * create/save/delete/state transitions — is mirrored onto the operations
294
+ * layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
295
+ * Failed/denied commands surface as {@link CmsStore.persistError}.
296
+ *
297
+ * EC-479 — this outer component exists to make ONE property structural: a CMS
298
+ * session always runs in the project's own locale, or it does not run. The locale
299
+ * is resolved here (the caller's, else the active `elytra.config.ts` project's)
300
+ * and handed to {@link CmsSession} as a REQUIRED value, so no branch inside the
301
+ * session can reach for the engine's `sampleLocaleConfig` — the default locale is
302
+ * the storage key every localized value is written under (EC-483), and a session
303
+ * that guessed it would corrupt content rather than fail. With no locale at all
304
+ * the surface says so instead of editing in an invented one.
305
+ */
306
+ export declare function CmsProvider(props: CmsProviderProps): import("react").JSX.Element;
267
307
  export { useCms };
268
308
  export { primaryField };
269
309
  export type { Locale };
@@ -1,11 +1,13 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { createCollectionRegistry, createDocumentStore, documentKey, documentSchema, resolveDocumentTitle, resolveField, sampleDocuments, seedDefaults, sampleFilterableCollections, sampleLocaleConfig, samplePublishedKeys, resolveSingletonDocument, titleFieldOf, validateDocument, } from '@elytracms/core/cms-core';
3
+ import { createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, resolveField, seedDefaults, resolveSingletonDocument, titleFieldOf, validateDocument, } from '@elytracms/core/cms-core';
4
4
  import { translate } from '@elytracms/studio/lib/i18n';
5
5
  import { configureStudioLocales, useStudioLocale } from '@elytracms/studio/lib/locale';
6
6
  import { studioManifestLookup } from '@elytracms/studio/lib/registry';
7
- import { resolveDeploymentCollections, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
8
- import { validateComposition, validateRichTextEmbeds } from './cms-composition-validators.js';
7
+ import { resolveDeploymentCollections, resolveDeploymentLocaleConfig, resolveDeploymentRoutes, } from '@elytracms/studio/lib/workspace';
8
+ import { Heading, Panel, Stack, Text } from '@elytracms/studio/ui';
9
+ import { gateValidateOptions } from './gate-validation.js';
10
+ import { clearSessionDocumentValidator, setSessionDocumentValidator, } from '@elytracms/studio/lib/cms/document-gate';
9
11
  import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
10
12
  import { createRequiredContext } from '@elytracms/studio/lib/required-context';
11
13
  import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
@@ -116,10 +118,34 @@ function defaultView(collection, pseudo = []) {
116
118
  * create/save/delete/state transitions — is mirrored onto the operations
117
119
  * layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
118
120
  * Failed/denied commands surface as {@link CmsStore.persistError}.
121
+ *
122
+ * EC-479 — this outer component exists to make ONE property structural: a CMS
123
+ * session always runs in the project's own locale, or it does not run. The locale
124
+ * is resolved here (the caller's, else the active `elytra.config.ts` project's)
125
+ * and handed to {@link CmsSession} as a REQUIRED value, so no branch inside the
126
+ * session can reach for the engine's `sampleLocaleConfig` — the default locale is
127
+ * the storage key every localized value is written under (EC-483), and a session
128
+ * that guessed it would corrupt content rather than fail. With no locale at all
129
+ * the surface says so instead of editing in an invented one.
130
+ */
131
+ export function CmsProvider(props) {
132
+ const localeConfig = props.initialLocaleConfig ?? resolveDeploymentLocaleConfig();
133
+ if (localeConfig === undefined)
134
+ return _jsx(MissingProjectLocales, {});
135
+ return _jsx(CmsSession, { ...props, localeConfig: localeConfig });
136
+ }
137
+ /**
138
+ * The project declares no locales, so there is no key to write localized values
139
+ * under. Visible and specific rather than a silent fixture locale (EC-479) — and
140
+ * the rail foot carries the same finding as a project-level validation issue
141
+ * (`projectConfigIssues`), so it is reported wherever the developer is looking.
119
142
  */
120
- export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, initialLocaleConfig, seedReflectsWrites, }) {
143
+ function MissingProjectLocales() {
144
+ return (_jsx(Panel, { className: "m-6 p-6", children: _jsxs(Stack, { gap: "2", children: [_jsx(Heading, { level: 2, size: "h4", children: translate('cms.session.noLocales.title') }), _jsx(Text, { size: "sm", tone: "subtle", children: translate('cms.session.noLocales.body') })] }) }));
145
+ }
146
+ function CmsSession({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, seedReflectsWrites, localeConfig, }) {
121
147
  const [documents, setDocuments] = useState(() => {
122
- const seeded = (initialDocuments ?? sampleDocuments).map((d) => structuredClone(d));
148
+ const seeded = (initialDocuments ?? []).map((d) => structuredClone(d));
123
149
  // EC-320 (0.0.44 follow-up): merge any documents STAGED by the
124
150
  // Components-view "Use in document…" transfer — a cross-mount handoff in
125
151
  // EVERY mode (see document-staging.ts). EC-461: the merge rule keys on the
@@ -131,15 +157,13 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
131
157
  : consumeStagedDocuments(projectId, seeded, seedReflectsWrites);
132
158
  });
133
159
  // Published-ness as the version POINTER (EC-224), decoupled from the flat
134
- // `state` enum: seeded from hydration's pointer set, or — when none is supplied
135
- // — the canonical sample published set for the playground. A kit's imported
136
- // content (`initialDocuments` without a pointer set) starts unpublished
137
- // (drafts) until an editor publishes it. The flat `state` is never consulted.
138
- const [publishedKeys, setPublishedKeys] = useState(() => {
139
- if (initialPublishedKeys)
140
- return new Set(initialPublishedKeys);
141
- return new Set(initialDocuments ? [] : samplePublishedKeys);
142
- });
160
+ // `state` enum: seeded from hydration's pointer set. A kit's imported content
161
+ // (`initialDocuments` without a pointer set) starts unpublished (drafts) until
162
+ // an editor publishes it, and so does a session with no pointer set at all —
163
+ // EC-479 retired the `samplePublishedKeys` branch, which pinned the ENGINE's
164
+ // sample documents live in a session that no longer has any. The flat `state`
165
+ // is never consulted.
166
+ const [publishedKeys, setPublishedKeys] = useState(() => new Set(initialPublishedKeys ?? []));
143
167
  // EC-445: per-document last-write timestamps behind the `$updatedAt` column.
144
168
  // Seeded from hydration (connected: the persisted `cmsDocuments.updatedAt`), then
145
169
  // re-stamped on the common edit paths. A seeded document with no hydrated timestamp
@@ -152,7 +176,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
152
176
  for (const [key, value] of Object.entries(initialTimestamps))
153
177
  map.set(key, value);
154
178
  }
155
- for (const doc of initialDocuments ?? sampleDocuments) {
179
+ for (const doc of initialDocuments ?? []) {
156
180
  const key = documentKey(doc);
157
181
  if (!map.has(key))
158
182
  map.set(key, now);
@@ -239,12 +263,13 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
239
263
  // Collections are stateful so AI-generated schemas (EC-097) can be accepted
240
264
  // into the session. The session schema IS the source the sort/filter
241
265
  // offering follows (EC-177 gap 3): hydrated sessions receive the persisted
242
- // schema via `initialCollections`; the fixture fallback is the canonical
243
- // cms-core sample schema WITH its declared `filterable` flags — no
244
- // session-side widenings, so the studio never offers a field the backend's
245
- // delivery engine would reject.
266
+ // schema via `initialCollections`, which is the project's own throughout —
267
+ // hydration, a kit, or `elytra.config.ts` (`resolveCmsInitialCollections`).
268
+ // EC-479: with none, the session has NO collections and the rail says so; it
269
+ // no longer borrows the engine's sample schema, which offered `filterable`
270
+ // fields the project's real backend would reject.
246
271
  const [collections, setCollections] = useState(() => [
247
- ...(initialCollections ?? sampleFilterableCollections),
272
+ ...(initialCollections ?? []),
248
273
  ]);
249
274
  const [persistError, setPersistError] = useState(null);
250
275
  const seq = useRef(0);
@@ -275,9 +300,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
275
300
  onDocumentSaved: adoptSavedRecord,
276
301
  });
277
302
  }
303
+ // EC-224: published-ness IS the version pointer (`publishedKeys`) — the SOLE
304
+ // studio source of truth. Declared up here (EC-497) because it is part of THE
305
+ // validation option bag, which the cell-edit commit — defined above the publish
306
+ // helpers — now also takes.
307
+ const isPublished = useCallback((ref) => publishedKeys.has(documentKey(ref)), [publishedKeys]);
278
308
  const registry = useMemo(() => createCollectionRegistry(collections), [collections]);
309
+ // EC-497: the gate seam registers ONCE and reads through refs, so it always sees
310
+ // the latest session without re-registering on every keystroke.
311
+ const registryRef = useRef(registry);
312
+ registryRef.current = registry;
279
313
  const store = useMemo(() => createDocumentStore(documents), [documents]);
280
- const localeConfig = initialLocaleConfig ?? sampleLocaleConfig;
281
314
  // Asset existence is validated against the REAL asset library (assets live in
282
315
  // their own repository, not as CMS documents). With the ambient AssetsProvider
283
316
  // present, the oracle is the set of uploaded asset ids — but only once loaded;
@@ -295,7 +328,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
295
328
  }, [assetStore]);
296
329
  // The ambient studio locale (EC-176): this session's locale config drives
297
330
  // the switcher options; every CMS surface follows the one active locale.
298
- const { locale: activeLocale } = useStudioLocale();
331
+ // EC-479: the ambient store is `null` until this effect installs the project's
332
+ // config (it no longer opens on the engine's `en/de` fixture), so the FIRST
333
+ // render — and any surface mounted outside a CMS session — reads the project's
334
+ // own default here rather than a locale nobody declared.
335
+ const { locale } = useStudioLocale();
336
+ const activeLocale = locale ?? localeConfig.default;
299
337
  useEffect(() => {
300
338
  configureStudioLocales(localeConfig);
301
339
  }, [localeConfig]);
@@ -338,6 +376,11 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
338
376
  registry,
339
377
  documents: documentsRef.current,
340
378
  localeConfig,
379
+ // EC-497: the remaining halves of THE option bag. A cell edit now
380
+ // validates with exactly what the editor and the publish gate use and
381
+ // filters the result to the committed cell — it is not a weaker check.
382
+ isPublished,
383
+ ...(assetExists ? { assetExists } : {}),
341
384
  // EC-445: the optimistic apply bumps the $updatedAt column (a rollback
342
385
  // leaves it — it self-corrects on the next hydration).
343
386
  applySession: (doc) => {
@@ -362,7 +405,15 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
362
405
  }
363
406
  : {}),
364
407
  }, input);
365
- }, [registry, localeConfig, projectId, stampUpdatedAt, adoptSavedRecord]);
408
+ }, [
409
+ registry,
410
+ localeConfig,
411
+ projectId,
412
+ isPublished,
413
+ assetExists,
414
+ stampUpdatedAt,
415
+ adoptSavedRecord,
416
+ ]);
366
417
  // Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
367
418
  // `buildNewDocument`) + optimistic append + persist, consumed by both
368
419
  // `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
@@ -438,7 +489,6 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
438
489
  return next;
439
490
  });
440
491
  }, []);
441
- const isPublished = useCallback((ref) => publishedKeys.has(documentKey(ref)), [publishedKeys]);
442
492
  // Reconcile published-ness for a ref against the AUTHORITATIVE backend record
443
493
  // (EC-225): the optimistic flip is a guess; once the queued op settles we read
444
494
  // the actual pin so the badge can never claim a state the backend refused
@@ -527,18 +577,53 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
527
577
  adoptSavedRecord(result.value);
528
578
  return restored;
529
579
  }, [projectId, adoptSavedRecord]);
530
- const validate = useCallback((doc) => validateDocument(registry, doc, {
580
+ /**
581
+ * EC-497 — THE option bag (AD-497a), built once and used by every surface that
582
+ * validates a CMS document. `gateValidateOptions` is the only place options are
583
+ * assembled; before it there were seven hand-assembled subsets and the weakest
584
+ * one was the publish gate. Note it carries `isPublished` too: the EC-173
585
+ * weak-reference warning sources published-ness from the version POINTER, so
586
+ * editing a published doc that references an unpublished target surfaces the
587
+ * fallback warning (it had gone inert when the flat `state` mirror was dropped).
588
+ */
589
+ const validateOptions = useMemo(() => gateValidateOptions({
531
590
  store,
532
591
  localeConfig,
533
- validateComposition,
534
- validateRichTextEmbeds,
535
- // EC-224: the EC-173 weak-reference warning sources published-ness from
536
- // the version pointer, so editing a published doc that references an
537
- // unpublished target surfaces the fallback warning again (it had gone
538
- // inert once the flat `state` mirror was dropped in B-3b).
539
592
  isPublished,
540
- assetExists,
541
- }), [registry, store, localeConfig, isPublished, assetExists]);
593
+ ...(assetExists ? { assetExists } : {}),
594
+ }), [store, localeConfig, isPublished, assetExists]);
595
+ const validate = useCallback((doc) => {
596
+ const issues = validateDocument(registry, doc, validateOptions);
597
+ // EC-492: a document whose composed URL no request can reach — a top-level
598
+ // slug that collides with a declared locale code, which the host reads as a
599
+ // locale prefix. Not part of `validateDocument` because it needs the project
600
+ // ROUTES (the schema validator is route-free by design); emitted here, where
601
+ // the deployment's routes and the collection's sibling set are both in hand.
602
+ // Silent in a single-locale project — the fault is structurally impossible.
603
+ const collection = registry.get(doc.collection);
604
+ if (!collection)
605
+ return issues;
606
+ return [
607
+ ...issues,
608
+ ...documentReachabilityIssues({ routes: resolveDeploymentRoutes(), localeConfig }, collection, doc, { siblings: documents.filter((sibling) => sibling.collection === doc.collection) }),
609
+ ];
610
+ }, [registry, validateOptions, documents, localeConfig]);
611
+ /**
612
+ * EC-497 — publish the session's canonical validator to the gate seam, so the
613
+ * operations-layer `documents.publish` gate judges a document with exactly these
614
+ * rules instead of the weaker, mode-dependent ones it read off the adapter's
615
+ * schema record. `[]` for a collection the config does not declare preserves the
616
+ * EC-183/AD-11 data-store rule (studio system collections carry no config field
617
+ * rules); a validator kept fresh through a ref means the seam sees the latest
618
+ * session without re-registering per keystroke.
619
+ */
620
+ const gateRef = useRef(validate);
621
+ gateRef.current = validate;
622
+ useEffect(() => {
623
+ const owned = (doc) => registryRef.current.has(doc.collection) ? gateRef.current(doc) : [];
624
+ setSessionDocumentValidator(owned);
625
+ return () => clearSessionDocumentValidator(owned);
626
+ }, []);
542
627
  const importDocuments = useCallback((collectionId, raw) => {
543
628
  const report = { added: 0, rejected: [] };
544
629
  // Pre-parse so intra-batch relation/asset targets resolve during validation.
@@ -565,8 +650,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
565
650
  // Validate each against existing docs PLUS the rest of the batch.
566
651
  const batchStore = createDocumentStore([...documents, ...parsedBatch]);
567
652
  const accepted = [];
653
+ // EC-497: the same bag as everywhere else, over the batch-aware store. It
654
+ // used to omit the composition leashes and the published-ness pointer, so an
655
+ // import could land a document the editor would immediately flag.
656
+ const importOptions = gateValidateOptions({
657
+ store: batchStore,
658
+ localeConfig,
659
+ isPublished,
660
+ ...(assetExists ? { assetExists } : {}),
661
+ });
568
662
  for (const doc of parsedBatch) {
569
- const issues = validateDocument(registry, doc, { store: batchStore, localeConfig, assetExists });
663
+ const issues = validateDocument(registry, doc, importOptions);
570
664
  const errors = issues.filter((iss) => iss.severity === 'error');
571
665
  if (errors.length > 0)
572
666
  report.rejected.push({ id: doc.id, issues: errors });
@@ -587,7 +681,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
587
681
  report.added = accepted.length;
588
682
  }
589
683
  return report;
590
- }, [registry, documents, localeConfig, assetExists]);
684
+ }, [registry, documents, localeConfig, isPublished, assetExists]);
591
685
  const isFieldReadOnly = useCallback((field) => field.form?.readOnly === true, []);
592
686
  // --- Views (EC-246b: config-as-code) --------------------------------------
593
687
  // EC-445: the reserved pseudo-columns a CMS collection's list surfaces. Every
@@ -720,17 +814,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
720
814
  return runBulkAction({
721
815
  documents: documentsRef.current,
722
816
  titleOf,
723
- validate: (doc) => validateDocument(registry, doc, {
817
+ // EC-497: THE bag, over the freshest session snapshot (`runBulk` is
818
+ // async, so `documentsRef.current` — not the memoized store — is the
819
+ // world as it is now). It used to be assembled here and silently missed
820
+ // `isPublished`, so a bulk publish and a single publish of the same
821
+ // document could disagree about the EC-173 weak-reference warning.
822
+ validate: (doc) => validateDocument(registry, doc, gateValidateOptions({
724
823
  store: createDocumentStore(documentsRef.current),
725
824
  localeConfig,
726
- // EC-225: include the composition validators so a bulk PUBLISH
727
- // gates blocks/richText validity the same way the detail editor
728
- // does — the operations-layer publish gate has no component
729
- // registry, so composition validity is a studio-validation invariant.
730
- validateComposition,
731
- validateRichTextEmbeds,
732
- assetExists,
733
- }),
825
+ isPublished,
826
+ ...(assetExists ? { assetExists } : {}),
827
+ })),
734
828
  // EC-425: bulk publish joins the single-document clean point — the
735
829
  // SAME shared hidden-strip `doPublish` runs (EC-273 field values +
736
830
  // EC-394 component props, session registry), per item.
@@ -775,6 +869,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
775
869
  localeConfig,
776
870
  activeLocale,
777
871
  setPublished,
872
+ isPublished,
778
873
  assetExists,
779
874
  adoptSavedRecord,
780
875
  stampHasUnpublishedChanges,
@@ -803,6 +898,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
803
898
  restoreDocumentVersion,
804
899
  importDocuments,
805
900
  validate,
901
+ validateOptions,
806
902
  isFieldReadOnly,
807
903
  viewsOf,
808
904
  activeViewId: (collectionId) => activeViews[collectionId] ?? initialViewId(collectionId),
@@ -858,6 +954,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
858
954
  restoreDocumentVersion,
859
955
  importDocuments,
860
956
  validate,
957
+ validateOptions,
861
958
  isFieldReadOnly,
862
959
  viewsOf,
863
960
  activeViews,