@elytracms/studio 0.0.81 → 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 (137) hide show
  1. package/convex/redirect-revalidate.test.ts +130 -0
  2. package/convex/revalidate.ts +9 -7
  3. package/convex/triggers.ts +33 -3
  4. package/dist/components/ai/ai-chat-panel.js +26 -4
  5. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  6. package/dist/components/ai/proposal-card.d.ts +27 -0
  7. package/dist/components/ai/proposal-card.js +31 -3
  8. package/dist/components/ai/proposal-card.js.map +1 -1
  9. package/dist/components/ai/proposal-issues.d.ts +55 -0
  10. package/dist/components/ai/proposal-issues.js +69 -0
  11. package/dist/components/ai/proposal-issues.js.map +1 -0
  12. package/dist/components/ai/translate-document-dialog.js +38 -3
  13. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  14. package/dist/components/app-views/studio-config.d.ts +1 -1
  15. package/dist/components/app-views/studio-config.js +1 -1
  16. package/dist/components/app-views/studio-config.js.map +1 -1
  17. package/dist/components/builder/canvas-render-context.js +3 -1
  18. package/dist/components/builder/canvas-render-context.js.map +1 -1
  19. package/dist/components/builder/clipboard-actions.js +5 -3
  20. package/dist/components/builder/clipboard-actions.js.map +1 -1
  21. package/dist/components/cms/bulk-actions.js +8 -5
  22. package/dist/components/cms/bulk-actions.js.map +1 -1
  23. package/dist/components/cms/cell-edit.d.ts +18 -3
  24. package/dist/components/cms/cell-edit.js +27 -9
  25. package/dist/components/cms/cell-edit.js.map +1 -1
  26. package/dist/components/cms/cms-store.d.ts +14 -1
  27. package/dist/components/cms/cms-store.js +93 -24
  28. package/dist/components/cms/cms-store.js.map +1 -1
  29. package/dist/components/cms/collection-table.js +1 -1
  30. package/dist/components/cms/collection-table.js.map +1 -1
  31. package/dist/components/cms/detail-editor.js +7 -12
  32. package/dist/components/cms/detail-editor.js.map +1 -1
  33. package/dist/components/cms/document-editor-context.d.ts +12 -1
  34. package/dist/components/cms/document-editor-context.js.map +1 -1
  35. package/dist/components/cms/field-control.js +20 -7
  36. package/dist/components/cms/field-control.js.map +1 -1
  37. package/dist/components/cms/gate-validation.d.ts +36 -0
  38. package/dist/components/cms/gate-validation.js +17 -0
  39. package/dist/components/cms/gate-validation.js.map +1 -0
  40. package/dist/components/cms/inline-field-errors.d.ts +8 -0
  41. package/dist/components/cms/inline-field-errors.js +52 -0
  42. package/dist/components/cms/inline-field-errors.js.map +1 -0
  43. package/dist/components/cms/issues-pane.js +8 -4
  44. package/dist/components/cms/issues-pane.js.map +1 -1
  45. package/dist/components/cms/slug-collision-report.d.ts +41 -0
  46. package/dist/components/cms/slug-collision-report.js +52 -0
  47. package/dist/components/cms/slug-collision-report.js.map +1 -0
  48. package/dist/components/deployment-setup-gate.d.ts +24 -0
  49. package/dist/components/deployment-setup-gate.js +29 -4
  50. package/dist/components/deployment-setup-gate.js.map +1 -1
  51. package/dist/components/issue-severity.d.ts +73 -0
  52. package/dist/components/issue-severity.js +56 -0
  53. package/dist/components/issue-severity.js.map +1 -0
  54. package/dist/components/nav-issues.d.ts +68 -0
  55. package/dist/components/nav-issues.js +78 -0
  56. package/dist/components/nav-issues.js.map +1 -0
  57. package/dist/components/operation-error.js +3 -3
  58. package/dist/components/operation-error.js.map +1 -1
  59. package/dist/components/section-nav.js +5 -38
  60. package/dist/components/section-nav.js.map +1 -1
  61. package/dist/lib/ai/apply-content.js +6 -7
  62. package/dist/lib/ai/apply-content.js.map +1 -1
  63. package/dist/lib/ai/chat-context.d.ts +32 -26
  64. package/dist/lib/ai/chat-context.js +81 -9
  65. package/dist/lib/ai/chat-context.js.map +1 -1
  66. package/dist/lib/ai/constraints.d.ts +42 -0
  67. package/dist/lib/ai/constraints.js +259 -0
  68. package/dist/lib/ai/constraints.js.map +1 -0
  69. package/dist/lib/ai/index.d.ts +2 -0
  70. package/dist/lib/ai/index.js +2 -0
  71. package/dist/lib/ai/index.js.map +1 -1
  72. package/dist/lib/ai/proposal-validation.d.ts +26 -0
  73. package/dist/lib/ai/proposal-validation.js +17 -0
  74. package/dist/lib/ai/proposal-validation.js.map +1 -0
  75. package/dist/lib/ai/shape.js +60 -13
  76. package/dist/lib/ai/shape.js.map +1 -1
  77. package/dist/lib/ai/tools.d.ts +8 -4
  78. package/dist/lib/ai/tools.js +43 -14
  79. package/dist/lib/ai/tools.js.map +1 -1
  80. package/dist/lib/ai/translate-document.d.ts +35 -1
  81. package/dist/lib/ai/translate-document.js +16 -1
  82. package/dist/lib/ai/translate-document.js.map +1 -1
  83. package/dist/lib/ai/types.d.ts +11 -6
  84. package/dist/lib/ai/types.js.map +1 -1
  85. package/dist/lib/ai/update-fields.d.ts +8 -6
  86. package/dist/lib/ai/update-fields.js +9 -22
  87. package/dist/lib/ai/update-fields.js.map +1 -1
  88. package/dist/lib/clipboard/paste-issues.d.ts +5 -0
  89. package/dist/lib/clipboard/paste-issues.js +16 -6
  90. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  91. package/dist/lib/cms/document-gate.d.ts +37 -0
  92. package/dist/lib/cms/document-gate.js +40 -0
  93. package/dist/lib/cms/document-gate.js.map +1 -0
  94. package/dist/lib/cms-views/view-model.d.ts +5 -0
  95. package/dist/lib/cms-views/view-model.js +10 -4
  96. package/dist/lib/cms-views/view-model.js.map +1 -1
  97. package/dist/lib/data/collection-content.js +1 -3
  98. package/dist/lib/data/collection-content.js.map +1 -1
  99. package/dist/lib/i18n/i18n.d.ts +51 -16
  100. package/dist/lib/i18n/i18n.js +119 -43
  101. package/dist/lib/i18n/i18n.js.map +1 -1
  102. package/dist/lib/i18n/index.d.ts +8 -1
  103. package/dist/lib/i18n/index.js +1 -1
  104. package/dist/lib/i18n/index.js.map +1 -1
  105. package/dist/lib/i18n/messages.de.js +68 -0
  106. package/dist/lib/i18n/messages.de.js.map +1 -1
  107. package/dist/lib/i18n/messages.en.d.ts +59 -0
  108. package/dist/lib/i18n/messages.en.js +80 -0
  109. package/dist/lib/i18n/messages.en.js.map +1 -1
  110. package/dist/lib/i18n/types.d.ts +15 -9
  111. package/dist/lib/i18n/types.js.map +1 -1
  112. package/dist/lib/operations/studio-operations.js +19 -2
  113. package/dist/lib/operations/studio-operations.js.map +1 -1
  114. package/dist/lib/persistence/deployment-readiness.js +10 -0
  115. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  116. package/dist/lib/persona/labels.d.ts +44 -9
  117. package/dist/lib/persona/labels.js +143 -60
  118. package/dist/lib/persona/labels.js.map +1 -1
  119. package/dist/lib/webhooks/dispatcher.d.ts +5 -2
  120. package/dist/lib/webhooks/dispatcher.js.map +1 -1
  121. package/dist/lib/webhooks/tags.js +150 -17
  122. package/dist/lib/webhooks/tags.js.map +1 -1
  123. package/dist/lib/workspace/config-issues.d.ts +3 -1
  124. package/dist/lib/workspace/config-issues.js +3 -1
  125. package/dist/lib/workspace/config-issues.js.map +1 -1
  126. package/dist/lib/workspace/index.d.ts +15 -8
  127. package/dist/lib/workspace/index.js +17 -18
  128. package/dist/lib/workspace/index.js.map +1 -1
  129. package/dist/lib/workspace/workspace-source.js +27 -0
  130. package/dist/lib/workspace/workspace-source.js.map +1 -1
  131. package/dist/previews/manifest-format.d.ts +13 -6
  132. package/dist/previews/manifest-format.js +9 -2
  133. package/dist/previews/manifest-format.js.map +1 -1
  134. package/dist/router/root.d.ts +36 -1
  135. package/dist/router/root.js +54 -7
  136. package/dist/router/root.js.map +1 -1
  137. package/package.json +2 -2
