@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
@@ -32,16 +32,22 @@ export interface StudioI18nConfig {
32
32
  */
33
33
  defaultLocale?: string;
34
34
  /**
35
- * **Pin** the studio UI to a fixed locale, overriding browser detection. Set this
36
- * for a single-language deployment (e.g. a German site whose editors work in German).
35
+ * **Pin** the studio UI to a fixed locale, overriding browser detection. **This is the
36
+ * supported way to choose the studio's language** (EC-513) — set it for any deployment
37
+ * whose editors work in one language, which is what both production consumers do.
37
38
  *
38
- * Why it exists: browser detection reads `navigator.languages`, which is **client-only**
39
- * — under SSR the server can't see it, so it renders the default locale while the client
40
- * renders the browser locale → a hydration mismatch (server text ≠ client text). A pin is
41
- * read from *config*, which is imported identically on server and client, so both render
42
- * the same locale with no header plumbing and no first-paint flash. When a locale is not
43
- * available (not `defaultLocale` and not in `locales`) the pin is ignored (with a console
44
- * signal) and detection resumes. Omit it to keep browser-preferred detection.
39
+ * Why prefer it: browser detection reads `navigator.languages`, which is **client-only**.
40
+ * The server renders the default locale, and the studio therefore boots in that locale
41
+ * and hands over to the browser's preference only after hydration — correct, but it
42
+ * means a moment of default-locale chrome on a cold load. A pin is read from *config*,
43
+ * imported identically on server and client, so the right language is there from the
44
+ * first byte.
45
+ *
46
+ * There is deliberately **no server-side locale seam** (no `Accept-Language` resolver):
47
+ * locale resolution is client-side, and a pin is how a host states the answer up front.
48
+ *
49
+ * A locale that is not available (neither `defaultLocale` nor a key of `locales`) is
50
+ * ignored with a console signal, and detection resumes. Omit it to follow the browser.
45
51
  */
46
52
  locale?: string;
47
53
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/i18n/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgDH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,WAAoB;IACvD,OAAO,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAA;AAC3E,CAAC","sourcesContent":["/**\n * Studio-UI localization types (EC-241) — the leaf module both the message catalog\n * and the resolver import, so nothing here may import studio runtime code (keeps the\n * `defineStudio` config surface free of import cycles).\n *\n * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a\n * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}\n * on the field/collection def, resolved via `resolveLabel`. Content-*value*\n * localization is a third, unrelated axis (`cms-core/localization.ts`).\n */\n\n/** A source-catalog entry: the English string + an optional translator hint. */\nexport interface SourceMessage {\n /** The English (source-of-truth) string. */\n message: string\n /**\n * Translator hint/context — the same word can translate differently by context\n * (the standard `msgctxt`/Lingui-comment idea). **Authoring/translation-time only:**\n * it lives on the in-engine source catalog and is NOT shipped in the flat\n * consumer-provided translation catalogs.\n */\n description?: string\n}\n\n/** The in-engine English source catalog: message key → {@link SourceMessage}. */\nexport type SourceCatalog = Record<string, SourceMessage>\n\n/** A consumer-provided translation catalog: flat message key → localized string. */\nexport type MessageCatalog = Record<string, string>\n\n/** The studio-UI localization config passed to `defineStudio({ i18n })` (EC-241). */\nexport interface StudioI18nConfig {\n /**\n * The locale used when the browser's preferred locale matches nothing available.\n * Defaults to `'en'` (the in-engine source language).\n */\n defaultLocale?: string\n /**\n * **Pin** the studio UI to a fixed locale, overriding browser detection. Set this\n * for a single-language deployment (e.g. a German site whose editors work in German).\n *\n * Why it exists: browser detection reads `navigator.languages`, which is **client-only**\n * — under SSR the server can't see it, so it renders the default locale while the client\n * renders the browser locale → a hydration mismatch (server text ≠ client text). A pin is\n * read from *config*, which is imported identically on server and client, so both render\n * the same locale with no header plumbing and no first-paint flash. When a locale is not\n * available (not `defaultLocale` and not in `locales`) the pin is ignored (with a console\n * signal) and detection resumes. Omit it to keep browser-preferred detection.\n */\n locale?: string\n /**\n * Imported translation catalogs keyed by locale code, e.g. `{ de: deMessages }`.\n * English is always available as the built-in source, so it need not be listed.\n */\n locales?: Record<string, MessageCatalog>\n}\n\n/**\n * Author a source-catalog entry with an optional translator hint (EC-241). Kept in\n * this leaf module so `messages.en.ts` can build the catalog without importing the\n * resolver (which would import studio runtime code).\n */\nexport function msg(message: string, description?: string): SourceMessage {\n return description !== undefined ? { message, description } : { message }\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/i18n/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsDH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,WAAoB;IACvD,OAAO,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAA;AAC3E,CAAC","sourcesContent":["/**\n * Studio-UI localization types (EC-241) — the leaf module both the message catalog\n * and the resolver import, so nothing here may import studio runtime code (keeps the\n * `defineStudio` config surface free of import cycles).\n *\n * This is the **studio-chrome** axis (engine-owned strings). Schema labels are a\n * separate mechanism — a {@link import('@elytracms/core/cms-core').LocalizedString}\n * on the field/collection def, resolved via `resolveLabel`. Content-*value*\n * localization is a third, unrelated axis (`cms-core/localization.ts`).\n */\n\n/** A source-catalog entry: the English string + an optional translator hint. */\nexport interface SourceMessage {\n /** The English (source-of-truth) string. */\n message: string\n /**\n * Translator hint/context — the same word can translate differently by context\n * (the standard `msgctxt`/Lingui-comment idea). **Authoring/translation-time only:**\n * it lives on the in-engine source catalog and is NOT shipped in the flat\n * consumer-provided translation catalogs.\n */\n description?: string\n}\n\n/** The in-engine English source catalog: message key → {@link SourceMessage}. */\nexport type SourceCatalog = Record<string, SourceMessage>\n\n/** A consumer-provided translation catalog: flat message key → localized string. */\nexport type MessageCatalog = Record<string, string>\n\n/** The studio-UI localization config passed to `defineStudio({ i18n })` (EC-241). */\nexport interface StudioI18nConfig {\n /**\n * The locale used when the browser's preferred locale matches nothing available.\n * Defaults to `'en'` (the in-engine source language).\n */\n defaultLocale?: string\n /**\n * **Pin** the studio UI to a fixed locale, overriding browser detection. **This is the\n * supported way to choose the studio's language** (EC-513) — set it for any deployment\n * whose editors work in one language, which is what both production consumers do.\n *\n * Why prefer it: browser detection reads `navigator.languages`, which is **client-only**.\n * The server renders the default locale, and the studio therefore boots in that locale\n * and hands over to the browser's preference only after hydration — correct, but it\n * means a moment of default-locale chrome on a cold load. A pin is read from *config*,\n * imported identically on server and client, so the right language is there from the\n * first byte.\n *\n * There is deliberately **no server-side locale seam** (no `Accept-Language` resolver):\n * locale resolution is client-side, and a pin is how a host states the answer up front.\n *\n * A locale that is not available (neither `defaultLocale` nor a key of `locales`) is\n * ignored with a console signal, and detection resumes. Omit it to follow the browser.\n */\n locale?: string\n /**\n * Imported translation catalogs keyed by locale code, e.g. `{ de: deMessages }`.\n * English is always available as the built-in source, so it need not be listed.\n */\n locales?: Record<string, MessageCatalog>\n}\n\n/**\n * Author a source-catalog entry with an optional translator hint (EC-241). Kept in\n * this leaf module so `messages.en.ts` can build the catalog without importing the\n * resolver (which would import studio runtime code).\n */\nexport function msg(message: string, description?: string): SourceMessage {\n return description !== undefined ? { message, description } : { message }\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { activeDeploymentKey, selectPersistenceAdapter } from '../persistence/index.js';
2
2
  import { attachSessionExpiryReporting } from '../session-expiry.js';
3
3
  import { attachConfiguredRevalidationWebhooks } from '../webhooks/index.js';
4
+ import { sessionValidateDocument } from '../cms/document-gate.js';
4
5
  import { attachReferenceIndex, createChangesets, createOperations, createRoleAuthorization, } from './operations-package.js';
5
6
  /**
6
7
  * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):
@@ -15,7 +16,17 @@ export function createStudioOperations(adapter, options = {}) {
15
16
  const authorized = options.actor
16
17
  ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }
17
18
  : {};
18
- const operations = createOperations({ adapter, ...authorized });
19
+ // EC-497: the publish gate validates with the CMS session's canonical option
20
+ // bag — the same one the detail editor, bulk publish, cell edit, import and
21
+ // every AI path use — instead of deciding its own (weaker, mode-dependent)
22
+ // strength off the adapter-served schema record. The seam reads a validator the
23
+ // mounted session published; with no session it returns `undefined` and the gate
24
+ // takes its documented fallback.
25
+ const operations = createOperations({
26
+ adapter,
27
+ validateDocument: sessionValidateDocument,
28
+ ...authorized,
29
+ });
19
30
  // EC-277: when the adapter maintains the index in its own write path (AD-6 "at the
20
31
  // source" — the Convex provider does this server-side, so a raw `convex run` is
21
32
  // caught too), the client-side subscriber would only double the work. Attach it
@@ -25,7 +36,13 @@ export function createStudioOperations(adapter, options = {}) {
25
36
  const references = adapter.maintainsReferenceIndex
26
37
  ? noopReferenceIndexAttachment()
27
38
  : attachReferenceIndex(operations.events, adapter);
28
- const changesets = createChangesets({ adapter, events: operations.events, ...authorized });
39
+ const changesets = createChangesets({
40
+ adapter,
41
+ events: operations.events,
42
+ // EC-497: a changeset-driven publish is gated identically — same seam, same bag.
43
+ validateDocument: sessionValidateDocument,
44
+ ...authorized,
45
+ });
29
46
  // EC-146 instant publish: when a host revalidate endpoint is configured via
30
47
  // env, every successful publish-shaped command (document upsert/delete/
31
48
  // setState, graph publish/unpublish) fires a signed revalidation webhook.
@@ -1 +1 @@
1
- {"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n const operations = createOperations({ adapter, ...authorized })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({ adapter, events: operations.events, ...authorized })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
1
+ {"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport { sessionValidateDocument } from '../cms/document-gate'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n // EC-497: the publish gate validates with the CMS session's canonical option\n // bag — the same one the detail editor, bulk publish, cell edit, import and\n // every AI path use — instead of deciding its own (weaker, mode-dependent)\n // strength off the adapter-served schema record. The seam reads a validator the\n // mounted session published; with no session it returns `undefined` and the gate\n // takes its documented fallback.\n const operations = createOperations({\n adapter,\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({\n adapter,\n events: operations.events,\n // EC-497: a changeset-driven publish is gated identically — same seam, same bag.\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
@@ -2,6 +2,16 @@ import { checkDeploymentReady, expectedDeploymentEnv } from '@elytracms/core/exp
2
2
  /** Map the studio's resolved state onto the core readiness facts (pure). */
3
3
  export function studioReadinessFacts(inputs) {
4
4
  const { backend, workspace, collections, reachable, effectiveUrl } = inputs;
5
+ // The `config` fact carries `issueCount` only on the INVALID branch — deliberately,
6
+ // not by omission (EC-506, decided 2026-08-01). `elytra doctor` sets it on the valid
7
+ // branch too (`cli/src/doctor.js` `configFactsFromOutcome`), which is what makes
8
+ // `deployment-ready.ts`'s `config-valid` `warn` branch reachable there and not here.
9
+ // The studio cannot supply it: `WorkspaceState`'s `config` variant carries no issues,
10
+ // deliberately (see `lib/workspace/workspace-source.ts` for the full argument and the
11
+ // resurface trigger). The divergence is LATENT, not live — the resolver has no
12
+ // warning-severity producer, so on a valid config `issues.length` is 0 and both tools
13
+ // render the same `ok`. When the first config warning exists, close this with the
14
+ // rest of EC-506, not on its own.
5
15
  const config = workspace.kind === 'invalid'
6
16
  ? { valid: false, source: workspace.source, issueCount: workspace.issues.length }
7
17
  : workspace.kind === 'config'
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-readiness.js","sourceRoot":"","sources":["../../../src/lib/persistence/deployment-readiness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAA;AA2C1G,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,MAA6B;IAChE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,MAAM,CAAA;IAE3E,MAAM,MAAM,GACV,SAAS,CAAC,IAAI,KAAK,SAAS;QAC1B,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE;QACjF,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,QAAQ;YAC3B,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE;YAC3C,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAEvB,MAAM,WAAW,GACf,OAAO,CAAC,IAAI,KAAK,QAAQ;QACvB,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,IAAI,OAAO,CAAC,GAAG,EAAE;QACtD,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;YAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE;YACxB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAA;IAExD,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,kFAAkF;IAClF,MAAM,aAAa,GACjB,WAAW,CAAC,IAAI,KAAK,QAAQ;QAC3B,CAAC,CAAC;YACE,QAAQ,EAAE,qBAAqB,CAAC;gBAC9B,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,KAAK;gBACpD,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChE,CAAC;YACF,iBAAiB,EAAE,kFAAkF;SACtG;QACH,CAAC,CAAC,SAAS,CAAA;IAEf,OAAO;QACL,OAAO,EAAE,WAAW;QACpB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,WAAW;QACX,MAAM;QACN,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5C,CAAA;AACH,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,yBAAyB,CAAC,MAA6B;IACrE,OAAO,oBAAoB,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAA;AAC3D,CAAC","sourcesContent":["import { checkDeploymentReady, expectedDeploymentEnv } from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeclaredEnvVar,\n DeploymentReadinessFacts,\n DeploymentReadinessReport,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { StudioBackendConfig } from './backend-mode'\nimport type { WorkspaceState } from '../workspace'\n\n/**\n * Studio-side adapter for the shared `checkDeploymentReady` contract (EC-258).\n * Maps the studio's resolved state — backend mode (`resolveBackendConfig`),\n * workspace config (`getWorkspaceState`), config-owned collections\n * (`resolveDeploymentCollections`), and an optional reachability probe outcome —\n * onto the core-owned {@link DeploymentReadinessFacts}, then composes the report.\n *\n * A PURE mapper: the async I/O (the `adapter.status()` reachability probe) lives in\n * the boot `DeploymentSetupGate`, which passes the outcome in. `authKeys` is\n * deliberately left unprobed here — the deployment's JWKS endpoint is CORS-blocked\n * from the browser, so the auth-signing-keys check resolves to `unknown` at boot\n * (EC-257's honest sign-in error is the browser's actionable surface; the\n * `elytra doctor` CLI probes it definitively from node). `session` is left unset —\n * this gate runs before auth, so the per-user bucket stays `unknown` (never a false\n * fail) until a signed-in surface supplies it.\n */\nexport interface StudioReadinessInputs {\n backend: StudioBackendConfig\n /** The active project's effective deployment URL (`resolveDeploymentUrl`); defaults to `backend.url`. */\n effectiveUrl?: string\n workspace: WorkspaceState\n collections: readonly CollectionDef[]\n /** Reachability probe outcome (`adapter.status()`); omitted when not probed (playground / unconfigured). */\n reachable?: { available: boolean; detail?: string }\n /**\n * Whether `defineStudio({ ai })` is configured and enabled (EC-399) — drives the\n * AI provider-key expectation in the deployment-env checks.\n */\n aiRouteConfigured?: boolean\n /** Consumer-declared deployment-env expectations (EC-399) — merged into the engine contract. */\n declaredEnv?: readonly DeclaredEnvVar[]\n}\n\n/** Map the studio's resolved state onto the core readiness facts (pure). */\nexport function studioReadinessFacts(inputs: StudioReadinessInputs): DeploymentReadinessFacts {\n const { backend, workspace, collections, reachable, effectiveUrl } = inputs\n\n const config: DeploymentReadinessFacts['config'] =\n workspace.kind === 'invalid'\n ? { valid: false, source: workspace.source, issueCount: workspace.issues.length }\n : workspace.kind === 'config'\n ? { valid: true, source: workspace.source }\n : { valid: true }\n\n const backendFact: DeploymentReadinessFacts['backend'] =\n backend.kind === 'convex'\n ? { kind: 'convex', url: effectiveUrl ?? backend.url }\n : backend.kind === 'playground'\n ? { kind: 'playground' }\n : { kind: 'unconfigured', detail: backend.detail }\n\n // Expected-env facts (EC-399): the studio surfaces the SAME contract the CLI\n // checks, but the browser can read neither the Convex deployment env nor the\n // studio host's server env (the AI key is deliberately never VITE_-prefixed) —\n // so no presence source is gathered and every env check surfaces as an honest\n // `unknown` pointing at `elytra doctor`, exactly the auth-signing-keys precedent.\n const deploymentEnv: DeploymentReadinessFacts['deploymentEnv'] =\n backendFact.kind === 'convex'\n ? {\n expected: expectedDeploymentEnv({\n aiRouteConfigured: inputs.aiRouteConfigured ?? false,\n ...(inputs.declaredEnv ? { declared: inputs.declaredEnv } : {}),\n }),\n unavailableDetail: 'Not readable from the browser — run `elytra doctor` to check it (presence only).',\n }\n : undefined\n\n return {\n backend: backendFact,\n ...(reachable ? { reachable } : {}),\n collections,\n config,\n ...(deploymentEnv ? { deploymentEnv } : {}),\n }\n}\n\n/** Compose the studio's deployment-readiness report (pure). */\nexport function studioDeploymentReadiness(inputs: StudioReadinessInputs): DeploymentReadinessReport {\n return checkDeploymentReady(studioReadinessFacts(inputs))\n}\n"]}
1
+ {"version":3,"file":"deployment-readiness.js","sourceRoot":"","sources":["../../../src/lib/persistence/deployment-readiness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAA;AA2C1G,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,MAA6B;IAChE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,MAAM,CAAA;IAE3E,oFAAoF;IACpF,qFAAqF;IACrF,iFAAiF;IACjF,qFAAqF;IACrF,sFAAsF;IACtF,sFAAsF;IACtF,+EAA+E;IAC/E,sFAAsF;IACtF,kFAAkF;IAClF,kCAAkC;IAClC,MAAM,MAAM,GACV,SAAS,CAAC,IAAI,KAAK,SAAS;QAC1B,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE;QACjF,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,QAAQ;YAC3B,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE;YAC3C,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAEvB,MAAM,WAAW,GACf,OAAO,CAAC,IAAI,KAAK,QAAQ;QACvB,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,IAAI,OAAO,CAAC,GAAG,EAAE;QACtD,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;YAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE;YACxB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAA;IAExD,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,kFAAkF;IAClF,MAAM,aAAa,GACjB,WAAW,CAAC,IAAI,KAAK,QAAQ;QAC3B,CAAC,CAAC;YACE,QAAQ,EAAE,qBAAqB,CAAC;gBAC9B,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,KAAK;gBACpD,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChE,CAAC;YACF,iBAAiB,EAAE,kFAAkF;SACtG;QACH,CAAC,CAAC,SAAS,CAAA;IAEf,OAAO;QACL,OAAO,EAAE,WAAW;QACpB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,WAAW;QACX,MAAM;QACN,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5C,CAAA;AACH,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,yBAAyB,CAAC,MAA6B;IACrE,OAAO,oBAAoB,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAA;AAC3D,CAAC","sourcesContent":["import { checkDeploymentReady, expectedDeploymentEnv } from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeclaredEnvVar,\n DeploymentReadinessFacts,\n DeploymentReadinessReport,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { StudioBackendConfig } from './backend-mode'\nimport type { WorkspaceState } from '../workspace'\n\n/**\n * Studio-side adapter for the shared `checkDeploymentReady` contract (EC-258).\n * Maps the studio's resolved state — backend mode (`resolveBackendConfig`),\n * workspace config (`getWorkspaceState`), config-owned collections\n * (`resolveDeploymentCollections`), and an optional reachability probe outcome —\n * onto the core-owned {@link DeploymentReadinessFacts}, then composes the report.\n *\n * A PURE mapper: the async I/O (the `adapter.status()` reachability probe) lives in\n * the boot `DeploymentSetupGate`, which passes the outcome in. `authKeys` is\n * deliberately left unprobed here — the deployment's JWKS endpoint is CORS-blocked\n * from the browser, so the auth-signing-keys check resolves to `unknown` at boot\n * (EC-257's honest sign-in error is the browser's actionable surface; the\n * `elytra doctor` CLI probes it definitively from node). `session` is left unset —\n * this gate runs before auth, so the per-user bucket stays `unknown` (never a false\n * fail) until a signed-in surface supplies it.\n */\nexport interface StudioReadinessInputs {\n backend: StudioBackendConfig\n /** The active project's effective deployment URL (`resolveDeploymentUrl`); defaults to `backend.url`. */\n effectiveUrl?: string\n workspace: WorkspaceState\n collections: readonly CollectionDef[]\n /** Reachability probe outcome (`adapter.status()`); omitted when not probed (playground / unconfigured). */\n reachable?: { available: boolean; detail?: string }\n /**\n * Whether `defineStudio({ ai })` is configured and enabled (EC-399) — drives the\n * AI provider-key expectation in the deployment-env checks.\n */\n aiRouteConfigured?: boolean\n /** Consumer-declared deployment-env expectations (EC-399) — merged into the engine contract. */\n declaredEnv?: readonly DeclaredEnvVar[]\n}\n\n/** Map the studio's resolved state onto the core readiness facts (pure). */\nexport function studioReadinessFacts(inputs: StudioReadinessInputs): DeploymentReadinessFacts {\n const { backend, workspace, collections, reachable, effectiveUrl } = inputs\n\n // The `config` fact carries `issueCount` only on the INVALID branch — deliberately,\n // not by omission (EC-506, decided 2026-08-01). `elytra doctor` sets it on the valid\n // branch too (`cli/src/doctor.js` `configFactsFromOutcome`), which is what makes\n // `deployment-ready.ts`'s `config-valid` `warn` branch reachable there and not here.\n // The studio cannot supply it: `WorkspaceState`'s `config` variant carries no issues,\n // deliberately (see `lib/workspace/workspace-source.ts` for the full argument and the\n // resurface trigger). The divergence is LATENT, not live — the resolver has no\n // warning-severity producer, so on a valid config `issues.length` is 0 and both tools\n // render the same `ok`. When the first config warning exists, close this with the\n // rest of EC-506, not on its own.\n const config: DeploymentReadinessFacts['config'] =\n workspace.kind === 'invalid'\n ? { valid: false, source: workspace.source, issueCount: workspace.issues.length }\n : workspace.kind === 'config'\n ? { valid: true, source: workspace.source }\n : { valid: true }\n\n const backendFact: DeploymentReadinessFacts['backend'] =\n backend.kind === 'convex'\n ? { kind: 'convex', url: effectiveUrl ?? backend.url }\n : backend.kind === 'playground'\n ? { kind: 'playground' }\n : { kind: 'unconfigured', detail: backend.detail }\n\n // Expected-env facts (EC-399): the studio surfaces the SAME contract the CLI\n // checks, but the browser can read neither the Convex deployment env nor the\n // studio host's server env (the AI key is deliberately never VITE_-prefixed) —\n // so no presence source is gathered and every env check surfaces as an honest\n // `unknown` pointing at `elytra doctor`, exactly the auth-signing-keys precedent.\n const deploymentEnv: DeploymentReadinessFacts['deploymentEnv'] =\n backendFact.kind === 'convex'\n ? {\n expected: expectedDeploymentEnv({\n aiRouteConfigured: inputs.aiRouteConfigured ?? false,\n ...(inputs.declaredEnv ? { declared: inputs.declaredEnv } : {}),\n }),\n unavailableDetail: 'Not readable from the browser — run `elytra doctor` to check it (presence only).',\n }\n : undefined\n\n return {\n backend: backendFact,\n ...(reachable ? { reachable } : {}),\n collections,\n config,\n ...(deploymentEnv ? { deploymentEnv } : {}),\n }\n}\n\n/** Compose the studio's deployment-readiness report (pure). */\nexport function studioDeploymentReadiness(inputs: StudioReadinessInputs): DeploymentReadinessReport {\n return checkDeploymentReady(studioReadinessFacts(inputs))\n}\n"]}
@@ -1,16 +1,21 @@
1
- import type { IssueCode, ConditionOperator } from '@elytracms/core/project-graph';
2
- import type { CmsIssueCode, CmsValidationIssue } from '@elytracms/core/cms-core';
3
- import type { OperationErrorCode } from '@elytracms/core/operations';
1
+ import type { CmsIssueCode, CmsIssueSeverity, CmsValidationIssue } from '@elytracms/core/cms-core';
2
+ import type { OperationError, OperationErrorCode, OperationIssue } from '@elytracms/core/operations';
3
+ import type { BuildFragmentError, FragmentParseError } from '@elytracms/core/clipboard';
4
+ import type { ContentQueryError } from '@elytracms/core/content';
4
5
  import type { CustomizationIssue } from '@elytracms/core/studio-core';
5
6
  import type { ComponentManifest } from '@elytracms/core/component-registry';
6
- /** Plain-language title for a graph validation issue code. */
7
- export declare function graphIssueLabel(code: IssueCode): string;
8
7
  /**
9
8
  * Plain-language title for a CMS validation issue code — localized against the
10
9
  * active studio-UI locale (EC-443). A runtime-unknown code (impossible typewise;
11
10
  * a cast could smuggle one) degrades to its humanized form, never the raw enum.
11
+ *
12
+ * `severity` is optional and additive (EC-507): pass the issue's own severity and a
13
+ * code with a softer warning title (see {@link CMS_ISSUE_WARNING_LABEL_KEYS}) uses
14
+ * it; omit it, or pass `'error'`, and nothing changes. Every render site passes it —
15
+ * the parameter is optional so that a pure/unit caller naming only a code still
16
+ * compiles, not because a surface may skip it.
12
17
  */
13
- export declare function cmsIssueLabel(code: CmsIssueCode): string;
18
+ export declare function cmsIssueLabel(code: CmsIssueCode, severity?: CmsIssueSeverity): string;
14
19
  /**
15
20
  * The localized text of a {@link CustomizationIssue} (EC-478) — the ONE place the
16
21
  * studio turns a config-level finding into rendered prose.
@@ -51,12 +56,42 @@ export declare function customizationIssueMessage(issue: CustomizationIssue): st
51
56
  * beats a missing key.
52
57
  */
53
58
  export declare function cmsValidationIssueMessage(issue: CmsValidationIssue): string;
54
- /** Plain-language title for an operation failure code. */
59
+ /**
60
+ * The localized text of ONE {@link OperationIssue} line in the error banner (EC-494).
61
+ *
62
+ * These issues cross a process boundary: `operations/documents.ts` rebuilds a
63
+ * `CmsValidationIssue` as an `OperationIssue` **server-side, with no locale**, so the
64
+ * producer cannot translate and the catalog key rides on the wire instead. A ref-less
65
+ * issue — a Zod input diagnostic, a persistence-layer message, a third-party producer
66
+ * of this published type — keeps its English.
67
+ */
68
+ export declare function operationIssueMessage(issue: OperationIssue): string;
69
+ /**
70
+ * The localized ENVELOPE sentence of an {@link OperationError} (EC-494) — the line
71
+ * beside the localized badge in `OperationErrorBanner`. Localizing only the issue
72
+ * lines would have left exactly one English sentence under a German badge.
73
+ */
74
+ export declare function operationErrorMessage(error: OperationError): string;
75
+ /**
76
+ * The localized text of a clipboard fragment failure (EC-494) — parse (`corrupt-json`,
77
+ * `foreign-format`, `unsupported-version`, `invalid-payload`) or build
78
+ * (`node-not-found`, `fixture-not-found`), rendered by the builder's paste notice.
79
+ */
80
+ export declare function clipboardErrorMessage(error: FragmentParseError | BuildFragmentError): string;
81
+ /**
82
+ * The localized text of a {@link ContentQueryError} (EC-494) — the filter builder's
83
+ * per-clause line and the canvas listing notice, both of which render errors built by
84
+ * `core/content`'s `validateWhere`/`validateSort`/`validateLimit`.
85
+ */
86
+ export declare function contentQueryErrorMessage(error: ContentQueryError): string;
87
+ /**
88
+ * Plain-language title for an operation failure code — localized against the active
89
+ * studio-UI locale. A runtime-unknown code (impossible typewise; a cast could smuggle
90
+ * one) degrades to the generic title, never the raw enum.
91
+ */
55
92
  export declare function operationErrorLabel(code: OperationErrorCode): string;
56
93
  /** Friendly name for a project export target (`nextjs` → `Next.js`). */
57
94
  export declare function exportTargetLabel(target: string): string;
58
- /** Plain-language label for a condition operator (`gte` → `At least`). */
59
- export declare function conditionOperatorLabel(operator: ConditionOperator): string;
60
95
  /** The leaf of a component id, namespace stripped (`base.Heading` → `Heading`). */
61
96
  export declare function componentLeafId(componentId: string): string;
62
97
  /**
@@ -6,35 +6,49 @@ import { humanizeName } from '../utils.js';
6
6
  *
7
7
  * A non-technical editor must never see a manifest/binding/path-token/validation
8
8
  * identifier on — or one click off — the editor path. The engine speaks in stable
9
- * machine codes (`issueCodeSchema`, `cmsIssueCodeSchema`, `operationErrorCodeSchema`)
10
- * and machine ids (`base.Heading`, `r-blog-slug-en`); this module is the
11
- * single translation layer the editor-facing surfaces route through.
9
+ * machine codes (`cmsIssueCodeSchema`, `operationErrorCodeSchema`) and machine ids
10
+ * (`base.Heading`, `r-blog-slug-en`); this module is the single translation layer the
11
+ * editor-facing surfaces route through.
12
12
  *
13
13
  * The maps are exhaustive `Record<…Code, string>` over the Zod enums, so adding a
14
- * code in `@elytracms/core/project-graph`, `@elytracms/core/cms-core`, or `@elytracms/core/operations`
14
+ * code in `@elytracms/core/cms-core` or `@elytracms/core/operations`
15
15
  * fails typecheck here until it is given an editor-facing label — the persona
16
- * boundary cannot silently regress. Engineers keep the raw code: every surface that
16
+ * boundary cannot silently regress. A table is only worth that guard while something
17
+ * renders it: EC-509 deleted two that nothing did (see below). Engineers keep the raw
18
+ * code: every surface that
17
19
  * shows a label also carries the code in a `title=`/`data-*` attribute (inspectable
18
20
  * via hover/devtools, never rendered as visible copy).
19
21
  */
20
- const GRAPH_ISSUE_LABELS = {
21
- 'invalid-component-name': 'Invalid component',
22
- 'unknown-component': 'Unknown component',
23
- 'duplicate-node-id': 'Duplicate item',
24
- 'unknown-prop': 'Unknown setting',
25
- 'invalid-prop': 'Invalid setting',
26
- 'binding-not-allowed': 'Data not allowed here',
27
- 'unresolved-binding': 'Missing data',
28
- 'missing-required-slot': 'Missing content',
29
- 'unknown-slot': 'Unknown content area',
30
- 'forbidden-component': 'Not allowed here',
31
- 'server-only-component': 'Not available in the editor',
32
- 'invalid-container-direction': 'Invalid layout',
33
- 'invalid-container-alignment': 'Invalid alignment',
34
- 'invalid-container-gap': 'Invalid spacing',
35
- 'invalid-container-columns': 'Invalid columns',
36
- 'invalid-container-responsive': 'Invalid responsive setting',
37
- };
22
+ /**
23
+ * EC-509 — two label tables were DELETED here rather than localized, because measuring
24
+ * first showed nothing rendered them.
25
+ *
26
+ * `GRAPH_ISSUE_LABELS`/`graphIssueLabel` (16 entries, project-graph `IssueCode`) and
27
+ * `CONDITION_OPERATOR_LABELS`/`conditionOperatorLabel` (8, `ConditionOperator`) had
28
+ * **zero call sites** outside this file and its own test — measured by grep over
29
+ * `packages/` + `apps/`, with `cmsIssueLabel` (4 sites) and `operationErrorLabel` (1)
30
+ * as the known-positive control that the search finds a render site when one exists.
31
+ *
32
+ * The graph table was unreachable structurally, not just unused: the ONE live path from
33
+ * graph validation into the studio is `cms-composition-validators.ts`, which maps a
34
+ * graph issue to `CompositionIssue` — a type with `message`/`severity`/`nodePath` and
35
+ * **no `code` field at all** (`cms-core/validate-document.ts:809`). The code an
36
+ * exhaustiveness guard exists to force a label for cannot reach an editor surface.
37
+ * `ConditionOperator` is live in the runtime renderer's condition gate
38
+ * (`runtime-renderer/render.tsx:48`) but has no studio surface: v1 canvas is static
39
+ * props, editor bindings parked, so no inspector ever shows an operator to name.
40
+ *
41
+ * **This overturns an EC-210 decision** (2026-06-17), which kept `graphIssueLabel` when
42
+ * the validation panel was removed, on the rationale that it was "the EC-192 boundary's
43
+ * exhaustiveness guard, not panel code". That was right then and its premise is false
44
+ * now — the guard guards a boundary with no traffic, so it only forced editor copy for
45
+ * strings no editor can see. Localizing them would have added 24 keys to the same
46
+ * effect. If a surface ever renders these codes it needs labels written for it, and
47
+ * will write them localized from the start.
48
+ *
49
+ * Neither symbol is referenced by either consumer repo (measured read-only) and neither
50
+ * appears in docs.
51
+ */
38
52
  /**
39
53
  * EC-443: the CMS issue titles live in the EC-241 message catalog
40
54
  * (`cms.issue.<code>`, EN source + shipped de), so the Issues pane / for-review
@@ -69,46 +83,61 @@ const CMS_ISSUE_LABEL_KEYS = {
69
83
  'unknown-alias': 'cms.issue.unknown-alias',
70
84
  'unfilled-slot': 'cms.issue.unfilled-slot',
71
85
  'custom-validation': 'cms.issue.custom-validation',
86
+ 'unaddressable-url': 'cms.issue.unaddressable-url',
72
87
  'unknown-version': 'cms.issue.unknown-version',
73
88
  };
74
- const OPERATION_ERROR_LABELS = {
75
- validation: "Couldn't save",
76
- 'not-found': 'Not found',
77
- conflict: 'Already exists',
78
- 'backend-unavailable': 'Connection problem',
79
- unauthorized: 'Not allowed',
80
- unknown: 'Something went wrong',
89
+ /**
90
+ * EC-507 rider — the few codes whose TITLE overstates at `warning` severity.
91
+ *
92
+ * `invalid-field-value` reads "Invalid value", which is right when the value is
93
+ * rejected (wrong type, not among its options) and wrong when it is not: EC-472
94
+ * deliberately STORES `0202-09-18` as a legal ISO date and merely flags the year as
95
+ * probably unintended. A title calling that invalid contradicts the value the editor
96
+ * can plainly see was accepted, on all three surfaces at once.
97
+ *
98
+ * Keyed on SEVERITY rather than on the implausible-year case specifically, because
99
+ * the overstatement belongs to the severity: any future warning under this code has
100
+ * the same property (the value was kept), and the fix then needs no new entry. A
101
+ * sparse `Partial` on purpose — a code with no entry keeps one title for both
102
+ * severities, which is the correct default and keeps
103
+ * {@link CMS_ISSUE_LABEL_KEYS} the single exhaustive map.
104
+ */
105
+ const CMS_ISSUE_WARNING_LABEL_KEYS = {
106
+ 'invalid-field-value': 'cms.issue.invalid-field-value.warning',
107
+ };
108
+ /**
109
+ * EC-494: the badge titles live in the EC-241 catalog (`operations.errorLabel.<code>`),
110
+ * exactly as EC-443 moved the CMS issue titles. **Measured before changing it:** this
111
+ * map was a hard-coded English `Record`, so once the EC-494 bodies localized the banner
112
+ * rendered a German sentence under an English badge — the inverse of the bug this
113
+ * ticket exists for, and not what the ticket said was there. Still the exhaustive
114
+ * `Record` over the Zod enum, so a new code fails typecheck until it has a key.
115
+ */
116
+ const OPERATION_ERROR_LABEL_KEYS = {
117
+ validation: 'operations.errorLabel.validation',
118
+ 'not-found': 'operations.errorLabel.not-found',
119
+ conflict: 'operations.errorLabel.conflict',
120
+ 'backend-unavailable': 'operations.errorLabel.backend-unavailable',
121
+ unauthorized: 'operations.errorLabel.unauthorized',
122
+ unknown: 'operations.errorLabel.unknown',
81
123
  };
82
124
  const EXPORT_TARGET_LABELS = {
83
125
  nextjs: 'Next.js',
84
126
  };
85
- /**
86
- * Plain-language labels for condition operators. Exhaustive over
87
- * `conditionOperatorSchema` — a new operator fails typecheck here until it has an
88
- * editor-facing label. The raw operator (`eq`, `gte`, …) is a machine token an
89
- * editor must never read in the inspector.
90
- */
91
- const CONDITION_OPERATOR_LABELS = {
92
- truthy: 'Is set',
93
- exists: 'Exists',
94
- eq: 'Equals',
95
- neq: 'Does not equal',
96
- gt: 'Greater than',
97
- gte: 'At least',
98
- lt: 'Less than',
99
- lte: 'At most',
100
- };
101
- /** Plain-language title for a graph validation issue code. */
102
- export function graphIssueLabel(code) {
103
- return GRAPH_ISSUE_LABELS[code];
104
- }
105
127
  /**
106
128
  * Plain-language title for a CMS validation issue code — localized against the
107
129
  * active studio-UI locale (EC-443). A runtime-unknown code (impossible typewise;
108
130
  * a cast could smuggle one) degrades to its humanized form, never the raw enum.
131
+ *
132
+ * `severity` is optional and additive (EC-507): pass the issue's own severity and a
133
+ * code with a softer warning title (see {@link CMS_ISSUE_WARNING_LABEL_KEYS}) uses
134
+ * it; omit it, or pass `'error'`, and nothing changes. Every render site passes it —
135
+ * the parameter is optional so that a pure/unit caller naming only a code still
136
+ * compiles, not because a surface may skip it.
109
137
  */
110
- export function cmsIssueLabel(code) {
111
- const key = CMS_ISSUE_LABEL_KEYS[code];
138
+ export function cmsIssueLabel(code, severity) {
139
+ const key = (severity === 'warning' ? CMS_ISSUE_WARNING_LABEL_KEYS[code] : undefined) ??
140
+ CMS_ISSUE_LABEL_KEYS[code];
112
141
  return key !== undefined ? translate(key) : humanizeName(code);
113
142
  }
114
143
  /**
@@ -159,27 +188,81 @@ export function customizationIssueMessage(issue) {
159
188
  * beats a missing key.
160
189
  */
161
190
  export function cmsValidationIssueMessage(issue) {
162
- const ref = issue.messageRef;
191
+ return resolveMessageRef(issue.messageRef, issue.message);
192
+ }
193
+ /**
194
+ * EC-494 — the ONE resolver every carried message ref goes through.
195
+ *
196
+ * Four published types now carry the same `{ key, vars?, details? }` reference —
197
+ * `CmsValidationIssue` (EC-487), `CustomizationIssue` (EC-478, via its own `reason`
198
+ * slot), and now `OperationIssue`/`OperationError` and the clipboard + content-query
199
+ * errors. They are separate declarations only because the EC-265 DAG forbids the
200
+ * lower subpaths from importing `cms-core`; the RESOLUTION is one rule, so it lives
201
+ * in one function.
202
+ *
203
+ * `fallback` is the producer's prebuilt English, rendered whenever there is no ref —
204
+ * a third-party producer, a Zod diagnostic, a persistence-layer message, a
205
+ * config-authored `validate` string. A visible English sentence beats a missing key
206
+ * (EC-443).
207
+ */
208
+ function resolveMessageRef(ref, fallback) {
163
209
  if (!ref)
164
- return issue.message;
210
+ return fallback;
165
211
  const vars = { ...ref.vars };
166
212
  for (const [placeholder, nested] of Object.entries(ref.details ?? {})) {
167
213
  vars[placeholder] = translate(nested.key, nested.vars);
168
214
  }
169
215
  return translate(ref.key, vars);
170
216
  }
171
- /** Plain-language title for an operation failure code. */
217
+ /**
218
+ * The localized text of ONE {@link OperationIssue} line in the error banner (EC-494).
219
+ *
220
+ * These issues cross a process boundary: `operations/documents.ts` rebuilds a
221
+ * `CmsValidationIssue` as an `OperationIssue` **server-side, with no locale**, so the
222
+ * producer cannot translate and the catalog key rides on the wire instead. A ref-less
223
+ * issue — a Zod input diagnostic, a persistence-layer message, a third-party producer
224
+ * of this published type — keeps its English.
225
+ */
226
+ export function operationIssueMessage(issue) {
227
+ return resolveMessageRef(issue.messageRef, issue.message);
228
+ }
229
+ /**
230
+ * The localized ENVELOPE sentence of an {@link OperationError} (EC-494) — the line
231
+ * beside the localized badge in `OperationErrorBanner`. Localizing only the issue
232
+ * lines would have left exactly one English sentence under a German badge.
233
+ */
234
+ export function operationErrorMessage(error) {
235
+ return resolveMessageRef(error.messageRef, error.message);
236
+ }
237
+ /**
238
+ * The localized text of a clipboard fragment failure (EC-494) — parse (`corrupt-json`,
239
+ * `foreign-format`, `unsupported-version`, `invalid-payload`) or build
240
+ * (`node-not-found`, `fixture-not-found`), rendered by the builder's paste notice.
241
+ */
242
+ export function clipboardErrorMessage(error) {
243
+ return resolveMessageRef(error.messageRef, error.message);
244
+ }
245
+ /**
246
+ * The localized text of a {@link ContentQueryError} (EC-494) — the filter builder's
247
+ * per-clause line and the canvas listing notice, both of which render errors built by
248
+ * `core/content`'s `validateWhere`/`validateSort`/`validateLimit`.
249
+ */
250
+ export function contentQueryErrorMessage(error) {
251
+ return resolveMessageRef(error.messageRef, error.message);
252
+ }
253
+ /**
254
+ * Plain-language title for an operation failure code — localized against the active
255
+ * studio-UI locale. A runtime-unknown code (impossible typewise; a cast could smuggle
256
+ * one) degrades to the generic title, never the raw enum.
257
+ */
172
258
  export function operationErrorLabel(code) {
173
- return OPERATION_ERROR_LABELS[code] ?? OPERATION_ERROR_LABELS.unknown;
259
+ const key = OPERATION_ERROR_LABEL_KEYS[code];
260
+ return translate(key ?? OPERATION_ERROR_LABEL_KEYS.unknown);
174
261
  }
175
262
  /** Friendly name for a project export target (`nextjs` → `Next.js`). */
176
263
  export function exportTargetLabel(target) {
177
264
  return EXPORT_TARGET_LABELS[target] ?? humanizeName(target);
178
265
  }
179
- /** Plain-language label for a condition operator (`gte` → `At least`). */
180
- export function conditionOperatorLabel(operator) {
181
- return CONDITION_OPERATOR_LABELS[operator];
182
- }
183
266
  /** The leaf of a component id, namespace stripped (`base.Heading` → `Heading`). */
184
267
  export function componentLeafId(componentId) {
185
268
  const dot = componentId.lastIndexOf('.');