@@ -1 +1 @@
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 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'\nimport { cmsValidationIssueMessage } 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 */\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 // EC-487: an `OperationIssue` carries no catalog reference (it crosses the\n // operations boundary as plain text), so the message is resolved HERE, at the\n // last point that still has the structured issue.\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 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;
@@ -1,12 +1,13 @@
1
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, seedDefaults, 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
7
  import { resolveDeploymentCollections, resolveDeploymentLocaleConfig, resolveDeploymentRoutes, } from '@elytracms/studio/lib/workspace';
8
8
  import { Heading, Panel, Stack, Text } from '@elytracms/studio/ui';
9
- import { validateComposition, validateRichTextEmbeds } from './cms-composition-validators.js';
9
+ import { gateValidateOptions } from './gate-validation.js';
10
+ import { clearSessionDocumentValidator, setSessionDocumentValidator, } from '@elytracms/studio/lib/cms/document-gate';
10
11
  import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
11
12
  import { createRequiredContext } from '@elytracms/studio/lib/required-context';
12
13
  import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
@@ -299,7 +300,16 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
299
300
  onDocumentSaved: adoptSavedRecord,
300
301
  });
301
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]);
302
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;
303
313
  const store = useMemo(() => createDocumentStore(documents), [documents]);
304
314
  // Asset existence is validated against the REAL asset library (assets live in
305
315
  // their own repository, not as CMS documents). With the ambient AssetsProvider
@@ -366,6 +376,11 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
366
376
  registry,
367
377
  documents: documentsRef.current,
368
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 } : {}),
369
384
  // EC-445: the optimistic apply bumps the $updatedAt column (a rollback
370
385
  // leaves it — it self-corrects on the next hydration).
371
386
  applySession: (doc) => {
@@ -390,7 +405,15 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
390
405
  }
391
406
  : {}),
392
407
  }, input);
393
- }, [registry, localeConfig, projectId, stampUpdatedAt, adoptSavedRecord]);
408
+ }, [
409
+ registry,
410
+ localeConfig,
411
+ projectId,
412
+ isPublished,
413
+ assetExists,
414
+ stampUpdatedAt,
415
+ adoptSavedRecord,
416
+ ]);
394
417
  // Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
395
418
  // `buildNewDocument`) + optimistic append + persist, consumed by both
396
419
  // `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
@@ -466,7 +489,6 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
466
489
  return next;
467
490
  });
468
491
  }, []);
469
- const isPublished = useCallback((ref) => publishedKeys.has(documentKey(ref)), [publishedKeys]);
470
492
  // Reconcile published-ness for a ref against the AUTHORITATIVE backend record
471
493
  // (EC-225): the optimistic flip is a guess; once the queued op settles we read
472
494
  // the actual pin so the badge can never claim a state the backend refused
@@ -555,18 +577,53 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
555
577
  adoptSavedRecord(result.value);
556
578
  return restored;
557
579
  }, [projectId, adoptSavedRecord]);
558
- 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({
559
590
  store,
560
591
  localeConfig,
561
- validateComposition,
562
- validateRichTextEmbeds,
563
- // EC-224: the EC-173 weak-reference warning sources published-ness from
564
- // the version pointer, so editing a published doc that references an
565
- // unpublished target surfaces the fallback warning again (it had gone
566
- // inert once the flat `state` mirror was dropped in B-3b).
567
592
  isPublished,
568
- assetExists,
569
- }), [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
+ }, []);
570
627
  const importDocuments = useCallback((collectionId, raw) => {
571
628
  const report = { added: 0, rejected: [] };
572
629
  // Pre-parse so intra-batch relation/asset targets resolve during validation.
@@ -593,8 +650,17 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
593
650
  // Validate each against existing docs PLUS the rest of the batch.
594
651
  const batchStore = createDocumentStore([...documents, ...parsedBatch]);
595
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
+ });
596
662
  for (const doc of parsedBatch) {
597
- const issues = validateDocument(registry, doc, { store: batchStore, localeConfig, assetExists });
663
+ const issues = validateDocument(registry, doc, importOptions);
598
664
  const errors = issues.filter((iss) => iss.severity === 'error');
599
665
  if (errors.length > 0)
600
666
  report.rejected.push({ id: doc.id, issues: errors });
@@ -615,7 +681,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
615
681
  report.added = accepted.length;
616
682
  }
617
683
  return report;
618
- }, [registry, documents, localeConfig, assetExists]);
684
+ }, [registry, documents, localeConfig, isPublished, assetExists]);
619
685
  const isFieldReadOnly = useCallback((field) => field.form?.readOnly === true, []);
620
686
  // --- Views (EC-246b: config-as-code) --------------------------------------
621
687
  // EC-445: the reserved pseudo-columns a CMS collection's list surfaces. Every
@@ -748,17 +814,17 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
748
814
  return runBulkAction({
749
815
  documents: documentsRef.current,
750
816
  titleOf,
751
- 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({
752
823
  store: createDocumentStore(documentsRef.current),
753
824
  localeConfig,
754
- // EC-225: include the composition validators so a bulk PUBLISH
755
- // gates blocks/richText validity the same way the detail editor
756
- // does — the operations-layer publish gate has no component
757
- // registry, so composition validity is a studio-validation invariant.
758
- validateComposition,
759
- validateRichTextEmbeds,
760
- assetExists,
761
- }),
825
+ isPublished,
826
+ ...(assetExists ? { assetExists } : {}),
827
+ })),
762
828
  // EC-425: bulk publish joins the single-document clean point — the
763
829
  // SAME shared hidden-strip `doPublish` runs (EC-273 field values +
764
830
  // EC-394 component props, session registry), per item.
@@ -803,6 +869,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
803
869
  localeConfig,
804
870
  activeLocale,
805
871
  setPublished,
872
+ isPublished,
806
873
  assetExists,
807
874
  adoptSavedRecord,
808
875
  stampHasUnpublishedChanges,
@@ -831,6 +898,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
831
898
  restoreDocumentVersion,
832
899
  importDocuments,
833
900
  validate,
901
+ validateOptions,
834
902
  isFieldReadOnly,
835
903
  viewsOf,
836
904
  activeViewId: (collectionId) => activeViews[collectionId] ?? initialViewId(collectionId),
@@ -886,6 +954,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
886
954
  restoreDocumentVersion,
887
955
  importDocuments,
888
956
  validate,
957
+ validateOptions,
889
958
  isFieldReadOnly,
890
959
  viewsOf,
891
960
  activeViews,