@elytracms/studio 0.0.92 → 0.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/convex/asset-alt-migration.test.ts +127 -0
  2. package/convex/asset-replace.test.ts +168 -0
  3. package/convex/asset-revalidate.test.ts +6 -6
  4. package/convex/assets.ts +41 -5
  5. package/convex/cms.ts +25 -11
  6. package/convex/contract-parity.test.ts +18 -3
  7. package/convex/document-revalidate.test.ts +10 -12
  8. package/convex/orphan-hydration.test.ts +116 -0
  9. package/convex/rail-write-parity.test.ts +11 -8
  10. package/convex/schema.ts +6 -0
  11. package/dist/components/ai/proposal-card.js +2 -1
  12. package/dist/components/ai/proposal-card.js.map +1 -1
  13. package/dist/components/ai/translate-document-dialog.js +4 -2
  14. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  15. package/dist/components/ai/translate-skipped.d.ts +19 -0
  16. package/dist/components/ai/translate-skipped.js +46 -0
  17. package/dist/components/ai/translate-skipped.js.map +1 -0
  18. package/dist/components/assets/asset-detail.js +18 -8
  19. package/dist/components/assets/asset-detail.js.map +1 -1
  20. package/dist/components/assets/asset-metadata.d.ts +15 -4
  21. package/dist/components/assets/asset-metadata.js +15 -2
  22. package/dist/components/assets/asset-metadata.js.map +1 -1
  23. package/dist/components/assets/asset-search.js +4 -1
  24. package/dist/components/assets/asset-search.js.map +1 -1
  25. package/dist/components/assets/asset-selection.d.ts +0 -8
  26. package/dist/components/assets/asset-selection.js +4 -23
  27. package/dist/components/assets/asset-selection.js.map +1 -1
  28. package/dist/components/assets/asset-thumb.js +3 -2
  29. package/dist/components/assets/asset-thumb.js.map +1 -1
  30. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  31. package/dist/components/assets/assets-collection-view-source.js +9 -5
  32. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  33. package/dist/components/assets/assets-store.d.ts +14 -0
  34. package/dist/components/assets/assets-store.js +23 -1
  35. package/dist/components/assets/assets-store.js.map +1 -1
  36. package/dist/components/assets/replace-file.d.ts +22 -0
  37. package/dist/components/assets/replace-file.js +59 -0
  38. package/dist/components/assets/replace-file.js.map +1 -0
  39. package/dist/components/builder/blocks-model.d.ts +14 -26
  40. package/dist/components/builder/blocks-model.js +35 -41
  41. package/dist/components/builder/blocks-model.js.map +1 -1
  42. package/dist/components/builder/blocks-panel.d.ts +7 -3
  43. package/dist/components/builder/blocks-panel.js +6 -5
  44. package/dist/components/builder/blocks-panel.js.map +1 -1
  45. package/dist/components/builder/builder-store.d.ts +104 -15
  46. package/dist/components/builder/builder-store.js +156 -31
  47. package/dist/components/builder/builder-store.js.map +1 -1
  48. package/dist/components/builder/canvas-body.d.ts +30 -0
  49. package/dist/components/builder/canvas-body.js +38 -0
  50. package/dist/components/builder/canvas-body.js.map +1 -0
  51. package/dist/components/builder/canvas-chrome-css.js +7 -0
  52. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  53. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  54. package/dist/components/builder/canvas-dnd.js +5 -5
  55. package/dist/components/builder/canvas-dnd.js.map +1 -1
  56. package/dist/components/builder/canvas-render-context.d.ts +27 -19
  57. package/dist/components/builder/canvas-render-context.js +42 -43
  58. package/dist/components/builder/canvas-render-context.js.map +1 -1
  59. package/dist/components/builder/canvas-runs.d.ts +45 -0
  60. package/dist/components/builder/canvas-runs.js +51 -0
  61. package/dist/components/builder/canvas-runs.js.map +1 -0
  62. package/dist/components/builder/canvas.js +32 -5
  63. package/dist/components/builder/canvas.js.map +1 -1
  64. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  65. package/dist/components/builder/clipboard-actions.js +44 -7
  66. package/dist/components/builder/clipboard-actions.js.map +1 -1
  67. package/dist/components/builder/composition-field-editor.d.ts +3 -15
  68. package/dist/components/builder/composition-field-editor.js +29 -40
  69. package/dist/components/builder/composition-field-editor.js.map +1 -1
  70. package/dist/components/builder/delete-key.d.ts +5 -0
  71. package/dist/components/builder/delete-key.js +4 -0
  72. package/dist/components/builder/delete-key.js.map +1 -1
  73. package/dist/components/builder/insert-context.d.ts +17 -25
  74. package/dist/components/builder/insert-context.js +45 -92
  75. package/dist/components/builder/insert-context.js.map +1 -1
  76. package/dist/components/builder/page-tree.d.ts +3 -3
  77. package/dist/components/builder/page-tree.js +3 -3
  78. package/dist/components/builder/page-tree.js.map +1 -1
  79. package/dist/components/builder/placement-refusal.d.ts +18 -0
  80. package/dist/components/builder/placement-refusal.js +46 -0
  81. package/dist/components/builder/placement-refusal.js.map +1 -0
  82. package/dist/components/builder/property-inspector.js +4 -0
  83. package/dist/components/builder/property-inspector.js.map +1 -1
  84. package/dist/components/builder/reuse-context.js +65 -27
  85. package/dist/components/builder/reuse-context.js.map +1 -1
  86. package/dist/components/builder/reuse.d.ts +79 -1
  87. package/dist/components/builder/reuse.js +69 -2
  88. package/dist/components/builder/reuse.js.map +1 -1
  89. package/dist/components/builder/run-inspector.d.ts +11 -0
  90. package/dist/components/builder/run-inspector.js +33 -0
  91. package/dist/components/builder/run-inspector.js.map +1 -0
  92. package/dist/components/cms/asset-field.d.ts +38 -1
  93. package/dist/components/cms/asset-field.js +182 -58
  94. package/dist/components/cms/asset-field.js.map +1 -1
  95. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  96. package/dist/components/cms/asset-picker-core.js +20 -0
  97. package/dist/components/cms/asset-picker-core.js.map +1 -1
  98. package/dist/components/cms/asset-picker.js +12 -18
  99. package/dist/components/cms/asset-picker.js.map +1 -1
  100. package/dist/components/cms/bulk-actions.d.ts +3 -19
  101. package/dist/components/cms/bulk-actions.js +3 -24
  102. package/dist/components/cms/bulk-actions.js.map +1 -1
  103. package/dist/components/cms/cell.js.map +1 -1
  104. package/dist/components/cms/cms-store.d.ts +5 -4
  105. package/dist/components/cms/cms-store.js +16 -36
  106. package/dist/components/cms/cms-store.js.map +1 -1
  107. package/dist/components/cms/detail-editor.js +0 -1
  108. package/dist/components/cms/detail-editor.js.map +1 -1
  109. package/dist/components/cms/document-diff-view.d.ts +3 -1
  110. package/dist/components/cms/document-diff-view.js +12 -7
  111. package/dist/components/cms/document-diff-view.js.map +1 -1
  112. package/dist/components/cms/field-anchors.d.ts +7 -4
  113. package/dist/components/cms/field-anchors.js +9 -7
  114. package/dist/components/cms/field-anchors.js.map +1 -1
  115. package/dist/components/cms/field-control-lib.d.ts +15 -1
  116. package/dist/components/cms/field-control-lib.js +22 -3
  117. package/dist/components/cms/field-control-lib.js.map +1 -1
  118. package/dist/components/cms/gate-validation.d.ts +21 -30
  119. package/dist/components/cms/gate-validation.js +11 -32
  120. package/dist/components/cms/gate-validation.js.map +1 -1
  121. package/dist/components/cms/object-array-field.js +4 -4
  122. package/dist/components/cms/object-array-field.js.map +1 -1
  123. package/dist/components/cms/prop-fields.d.ts +7 -2
  124. package/dist/components/cms/prop-fields.js +13 -6
  125. package/dist/components/cms/prop-fields.js.map +1 -1
  126. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  127. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  128. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  129. package/dist/components/cms/rich-text/insert.js +25 -17
  130. package/dist/components/cms/rich-text/insert.js.map +1 -1
  131. package/dist/components/cms/use-publish-controller.d.ts +1 -2
  132. package/dist/components/cms/use-publish-controller.js +7 -19
  133. package/dist/components/cms/use-publish-controller.js.map +1 -1
  134. package/dist/components/cms/versions-pane.js +8 -1
  135. package/dist/components/cms/versions-pane.js.map +1 -1
  136. package/dist/components/collection-view/cms-collection-view-source.js +3 -1
  137. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  138. package/dist/components/components/transfer-dialog.js +2 -1
  139. package/dist/components/components/transfer-dialog.js.map +1 -1
  140. package/dist/components/components/transfer.d.ts +10 -10
  141. package/dist/components/components/transfer.js +13 -12
  142. package/dist/components/components/transfer.js.map +1 -1
  143. package/dist/lib/ai/apply-content.js +56 -116
  144. package/dist/lib/ai/apply-content.js.map +1 -1
  145. package/dist/lib/ai/chat-context.d.ts +8 -0
  146. package/dist/lib/ai/chat-context.js +37 -6
  147. package/dist/lib/ai/chat-context.js.map +1 -1
  148. package/dist/lib/ai/chat-state.d.ts +6 -0
  149. package/dist/lib/ai/chat-state.js.map +1 -1
  150. package/dist/lib/ai/constraints.d.ts +20 -5
  151. package/dist/lib/ai/constraints.js +125 -33
  152. package/dist/lib/ai/constraints.js.map +1 -1
  153. package/dist/lib/ai/fake-provider.d.ts +16 -3
  154. package/dist/lib/ai/fake-provider.js +22 -6
  155. package/dist/lib/ai/fake-provider.js.map +1 -1
  156. package/dist/lib/ai/index.d.ts +2 -0
  157. package/dist/lib/ai/index.js +2 -0
  158. package/dist/lib/ai/index.js.map +1 -1
  159. package/dist/lib/ai/leash.d.ts +34 -0
  160. package/dist/lib/ai/leash.js +28 -0
  161. package/dist/lib/ai/leash.js.map +1 -0
  162. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  163. package/dist/lib/ai/rich-text-input.js +2 -1
  164. package/dist/lib/ai/rich-text-input.js.map +1 -1
  165. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  166. package/dist/lib/ai/rich-text-translate.js +28 -10
  167. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  168. package/dist/lib/ai/shape.d.ts +13 -13
  169. package/dist/lib/ai/shape.js +69 -93
  170. package/dist/lib/ai/shape.js.map +1 -1
  171. package/dist/lib/ai/tools.d.ts +36 -41
  172. package/dist/lib/ai/tools.js +83 -87
  173. package/dist/lib/ai/tools.js.map +1 -1
  174. package/dist/lib/ai/translate-document.d.ts +32 -4
  175. package/dist/lib/ai/translate-document.js +144 -46
  176. package/dist/lib/ai/translate-document.js.map +1 -1
  177. package/dist/lib/ai/types.d.ts +34 -23
  178. package/dist/lib/ai/types.js.map +1 -1
  179. package/dist/lib/ai/update-fields.d.ts +15 -12
  180. package/dist/lib/ai/update-fields.js +112 -188
  181. package/dist/lib/ai/update-fields.js.map +1 -1
  182. package/dist/lib/ai/writer-form.d.ts +116 -0
  183. package/dist/lib/ai/writer-form.js +429 -0
  184. package/dist/lib/ai/writer-form.js.map +1 -0
  185. package/dist/lib/assets/upload.d.ts +31 -2
  186. package/dist/lib/assets/upload.js +61 -6
  187. package/dist/lib/assets/upload.js.map +1 -1
  188. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  189. package/dist/lib/clipboard/os-clipboard.js +8 -4
  190. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  191. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  192. package/dist/lib/clipboard/paste-target.js +8 -7
  193. package/dist/lib/clipboard/paste-target.js.map +1 -1
  194. package/dist/lib/cms/field-patch.d.ts +0 -10
  195. package/dist/lib/cms/field-patch.js +4 -30
  196. package/dist/lib/cms/field-patch.js.map +1 -1
  197. package/dist/lib/cms-views/view-model.d.ts +3 -3
  198. package/dist/lib/collection-view/collection-view-source.d.ts +7 -3
  199. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  200. package/dist/lib/data/collection-content.js +3 -2
  201. package/dist/lib/data/collection-content.js.map +1 -1
  202. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  203. package/dist/lib/dnd/drop-rules.js +53 -59
  204. package/dist/lib/dnd/drop-rules.js.map +1 -1
  205. package/dist/lib/engine-version.d.ts +1 -1
  206. package/dist/lib/engine-version.js +1 -1
  207. package/dist/lib/engine-version.js.map +1 -1
  208. package/dist/lib/i18n/messages.de.js +40 -3
  209. package/dist/lib/i18n/messages.de.js.map +1 -1
  210. package/dist/lib/i18n/messages.en.d.ts +34 -2
  211. package/dist/lib/i18n/messages.en.js +43 -3
  212. package/dist/lib/i18n/messages.en.js.map +1 -1
  213. package/dist/lib/operations/project-runtime-config.d.ts +25 -6
  214. package/dist/lib/operations/project-runtime-config.js +37 -10
  215. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  216. package/dist/lib/operations/publish-with-revision.d.ts +33 -1
  217. package/dist/lib/operations/publish-with-revision.js +32 -0
  218. package/dist/lib/operations/publish-with-revision.js.map +1 -1
  219. package/dist/lib/operations/runtime-validator.d.ts +19 -68
  220. package/dist/lib/operations/runtime-validator.js +12 -138
  221. package/dist/lib/operations/runtime-validator.js.map +1 -1
  222. package/dist/lib/operations/studio-operations.d.ts +9 -2
  223. package/dist/lib/operations/studio-operations.js +13 -11
  224. package/dist/lib/operations/studio-operations.js.map +1 -1
  225. package/dist/lib/persistence/convex-adapter.js +6 -1
  226. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  227. package/dist/lib/persona/labels.js +3 -1
  228. package/dist/lib/persona/labels.js.map +1 -1
  229. package/dist/lib/workspace/index.d.ts +8 -11
  230. package/dist/lib/workspace/index.js +12 -24
  231. package/dist/lib/workspace/index.js.map +1 -1
  232. package/package.json +3 -3
  233. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  234. package/dist/components/cms/cms-composition-validators.js +0 -133
  235. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  236. package/dist/components/cms/publish-clean.d.ts +0 -28
  237. package/dist/components/cms/publish-clean.js +0 -38
  238. package/dist/components/cms/publish-clean.js.map +0 -1
  239. package/dist/lib/assets/asset-placements.d.ts +0 -28
  240. package/dist/lib/assets/asset-placements.js +0 -83
  241. package/dist/lib/assets/asset-placements.js.map +0 -1
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { convexTest } from 'convex-test'
3
+ import type { ConvexHttpClient } from 'convex/browser'
4
+ import type { FunctionReference } from 'convex/server'
5
+ import type { CollectionDef } from '@elytracms/core/cms-core'
6
+ import { createSequentialIdFactory } from '@elytracms/core/determinism'
7
+ import { createOperations } from '@elytracms/core/operations'
8
+ import { replaceAtCurrentRevision } from '@elytracms/core/persistence'
9
+ import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
10
+ import { loadHydratedCms } from '../src/lib/operations/session-hydration'
11
+ import schema from './schema'
12
+
13
+ /**
14
+ * EC-606 close-out — the orphan count on the CONNECTED path, short of a signed-in
15
+ * connected studio (owner-run).
16
+ *
17
+ * The count itself is mode-independent: `loadHydratedCms` runs over whatever
18
+ * operations client the session has, and `project-session.test.tsx` already proves
19
+ * the content session carries its `orphans` to the rail foot. What only the connected
20
+ * mode adds is the backend between them — the real `cms:listDocuments` handler and the
21
+ * production `ConvexPersistenceAdapter`. This runs exactly that path on convex-test's
22
+ * in-memory backend (the harness of `contract-parity.test.ts`: the adapter's HTTP client
23
+ * is a call-through double onto `t.query` / `t.mutation`): a row of a collection the
24
+ * config does not declare must reach the session's orphan list, with its published
25
+ * count, and must not reach its documents.
26
+ */
27
+
28
+ const modules = {
29
+ ...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
30
+ './_generated/api.ts': async () => ({}),
31
+ }
32
+
33
+ const PROJECT_ID = 'orphan-hydration'
34
+ const SESSION_USER_ID = 'user_admin'
35
+
36
+ const DECLARED: CollectionDef[] = [{ id: 'post', kind: 'document', fields: [{ name: 'title', type: 'text' }] }]
37
+
38
+ interface AdapterBackend {
39
+ query: (ref: FunctionReference<'query'>, args: Record<string, unknown>) => Promise<unknown>
40
+ mutation: (ref: FunctionReference<'mutation'>, args: Record<string, unknown>) => Promise<unknown>
41
+ }
42
+
43
+ function asAdapterClient(backend: AdapterBackend): ConvexHttpClient {
44
+ return {
45
+ query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => backend.query(ref, args ?? {}),
46
+ mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => backend.mutation(ref, args ?? {}),
47
+ } as unknown as ConvexHttpClient
48
+ }
49
+
50
+ /** The production adapter over a fresh in-memory Convex backend, as the session admin. */
51
+ async function connectedAdapter() {
52
+ const t = convexTest(schema, modules)
53
+ const now = '2026-10-26T00:00:00.000Z'
54
+ await t.run(async (ctx) => {
55
+ await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
56
+ })
57
+ const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
58
+ return new ConvexPersistenceAdapter(asAdapterClient(asAdmin), PROJECT_ID)
59
+ }
60
+
61
+ /** The operations client a headless embedder (or the session) builds over that adapter. */
62
+ function operationsOver(adapter: ConvexPersistenceAdapter) {
63
+ return createOperations({
64
+ adapter,
65
+ collections: () => DECLARED,
66
+ validateDocument: () => [],
67
+ idFactory: createSequentialIdFactory('doc'),
68
+ })
69
+ }
70
+
71
+ describe('EC-606 — orphans reach a connected session through the Convex functions', () => {
72
+ it('a stored row of an undeclared collection is counted (published too), and kept out of the documents', async () => {
73
+ const adapter = await connectedAdapter()
74
+ await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
75
+ collection: 'post',
76
+ id: 'kept',
77
+ values: { title: 'Kept' },
78
+ })
79
+ await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
80
+ collection: 'cms-data-sources',
81
+ id: 'legacy-draft',
82
+ values: { name: 'left behind by the data-binding subsystem' },
83
+ })
84
+ const published = await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
85
+ collection: 'cms-data-sources',
86
+ id: 'legacy-live',
87
+ values: { name: 'and published' },
88
+ })
89
+ const pinned = await adapter.cms.publishDocument(
90
+ PROJECT_ID,
91
+ { collection: 'cms-data-sources', id: 'legacy-live' },
92
+ published.revision,
93
+ )
94
+ expect(pinned.status).toBe('ok')
95
+
96
+ const result = await loadHydratedCms(operationsOver(adapter), PROJECT_ID, DECLARED)
97
+ expect(result.ok).toBe(true)
98
+ if (!result.ok || !result.value) throw new Error('hydration must answer')
99
+ expect(result.value.orphans).toEqual([{ collection: 'cms-data-sources', count: 2, published: 1 }])
100
+ expect(result.value.documents.map((document) => `${document.collection}:${document.id}`)).toEqual(['post:kept'])
101
+ })
102
+
103
+ it('CONTROL: declared rows only → no orphans (the count is not a constant)', async () => {
104
+ const adapter = await connectedAdapter()
105
+ await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
106
+ collection: 'post',
107
+ id: 'kept',
108
+ values: { title: 'Kept' },
109
+ })
110
+
111
+ const result = await loadHydratedCms(operationsOver(adapter), PROJECT_ID, DECLARED)
112
+ if (!result.ok || !result.value) throw new Error('hydration must answer')
113
+ expect(result.value.orphans).toEqual([])
114
+ expect(result.value.documents).toHaveLength(1)
115
+ })
116
+ })
@@ -3,7 +3,7 @@ import { convexTest } from 'convex-test'
3
3
  import { makeFunctionReference } from 'convex/server'
4
4
  import type { ConvexHttpClient } from 'convex/browser'
5
5
  import type { FunctionReference } from 'convex/server'
6
- import { InMemoryPersistenceAdapter } from '@elytracms/core/persistence'
6
+ import { InMemoryPersistenceAdapter, assetAlt } from '@elytracms/core/persistence'
7
7
  import type { PersistenceAdapter } from '@elytracms/core/persistence'
8
8
  import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
9
9
  import { updateAssetMetadata } from '../src/components/assets/asset-metadata'
@@ -60,7 +60,6 @@ async function seedHero(adapter: PersistenceAdapter) {
60
60
  byteSize: 10,
61
61
  width: 4000,
62
62
  height: 2000,
63
- alt: 'Harbour',
64
63
  title: 'Hero',
65
64
  focalPoint: { x: 0.2, y: 0.8 },
66
65
  thumb: THUMB,
@@ -69,17 +68,21 @@ async function seedHero(adapter: PersistenceAdapter) {
69
68
  })
70
69
  }
71
70
 
72
- /** "Reset to centre", then clear alt — the two connected clears that never persisted. */
71
+ /**
72
+ * "Reset to centre", then clear alt — the two connected clears that never persisted.
73
+ * EC-587: the alt is cleared the way the options dialog clears it, by writing the bag
74
+ * without it (`meta.alt` is its one home).
75
+ */
73
76
  async function metadataClears(adapter: PersistenceAdapter) {
74
77
  await seedHero(adapter)
75
78
  const assets = adapter.assets
76
79
  const reset = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { focalPoint: null })
77
- const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { alt: '' })
80
+ const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { meta: { legacyKey: 'kept' } })
78
81
  expect([reset.ok, clearedAlt.ok]).toEqual([true, true])
79
82
  const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_hero')
80
83
  return {
81
84
  focalPoint: after.focalPoint ?? null,
82
- alt: after.alt ?? null,
85
+ alt: assetAlt(after),
83
86
  title: after.title ?? null,
84
87
  thumb: after.thumb ?? null,
85
88
  lqip: after.lqip ?? null,
@@ -97,7 +100,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
97
100
  title: 'Hero',
98
101
  thumb: THUMB,
99
102
  lqip: LQIP,
100
- meta: { alt: 'Harbour', legacyKey: 'kept' },
103
+ meta: { legacyKey: 'kept' },
101
104
  }
102
105
  expect({ memory, convex }).toEqual({ memory: expected, convex: expected })
103
106
  })
@@ -128,7 +131,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
128
131
  async get() {
129
132
  // An editor saves alt while the backfill is fetching this original — the
130
133
  // backfill's write lands after it, from a listing taken before it.
131
- await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { alt: 'Mid-run alt' })
134
+ await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { meta: { alt: 'Mid-run alt' } })
132
135
  return new Uint8Array([1])
133
136
  },
134
137
  }
@@ -142,7 +145,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
142
145
  )
143
146
  expect(progress.generated).toBe(1)
144
147
  const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_big')
145
- expect([after.alt, after.thumb, after.lqip]).toEqual(['Mid-run alt', THUMB, LQIP])
148
+ expect([assetAlt(after), after.thumb, after.lqip]).toEqual(['Mid-run alt', THUMB, LQIP])
146
149
  }
147
150
  })
148
151
  })
package/convex/schema.ts CHANGED
@@ -222,6 +222,12 @@ export default defineSchema({
222
222
  // EC-199 provenance: authoring origin ('assistant' for AI-authored content).
223
223
  // Additive scalar, outside the folded snapshot — invisible when unset.
224
224
  origin: v.optional(v.string()),
225
+ // EC-585: which mutation recorded the row — 'publish' (`publishDocument`, the only
226
+ // kind `publishDocumentVersion` pins) or 'snapshot' (`saveDocumentVersion`: a restore
227
+ // point). Optional ONLY for rows recorded before EC-585 (read through
228
+ // `isPinnableVersion`, which keeps a kind-less `label: 'published'` row pinnable). A
229
+ // top-level scalar beside the folded snapshot: it adds no nesting depth.
230
+ kind: v.optional(v.union(v.literal('publish'), v.literal('snapshot'))),
225
231
  createdAt: v.string(),
226
232
  })
227
233
  .index('by_doc', ['collection', 'docId'])
@@ -11,6 +11,7 @@ import { useCmsCollectionViewSource } from '../collection-view/cms-collection-vi
11
11
  import { CollectionViewProvider } from '../collection-view/collection-view-context.js';
12
12
  import { DocumentDiffView } from '../cms/document-diff-view.js';
13
13
  import { ProposalIssues } from './proposal-issues.js';
14
+ import { TranslateSkipped } from './translate-skipped.js';
14
15
  /**
15
16
  * The AI proposal review (EC-199, extended by EC-332/EC-378): a valid,
16
17
  * leash-compliant proposal the editor reviews as a DIFF (the shared EC-317
@@ -101,7 +102,7 @@ export function ProposalCardView({ proposal, collection, source, onApply, onDisc
101
102
  // roomy full-height sheet is offered for a large proposal (never a modal form).
102
103
  const [reviewOpen, setReviewOpen] = useState(false);
103
104
  const reviewable = diff !== null && source !== null;
104
- return (_jsxs(Stack, { gap: "2", className: "overflow-hidden rounded-xl border border-border bg-card", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/60 bg-muted/30 px-3 py-2", children: [_jsx(Sparkles, { className: "size-4 shrink-0 text-primary" }), _jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: captions.title }), _jsx(Spacer, {}), applied ? _jsx(Badge, { variant: "success", children: translate('ai.proposal.applied') }) : null, rejected ? _jsx(Badge, { variant: "outline", children: translate('ai.proposal.discarded') }) : null, reviewable ? (_jsx(Button, { size: "icon-sm", variant: "ghost", onClick: () => setReviewOpen(true), title: translate('ai.proposal.expandReview'), "aria-label": translate('ai.proposal.expandReview'), children: _jsx(Maximize2, {}) })) : null] }), _jsx("div", { className: "px-3", children: reviewable ? (_jsx(CollectionViewProvider, { source: source, children: _jsx(DocumentDiffView, { diff: diff, collection: collection, columnHeaders: columnHeaders, layout: "stacked" }) })) : (_jsx(Text, { size: "xs", tone: "muted", className: "py-2", children: translate('ai.proposal.unavailable') })) }), _jsx(ProposalIssues, { issues: proposal.issues, className: "px-3" }), _jsx(Row, { gap: "2", align: "center", className: "border-t border-border/60 px-3 py-2", children: proposal.status === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", tone: "muted", className: "min-w-0 flex-1", children: translate('ai.proposal.reviewHint') }), _jsxs(Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: [_jsx(X, {}), " ", translate('ai.proposal.discard')] }), _jsxs(Button, { size: "sm", onClick: onApply, children: [_jsx(Check, {}), " ", translate('ai.proposal.apply')] })] })) : applied ? (_jsxs(_Fragment, { children: [_jsx(Spacer, {}), _jsxs(Button, { size: "sm", variant: "outline", onClick: onOpen, children: [_jsx(ArrowUpRight, {}), " ", translate('ai.proposal.open')] })] })) : (_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.proposal.discardedNote') })) }), reviewable && reviewOpen ? (_jsx(ProposalReviewSheet, { title: captions.title, diff: diff, collection: collection, source: source, columnHeaders: columnHeaders, issues: proposal.issues, status: proposal.status, onApply: onApply, onDiscard: onDiscard, onClose: () => setReviewOpen(false) })) : null] }));
105
+ return (_jsxs(Stack, { gap: "2", className: "overflow-hidden rounded-xl border border-border bg-card", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/60 bg-muted/30 px-3 py-2", children: [_jsx(Sparkles, { className: "size-4 shrink-0 text-primary" }), _jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: captions.title }), _jsx(Spacer, {}), applied ? _jsx(Badge, { variant: "success", children: translate('ai.proposal.applied') }) : null, rejected ? _jsx(Badge, { variant: "outline", children: translate('ai.proposal.discarded') }) : null, reviewable ? (_jsx(Button, { size: "icon-sm", variant: "ghost", onClick: () => setReviewOpen(true), title: translate('ai.proposal.expandReview'), "aria-label": translate('ai.proposal.expandReview'), children: _jsx(Maximize2, {}) })) : null] }), _jsx("div", { className: "px-3", children: reviewable ? (_jsx(CollectionViewProvider, { source: source, children: _jsx(DocumentDiffView, { diff: diff, collection: collection, columnHeaders: columnHeaders, layout: "stacked" }) })) : (_jsx(Text, { size: "xs", tone: "muted", className: "py-2", children: translate('ai.proposal.unavailable') })) }), _jsx(ProposalIssues, { issues: proposal.issues, className: "px-3" }), proposal.skipped && proposal.skipped.length > 0 ? (_jsx("div", { className: "px-3", children: _jsx(TranslateSkipped, { skipped: proposal.skipped, collection: collection }) })) : null, _jsx(Row, { gap: "2", align: "center", className: "border-t border-border/60 px-3 py-2", children: proposal.status === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", tone: "muted", className: "min-w-0 flex-1", children: translate('ai.proposal.reviewHint') }), _jsxs(Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: [_jsx(X, {}), " ", translate('ai.proposal.discard')] }), _jsxs(Button, { size: "sm", onClick: onApply, children: [_jsx(Check, {}), " ", translate('ai.proposal.apply')] })] })) : applied ? (_jsxs(_Fragment, { children: [_jsx(Spacer, {}), _jsxs(Button, { size: "sm", variant: "outline", onClick: onOpen, children: [_jsx(ArrowUpRight, {}), " ", translate('ai.proposal.open')] })] })) : (_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.proposal.discardedNote') })) }), reviewable && reviewOpen ? (_jsx(ProposalReviewSheet, { title: captions.title, diff: diff, collection: collection, source: source, columnHeaders: columnHeaders, issues: proposal.issues, status: proposal.status, onApply: onApply, onDiscard: onDiscard, onClose: () => setReviewOpen(false) })) : null] }));
105
106
  }
106
107
  /**
107
108
  * The expandable review surface (EC-382): the proposal's diff promoted into a
@@ -1 +1 @@
1
- {"version":3,"file":"proposal-card.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-card.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { ArrowUpRight, Check, Maximize2, Sparkles, X } from 'lucide-react'\nimport { diffDocuments } from '@elytracms/core/content'\nimport type { DocumentDiff } from '@elytracms/core/content'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue, CollectionDef } from '@elytracms/core/cms-core'\nimport { Badge, Button, ExpandableSurface, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { proposalDocuments } from '@elytracms/studio/lib/ai'\nimport type { ProposalOrigin, ProposalReview } from '@elytracms/studio/lib/ai'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { DocumentDiffView } from '../cms/document-diff-view'\nimport { ProposalIssues } from './proposal-issues'\n\n/**\n * The AI proposal review (EC-199, extended by EC-332/EC-378): a valid,\n * leash-compliant proposal the editor reviews as a DIFF (the shared EC-317\n * {@link DocumentDiffView} — never a second renderer) before applying it as a\n * draft or discarding it. Create proposals diff `empty-before` vs\n * `proposed-after`; translate/update proposals diff the EXISTING document\n * (old → new). Apply/discard are handled by the panel (the operations write +\n * provenance snapshot happen there); this card is the surface.\n *\n * {@link ProposalCard} is the CMS-wired shell; {@link ProposalCardView} is the\n * presentational card over an injected collection + view source (node-smoked\n * with `renderToStaticMarkup`, house pattern).\n *\n * ## EC-496 — the proposal's own findings\n *\n * The diff answers *what changes*; {@link ProposalIssues} answers *what the engine\n * flagged about it*, immediately above the Apply/Discard row so it is read while the\n * decision is being made. Two calls made here, both deliberate:\n *\n * **An error on the card does NOT block apply.** Grounded before deciding\n * (`file:line`, read 2026-08-01): every producer already filters this list to\n * non-error findings (`lib/ai/apply-content.ts:259`, `lib/ai/update-fields.ts:442`,\n * `lib/ai/tools.ts:764`), so a card-blocking gate would be dead code today. More\n * importantly the real gate already exists one layer up and is strictly better\n * placed: EC-473 made apply RE-VALIDATE against the session as it is NOW\n * (`ai-chat-panel.tsx:371`) and refuse with `ai.error.applyInvalid` when the write\n * would INTRODUCE an error. A build-time snapshot cannot see an edit the editor made\n * since, and gating on pre-existing brokenness is exactly what EC-473 argued makes an\n * assistant unusable on real content. So this surface REPORTS truthfully — errors\n * render as errors and sort first ({@link orderedProposalIssues}) — and the gate\n * stays where it can see the truth.\n *\n * **`attempts` is not rendered** (the EC-496 scope-2 owner call). The repair-loop\n * count is provenance about the MODEL, not about the content: a proposal repaired on\n * the second attempt passed the identical validation gate as a first-shot one, so the\n * number changes nothing an editor can act on — and spending sidebar space plus a new\n * catalog string on it would trade a real signal (the findings) for a trivia line. The\n * field stays on `ChatProposal` (it is deterministic, cheap, and the natural input for\n * a future AI-diagnostics surface); it simply has no place on the editor's card.\n */\n\n/** The card's origin-dependent captions (title + diff column headers). */\nexport function proposalCaptions(\n origin: ProposalOrigin,\n collectionLabel: string,\n): { title: string; unified: string; before: string; after: string } {\n if (origin === 'translate') {\n return {\n title: translate('ai.translate.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.translate.diffBefore'),\n after: translate('ai.translate.diffAfter'),\n }\n }\n if (origin === 'update') {\n return {\n title: translate('ai.update.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.update.diffBefore'),\n after: translate('ai.update.diffAfter'),\n }\n }\n return {\n title: translate('ai.proposal.title', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.proposal.diffBefore'),\n after: translate('ai.proposal.diffAfter'),\n }\n}\n\nexport function ProposalCard({\n proposal,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const cms = useCms()\n const collection = cms.getCollection(proposal.collection)\n const source = useCmsCollectionViewSource(proposal.collection)\n return (\n <ProposalCardView\n proposal={proposal}\n collection={collection}\n source={source}\n onApply={onApply}\n onDiscard={onDiscard}\n onOpen={onOpen}\n />\n )\n}\n\n/** The presentational proposal card over an injected collection + view source. */\nexport function ProposalCardView({\n proposal,\n collection,\n source,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n collection: CollectionDef | undefined\n source: CollectionViewSource | null\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const diff = useMemo(() => {\n if (!collection) return null\n const { before, after } = proposalDocuments(proposal.patch, proposal.before)\n return diffDocuments(collection, before, after)\n }, [collection, proposal.patch, proposal.before])\n\n const collectionLabel =\n (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??\n (collection?.form?.label && resolveLabel(collection.form.label)) ??\n proposal.collection\n\n const captions = proposalCaptions(proposal.origin, collectionLabel)\n const applied = proposal.status === 'applied'\n const rejected = proposal.status === 'rejected'\n const columnHeaders = { unified: captions.unified, before: captions.before, after: captions.after }\n\n // EC-382 — the expandable review surface: the sidebar card stays stacked; a\n // roomy full-height sheet is offered for a large proposal (never a modal form).\n const [reviewOpen, setReviewOpen] = useState(false)\n const reviewable = diff !== null && source !== null\n\n return (\n <Stack\n gap=\"2\"\n className=\"overflow-hidden rounded-xl border border-border bg-card\"\n >\n <Row gap=\"2\" align=\"center\" className=\"border-b border-border/60 bg-muted/30 px-3 py-2\">\n <Sparkles className=\"size-4 shrink-0 text-primary\" />\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {captions.title}\n </Text>\n <Spacer />\n {applied ? <Badge variant=\"success\">{translate('ai.proposal.applied')}</Badge> : null}\n {rejected ? <Badge variant=\"outline\">{translate('ai.proposal.discarded')}</Badge> : null}\n {reviewable ? (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setReviewOpen(true)}\n title={translate('ai.proposal.expandReview')}\n aria-label={translate('ai.proposal.expandReview')}\n >\n <Maximize2 />\n </Button>\n ) : null}\n </Row>\n\n <div className=\"px-3\">\n {reviewable ? (\n <CollectionViewProvider source={source!}>\n <DocumentDiffView\n diff={diff!}\n collection={collection!}\n columnHeaders={columnHeaders}\n layout=\"stacked\"\n />\n </CollectionViewProvider>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"py-2\">\n {translate('ai.proposal.unavailable')}\n </Text>\n )}\n </div>\n\n {/* EC-496 — the engine's surviving findings, last thing before the actions. */}\n <ProposalIssues issues={proposal.issues} className=\"px-3\" />\n\n <Row gap=\"2\" align=\"center\" className=\"border-t border-border/60 px-3 py-2\">\n {proposal.status === 'pending' ? (\n <>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDiscard}>\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button size=\"sm\" onClick={onApply}>\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </>\n ) : applied ? (\n <>\n <Spacer />\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen}>\n <ArrowUpRight /> {translate('ai.proposal.open')}\n </Button>\n </>\n ) : (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.proposal.discardedNote')}\n </Text>\n )}\n </Row>\n\n {reviewable && reviewOpen ? (\n <ProposalReviewSheet\n title={captions.title}\n diff={diff!}\n collection={collection!}\n source={source!}\n columnHeaders={columnHeaders}\n issues={proposal.issues}\n status={proposal.status}\n onApply={onApply}\n onDiscard={onDiscard}\n onClose={() => setReviewOpen(false)}\n />\n ) : null}\n </Stack>\n )\n}\n\n/**\n * The expandable review surface (EC-382): the proposal's diff promoted into a\n * roomy, full-height right-anchored sheet where the wide `grid` layout is\n * comfortable — for a large proposal the 320px sidebar can only cramp. It is a\n * REVIEW surface, not a form, so it stays lightly dismissible (backdrop click /\n * Escape / close button) rather than a trapping modal — the forms-never-modal\n * spirit, applied. Presentation only: Apply/Discard call the same handlers the\n * card does; it changes no proposal data. Since EC-392 it renders through the\n * shared {@link ExpandableSurface} primitive (portal-to-body, backdrop, header\n * close, layer-stacked Escape) at the shipped 720px width, so it escapes the\n * narrow AI panel; the card default keeps it closed, so the SSR smoke path\n * never mounts the portal.\n */\nfunction ProposalReviewSheet({\n title,\n diff,\n collection,\n source,\n columnHeaders,\n issues,\n status,\n onApply,\n onDiscard,\n onClose,\n}: {\n title: string\n diff: DocumentDiff\n collection: CollectionDef\n source: CollectionViewSource\n columnHeaders: { unified: string; before: string; after: string }\n /** EC-496 — the same findings the card shows; the roomy surface must not hide them. */\n issues: readonly CmsValidationIssue[]\n status: ProposalReview['status']\n onApply: () => void\n onDiscard: () => void\n onClose: () => void\n}) {\n return (\n <ExpandableSurface\n title={title}\n closeLabel={translate('ai.proposal.collapseReview')}\n width=\"min(720px, 92vw)\"\n onDismiss={onClose}\n headerLeading={<Sparkles className=\"size-4 shrink-0 text-primary\" />}\n >\n <div className=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-3xl\">\n <CollectionViewProvider source={source}>\n <DocumentDiffView diff={diff} collection={collection} columnHeaders={columnHeaders} />\n </CollectionViewProvider>\n <ProposalIssues issues={issues} className=\"mt-4\" />\n </div>\n </div>\n\n {status === 'pending' ? (\n <Row gap=\"2\" align=\"center\" className=\"shrink-0 border-t border-border px-4 py-3\">\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n onDiscard()\n onClose()\n }}\n >\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button\n size=\"sm\"\n onClick={() => {\n onApply()\n onClose()\n }}\n >\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </Row>\n ) : null}\n </ExpandableSurface>\n )\n}\n"]}
1
+ {"version":3,"file":"proposal-card.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-card.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { ArrowUpRight, Check, Maximize2, Sparkles, X } from 'lucide-react'\nimport { diffDocuments } from '@elytracms/core/content'\nimport type { DocumentDiff } from '@elytracms/core/content'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue, CollectionDef } from '@elytracms/core/cms-core'\nimport { Badge, Button, ExpandableSurface, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { proposalDocuments } from '@elytracms/studio/lib/ai'\nimport type { ProposalOrigin, ProposalReview } from '@elytracms/studio/lib/ai'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { DocumentDiffView } from '../cms/document-diff-view'\nimport { ProposalIssues } from './proposal-issues'\nimport { TranslateSkipped } from './translate-skipped'\n\n/**\n * The AI proposal review (EC-199, extended by EC-332/EC-378): a valid,\n * leash-compliant proposal the editor reviews as a DIFF (the shared EC-317\n * {@link DocumentDiffView} — never a second renderer) before applying it as a\n * draft or discarding it. Create proposals diff `empty-before` vs\n * `proposed-after`; translate/update proposals diff the EXISTING document\n * (old → new). Apply/discard are handled by the panel (the operations write +\n * provenance snapshot happen there); this card is the surface.\n *\n * {@link ProposalCard} is the CMS-wired shell; {@link ProposalCardView} is the\n * presentational card over an injected collection + view source (node-smoked\n * with `renderToStaticMarkup`, house pattern).\n *\n * ## EC-496 — the proposal's own findings\n *\n * The diff answers *what changes*; {@link ProposalIssues} answers *what the engine\n * flagged about it*, immediately above the Apply/Discard row so it is read while the\n * decision is being made. Two calls made here, both deliberate:\n *\n * **An error on the card does NOT block apply.** Grounded before deciding\n * (`file:line`, read 2026-08-01): every producer already filters this list to\n * non-error findings (`lib/ai/apply-content.ts:259`, `lib/ai/update-fields.ts:442`,\n * `lib/ai/tools.ts:764`), so a card-blocking gate would be dead code today. More\n * importantly the real gate already exists one layer up and is strictly better\n * placed: EC-473 made apply RE-VALIDATE against the session as it is NOW\n * (`ai-chat-panel.tsx:371`) and refuse with `ai.error.applyInvalid` when the write\n * would INTRODUCE an error. A build-time snapshot cannot see an edit the editor made\n * since, and gating on pre-existing brokenness is exactly what EC-473 argued makes an\n * assistant unusable on real content. So this surface REPORTS truthfully — errors\n * render as errors and sort first ({@link orderedProposalIssues}) — and the gate\n * stays where it can see the truth.\n *\n * **`attempts` is not rendered** (the EC-496 scope-2 owner call). The repair-loop\n * count is provenance about the MODEL, not about the content: a proposal repaired on\n * the second attempt passed the identical validation gate as a first-shot one, so the\n * number changes nothing an editor can act on — and spending sidebar space plus a new\n * catalog string on it would trade a real signal (the findings) for a trivia line. The\n * field stays on `ChatProposal` (it is deterministic, cheap, and the natural input for\n * a future AI-diagnostics surface); it simply has no place on the editor's card.\n */\n\n/** The card's origin-dependent captions (title + diff column headers). */\nexport function proposalCaptions(\n origin: ProposalOrigin,\n collectionLabel: string,\n): { title: string; unified: string; before: string; after: string } {\n if (origin === 'translate') {\n return {\n title: translate('ai.translate.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.translate.diffBefore'),\n after: translate('ai.translate.diffAfter'),\n }\n }\n if (origin === 'update') {\n return {\n title: translate('ai.update.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.update.diffBefore'),\n after: translate('ai.update.diffAfter'),\n }\n }\n return {\n title: translate('ai.proposal.title', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.proposal.diffBefore'),\n after: translate('ai.proposal.diffAfter'),\n }\n}\n\nexport function ProposalCard({\n proposal,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const cms = useCms()\n const collection = cms.getCollection(proposal.collection)\n const source = useCmsCollectionViewSource(proposal.collection)\n return (\n <ProposalCardView\n proposal={proposal}\n collection={collection}\n source={source}\n onApply={onApply}\n onDiscard={onDiscard}\n onOpen={onOpen}\n />\n )\n}\n\n/** The presentational proposal card over an injected collection + view source. */\nexport function ProposalCardView({\n proposal,\n collection,\n source,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n collection: CollectionDef | undefined\n source: CollectionViewSource | null\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const diff = useMemo(() => {\n if (!collection) return null\n const { before, after } = proposalDocuments(proposal.patch, proposal.before)\n return diffDocuments(collection, before, after)\n }, [collection, proposal.patch, proposal.before])\n\n const collectionLabel =\n (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??\n (collection?.form?.label && resolveLabel(collection.form.label)) ??\n proposal.collection\n\n const captions = proposalCaptions(proposal.origin, collectionLabel)\n const applied = proposal.status === 'applied'\n const rejected = proposal.status === 'rejected'\n const columnHeaders = { unified: captions.unified, before: captions.before, after: captions.after }\n\n // EC-382 — the expandable review surface: the sidebar card stays stacked; a\n // roomy full-height sheet is offered for a large proposal (never a modal form).\n const [reviewOpen, setReviewOpen] = useState(false)\n const reviewable = diff !== null && source !== null\n\n return (\n <Stack\n gap=\"2\"\n className=\"overflow-hidden rounded-xl border border-border bg-card\"\n >\n <Row gap=\"2\" align=\"center\" className=\"border-b border-border/60 bg-muted/30 px-3 py-2\">\n <Sparkles className=\"size-4 shrink-0 text-primary\" />\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {captions.title}\n </Text>\n <Spacer />\n {applied ? <Badge variant=\"success\">{translate('ai.proposal.applied')}</Badge> : null}\n {rejected ? <Badge variant=\"outline\">{translate('ai.proposal.discarded')}</Badge> : null}\n {reviewable ? (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setReviewOpen(true)}\n title={translate('ai.proposal.expandReview')}\n aria-label={translate('ai.proposal.expandReview')}\n >\n <Maximize2 />\n </Button>\n ) : null}\n </Row>\n\n <div className=\"px-3\">\n {reviewable ? (\n <CollectionViewProvider source={source!}>\n <DocumentDiffView\n diff={diff!}\n collection={collection!}\n columnHeaders={columnHeaders}\n layout=\"stacked\"\n />\n </CollectionViewProvider>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"py-2\">\n {translate('ai.proposal.unavailable')}\n </Text>\n )}\n </div>\n\n {/* EC-496 — the engine's surviving findings, last thing before the actions. */}\n <ProposalIssues issues={proposal.issues} className=\"px-3\" />\n {/* EC-593 — what a translation could not translate, listed (never skipped silently). */}\n {proposal.skipped && proposal.skipped.length > 0 ? (\n <div className=\"px-3\">\n <TranslateSkipped skipped={proposal.skipped} collection={collection} />\n </div>\n ) : null}\n\n <Row gap=\"2\" align=\"center\" className=\"border-t border-border/60 px-3 py-2\">\n {proposal.status === 'pending' ? (\n <>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDiscard}>\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button size=\"sm\" onClick={onApply}>\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </>\n ) : applied ? (\n <>\n <Spacer />\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen}>\n <ArrowUpRight /> {translate('ai.proposal.open')}\n </Button>\n </>\n ) : (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.proposal.discardedNote')}\n </Text>\n )}\n </Row>\n\n {reviewable && reviewOpen ? (\n <ProposalReviewSheet\n title={captions.title}\n diff={diff!}\n collection={collection!}\n source={source!}\n columnHeaders={columnHeaders}\n issues={proposal.issues}\n status={proposal.status}\n onApply={onApply}\n onDiscard={onDiscard}\n onClose={() => setReviewOpen(false)}\n />\n ) : null}\n </Stack>\n )\n}\n\n/**\n * The expandable review surface (EC-382): the proposal's diff promoted into a\n * roomy, full-height right-anchored sheet where the wide `grid` layout is\n * comfortable — for a large proposal the 320px sidebar can only cramp. It is a\n * REVIEW surface, not a form, so it stays lightly dismissible (backdrop click /\n * Escape / close button) rather than a trapping modal — the forms-never-modal\n * spirit, applied. Presentation only: Apply/Discard call the same handlers the\n * card does; it changes no proposal data. Since EC-392 it renders through the\n * shared {@link ExpandableSurface} primitive (portal-to-body, backdrop, header\n * close, layer-stacked Escape) at the shipped 720px width, so it escapes the\n * narrow AI panel; the card default keeps it closed, so the SSR smoke path\n * never mounts the portal.\n */\nfunction ProposalReviewSheet({\n title,\n diff,\n collection,\n source,\n columnHeaders,\n issues,\n status,\n onApply,\n onDiscard,\n onClose,\n}: {\n title: string\n diff: DocumentDiff\n collection: CollectionDef\n source: CollectionViewSource\n columnHeaders: { unified: string; before: string; after: string }\n /** EC-496 — the same findings the card shows; the roomy surface must not hide them. */\n issues: readonly CmsValidationIssue[]\n status: ProposalReview['status']\n onApply: () => void\n onDiscard: () => void\n onClose: () => void\n}) {\n return (\n <ExpandableSurface\n title={title}\n closeLabel={translate('ai.proposal.collapseReview')}\n width=\"min(720px, 92vw)\"\n onDismiss={onClose}\n headerLeading={<Sparkles className=\"size-4 shrink-0 text-primary\" />}\n >\n <div className=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-3xl\">\n <CollectionViewProvider source={source}>\n <DocumentDiffView diff={diff} collection={collection} columnHeaders={columnHeaders} />\n </CollectionViewProvider>\n <ProposalIssues issues={issues} className=\"mt-4\" />\n </div>\n </div>\n\n {status === 'pending' ? (\n <Row gap=\"2\" align=\"center\" className=\"shrink-0 border-t border-border px-4 py-3\">\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n onDiscard()\n onClose()\n }}\n >\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button\n size=\"sm\"\n onClick={() => {\n onApply()\n onClose()\n }}\n >\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </Row>\n ) : null}\n </ExpandableSurface>\n )\n}\n"]}
@@ -10,6 +10,7 @@ import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
10
10
  import { resolveAiConfig, resolveKnowledge, translateDocument } from '@elytracms/studio/lib/ai';
11
11
  import { cmsValidationIssueMessage } from '@elytracms/studio/lib/persona/labels';
12
12
  import { useCms } from '../cms/cms-store.js';
13
+ import { TranslateSkipped } from './translate-skipped.js';
13
14
  export function TranslateDocumentDialog({ projectId, collection, docRef, document, localeConfig, flushDraft, open, onOpenChange, }) {
14
15
  const [phase, setPhase] = useState('confirm');
15
16
  const [progress, setProgress] = useState(null);
@@ -106,7 +107,8 @@ export function TranslateDocumentDialog({ projectId, collection, docRef, documen
106
107
  slugs: result.slugsDerived,
107
108
  locales: Object.keys(result.perLocale).length,
108
109
  cancelled: result.cancelled,
109
- nothing: result.total === 0,
110
+ nothing: result.total === 0 && result.skipped.length === 0,
111
+ skipped: result.skipped,
110
112
  });
111
113
  setPhase('done');
112
114
  }
@@ -131,6 +133,6 @@ export function TranslateDocumentDialog({ projectId, collection, docRef, documen
131
133
  };
132
134
  return (_jsx(Dialog, { open: open, onOpenChange: (next) => (!next ? close() : undefined), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Languages, { className: "size-4" }), " ", translate('ai.translate.dialogTitle')] }) }), _jsx(DialogDescription, { children: translate('ai.translate.dialogDescription', { base: localeChipLabel(base) }) })] }), phase === 'confirm' ? (_jsxs(Stack, { gap: "4", className: "py-2", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.targetsLabel') }), _jsx(Row, { gap: "1", wrap: true, children: targets.map((l) => (_jsx(Badge, { variant: "outline", children: localeChipLabel(l) }, l))) })] }), _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.scopeLabel') }), _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "default", children: translate('ai.translate.fillMissing') }), _jsx(Button, { size: "sm", variant: "ghost", disabled: true, title: translate('ai.translate.retranslateSoon'), children: translate('ai.translate.retranslateAll') }), _jsx(Text, { size: "xs", tone: "muted", children: translate('ai.translate.retranslateSoon') })] })] })] })) : null, phase === 'running' ? (_jsxs(Stack, { gap: "3", align: "center", className: "py-6", children: [_jsx(Loader2, { className: "size-6 animate-spin text-primary" }), _jsx(Text, { size: "sm", tone: "muted", "data-testid": "translate-progress", children: progress && progress.total > 0
133
135
  ? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })
134
- : translate('ai.translate.preparing') }), progress && progress.total > 0 ? (_jsx("div", { className: "h-1.5 w-56 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${Math.round((progress.completed / progress.total) * 100)}%` } }) })) : null] })) : null, phase === 'done' && summary ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(Check, { className: "size-6 text-success" }), _jsx(Text, { size: "sm", tone: "muted", className: "text-center", "data-testid": "translate-summary", children: doneMessage(summary) })] })) : null, phase === 'error' ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(X, { className: "size-6 text-destructive" }), _jsx(Text, { size: "sm", tone: "muted", className: "text-center", "data-testid": "translate-error", children: refusal ?? translate('ai.translate.failed') })] })) : null, _jsx(DialogFooter, { className: "mt-2", children: phase === 'confirm' ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: close, children: translate('common.cancel') }), _jsxs(Button, { size: "sm", onClick: () => void run(), "data-testid": "translate-confirm", children: [_jsx(Languages, {}), " ", translate('ai.translate.confirm')] })] })) : phase === 'running' ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => abortRef.current?.abort(), "data-testid": "translate-cancel", children: translate('common.cancel') })) : (_jsx(Button, { size: "sm", onClick: close, "data-testid": "translate-close", children: translate('ai.translate.close') })) })] }) }));
136
+ : translate('ai.translate.preparing') }), progress && progress.total > 0 ? (_jsx("div", { className: "h-1.5 w-56 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${Math.round((progress.completed / progress.total) * 100)}%` } }) })) : null] })) : null, phase === 'done' && summary ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(Check, { className: "size-6 text-success" }), _jsx(Text, { size: "sm", tone: "muted", className: "text-center", "data-testid": "translate-summary", children: doneMessage(summary) }), _jsx(TranslateSkipped, { skipped: summary.skipped, collection: collection })] })) : null, phase === 'error' ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(X, { className: "size-6 text-destructive" }), _jsx(Text, { size: "sm", tone: "muted", className: "text-center", "data-testid": "translate-error", children: refusal ?? translate('ai.translate.failed') })] })) : null, _jsx(DialogFooter, { className: "mt-2", children: phase === 'confirm' ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: close, children: translate('common.cancel') }), _jsxs(Button, { size: "sm", onClick: () => void run(), "data-testid": "translate-confirm", children: [_jsx(Languages, {}), " ", translate('ai.translate.confirm')] })] })) : phase === 'running' ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => abortRef.current?.abort(), "data-testid": "translate-cancel", children: translate('common.cancel') })) : (_jsx(Button, { size: "sm", onClick: close, "data-testid": "translate-close", children: translate('ai.translate.close') })) })] }) }));
135
137
  }
136
138
  //# sourceMappingURL=translate-document-dialog.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"translate-document-dialog.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { createCollectionRegistry } from '@elytracms/core/cms-core'\nimport { Check, Languages, Loader2, X } from 'lucide-react'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { localeChipLabel } from '@elytracms/studio/lib/locale'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { resolveAiConfig, resolveKnowledge, translateDocument } from '@elytracms/studio/lib/ai'\nimport type { TranslateProgress } from '@elytracms/studio/lib/ai'\nimport { cmsValidationIssueMessage } from '@elytracms/studio/lib/persona/labels'\nimport { useCms } from '../cms/cms-store'\nimport type { TranslateDocumentActionProps } from './translate-document-action'\n\n/**\n * The \"Translate document\" confirm + progress dialog (EC-332), heavy side — loaded lazily\n * by the gate so the AI engine stays off the critical path (EC-331 bundle discipline). A\n * modal is allowed here (a picker/confirmation, per the house rule — property forms are\n * not). It resolves the EC-331 provider + knowledge, runs the ONE `translateDocument`\n * engine over the live draft (fill-empty-only on EC-318 connection state), streams\n * progress, then writes the result to the DRAFT via the operations layer with a\n * provenance-tagged snapshot (mirroring the EC-199 apply) and reflects it into the open\n * editor. Cancel aborts cleanly — any already-translated units stay on the draft, visible\n * in Compare (EC-317) and rejectable via Discard (EC-316).\n */\ntype Phase = 'confirm' | 'running' | 'done' | 'error'\n\ninterface RunSummary {\n translated: number\n slugs: number\n locales: number\n cancelled: boolean\n nothing: boolean\n}\n\nexport function TranslateDocumentDialog({\n projectId,\n collection,\n docRef,\n document,\n localeConfig,\n flushDraft,\n open,\n onOpenChange,\n}: TranslateDocumentActionProps & { open: boolean; onOpenChange: (open: boolean) => void }) {\n const [phase, setPhase] = useState<Phase>('confirm')\n const [progress, setProgress] = useState<TranslateProgress | null>(null)\n const [summary, setSummary] = useState<RunSummary | null>(null)\n /**\n * EC-497 — why the refusal was blocked, when the gate refused. Reuses the\n * EC-473 `ai.error.applyInvalid` copy (\"the proposal no longer fits the current\n * document: …\"), which is exactly this situation: no new catalog string, and the\n * editor is told WHICH rules the translation would have broken rather than a bare\n * \"failed\".\n */\n const [refusal, setRefusal] = useState<string | null>(null)\n const abortRef = useRef<AbortController | null>(null)\n const cms = useCms()\n // The session registry the produced draft is judged against. Built here rather\n // than added to the CMS store's public surface for one consumer; the memo means\n // once per open, not once per render.\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n useEffect(() => () => abortRef.current?.abort(), [])\n\n const base = localeConfig.default\n const targets = localeConfig.locales.filter((l) => l !== base)\n\n const run = useCallback(async () => {\n if (projectId === undefined) {\n setPhase('error')\n return\n }\n const resolved = resolveAiConfig(resolveDeploymentAi())\n const provider = resolved.resolved?.provider\n if (!provider || !resolved.ok) {\n setPhase('error')\n return\n }\n const controller = new AbortController()\n abortRef.current = controller\n setRefusal(null)\n setPhase('running')\n await flushDraft()\n setProgress({ completed: 0, total: 0 })\n try {\n const knowledge = await resolveKnowledge(resolved.resolved?.knowledge ?? [])\n const result = await translateDocument({\n provider,\n collection,\n document,\n localeConfig,\n knowledge,\n signal: controller.signal,\n onProgress: setProgress,\n // EC-497 — this action used to hand `translateDocument`'s output STRAIGHT to\n // `documents.upsert` with nothing in between: no gate, no proposal card, no\n // re-validation, so model-generated content reached the document completely\n // unvalidated while the chat path validated twice. It now validates with the\n // session's canonical bag (`cms.validateOptions`) — the editor's own rules —\n // and refuses below on what the translation INTRODUCED.\n registry,\n validateOptions: cms.validateOptions,\n })\n if (result.introducedErrors.length > 0) {\n setRefusal(\n translate('ai.error.applyInvalid', {\n issues: result.introducedErrors\n .slice(0, 3)\n .map((issue) => cmsValidationIssueMessage(issue))\n .join('; '),\n }),\n )\n setPhase('error')\n return\n }\n if (result.translated > 0 || result.slugsDerived > 0) {\n // EC-595 A2: the translation persists as the patches of the buckets it filled\n // (never the whole document) — through the session's queue, at the revision\n // the editor's view is at; a bucket a colleague changed meanwhile is refused on\n // that field and shown there.\n const outcome = await cms.commitPatches(docRef, result.patches)\n if (outcome?.status === 'failed') {\n setPhase('error')\n return\n }\n const { operations } = await getStudioOperations()\n // Provenance: an AI-tagged version snapshot (best-effort — the draft already landed).\n try {\n await operations.documents.snapshot({\n projectId,\n ref: docRef,\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n }\n setSummary({\n translated: result.translated,\n slugs: result.slugsDerived,\n locales: Object.keys(result.perLocale).length,\n cancelled: result.cancelled,\n nothing: result.total === 0,\n })\n setPhase('done')\n } catch {\n setPhase('error')\n } finally {\n abortRef.current = null\n }\n }, [projectId, collection, document, localeConfig, docRef, flushDraft, registry, cms])\n\n const close = () => {\n if (phase === 'running') return // never close mid-run — Cancel aborts instead\n onOpenChange(false)\n }\n\n const doneMessage = (s: RunSummary): string => {\n if (s.nothing) return translate('ai.translate.nothing')\n if (s.cancelled) return translate('ai.translate.cancelled')\n return translate('ai.translate.applied', { count: s.translated, locales: s.locales })\n }\n\n return (\n <Dialog open={open} onOpenChange={(next) => (!next ? close() : undefined)}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n <Row gap=\"2\" align=\"center\">\n <Languages className=\"size-4\" /> {translate('ai.translate.dialogTitle')}\n </Row>\n </DialogTitle>\n <DialogDescription>\n {translate('ai.translate.dialogDescription', { base: localeChipLabel(base) })}\n </DialogDescription>\n </DialogHeader>\n\n {phase === 'confirm' ? (\n <Stack gap=\"4\" className=\"py-2\">\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.targetsLabel')}\n </Text>\n <Row gap=\"1\" wrap>\n {targets.map((l) => (\n <Badge key={l} variant=\"outline\">\n {localeChipLabel(l)}\n </Badge>\n ))}\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.scopeLabel')}\n </Text>\n <Row gap=\"2\" align=\"center\">\n <Badge variant=\"default\">{translate('ai.translate.fillMissing')}</Badge>\n <Button size=\"sm\" variant=\"ghost\" disabled title={translate('ai.translate.retranslateSoon')}>\n {translate('ai.translate.retranslateAll')}\n </Button>\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.retranslateSoon')}\n </Text>\n </Row>\n </Stack>\n </Stack>\n ) : null}\n\n {phase === 'running' ? (\n <Stack gap=\"3\" align=\"center\" className=\"py-6\">\n <Loader2 className=\"size-6 animate-spin text-primary\" />\n <Text size=\"sm\" tone=\"muted\" data-testid=\"translate-progress\">\n {progress && progress.total > 0\n ? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })\n : translate('ai.translate.preparing')}\n </Text>\n {progress && progress.total > 0 ? (\n <div className=\"h-1.5 w-56 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${Math.round((progress.completed / progress.total) * 100)}%` }}\n />\n </div>\n ) : null}\n </Stack>\n ) : null}\n\n {phase === 'done' && summary ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <Check className=\"size-6 text-success\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-summary\">\n {doneMessage(summary)}\n </Text>\n </Stack>\n ) : null}\n\n {phase === 'error' ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <X className=\"size-6 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-error\">\n {refusal ?? translate('ai.translate.failed')}\n </Text>\n </Stack>\n ) : null}\n\n <DialogFooter className=\"mt-2\">\n {phase === 'confirm' ? (\n <>\n <Button variant=\"ghost\" size=\"sm\" onClick={close}>\n {translate('common.cancel')}\n </Button>\n <Button size=\"sm\" onClick={() => void run()} data-testid=\"translate-confirm\">\n <Languages /> {translate('ai.translate.confirm')}\n </Button>\n </>\n ) : phase === 'running' ? (\n <Button variant=\"outline\" size=\"sm\" onClick={() => abortRef.current?.abort()} data-testid=\"translate-cancel\">\n {translate('common.cancel')}\n </Button>\n ) : (\n <Button size=\"sm\" onClick={close} data-testid=\"translate-close\">\n {translate('ai.translate.close')}\n </Button>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
1
+ {"version":3,"file":"translate-document-dialog.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { createCollectionRegistry } from '@elytracms/core/cms-core'\nimport { Check, Languages, Loader2, X } from 'lucide-react'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { localeChipLabel } from '@elytracms/studio/lib/locale'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { resolveAiConfig, resolveKnowledge, translateDocument } from '@elytracms/studio/lib/ai'\nimport type { TranslateProgress, TranslateSkip } from '@elytracms/studio/lib/ai'\nimport { cmsValidationIssueMessage } from '@elytracms/studio/lib/persona/labels'\nimport { useCms } from '../cms/cms-store'\nimport type { TranslateDocumentActionProps } from './translate-document-action'\nimport { TranslateSkipped } from './translate-skipped'\n\n/**\n * The \"Translate document\" confirm + progress dialog (EC-332), heavy side — loaded lazily\n * by the gate so the AI engine stays off the critical path (EC-331 bundle discipline). A\n * modal is allowed here (a picker/confirmation, per the house rule — property forms are\n * not). It resolves the EC-331 provider + knowledge, runs the ONE `translateDocument`\n * engine over the live draft (fill-empty-only on EC-318 connection state), streams\n * progress, then writes the result to the DRAFT via the operations layer with a\n * provenance-tagged snapshot (mirroring the EC-199 apply) and reflects it into the open\n * editor. Cancel aborts cleanly — any already-translated units stay on the draft, visible\n * in Compare (EC-317) and rejectable via Discard (EC-316).\n */\ntype Phase = 'confirm' | 'running' | 'done' | 'error'\n\ninterface RunSummary {\n translated: number\n slugs: number\n locales: number\n cancelled: boolean\n nothing: boolean\n /** EC-593: every text position the run could not translate — listed, never silent. */\n skipped: readonly TranslateSkip[]\n}\n\nexport function TranslateDocumentDialog({\n projectId,\n collection,\n docRef,\n document,\n localeConfig,\n flushDraft,\n open,\n onOpenChange,\n}: TranslateDocumentActionProps & { open: boolean; onOpenChange: (open: boolean) => void }) {\n const [phase, setPhase] = useState<Phase>('confirm')\n const [progress, setProgress] = useState<TranslateProgress | null>(null)\n const [summary, setSummary] = useState<RunSummary | null>(null)\n /**\n * EC-497 — why the refusal was blocked, when the gate refused. Reuses the\n * EC-473 `ai.error.applyInvalid` copy (\"the proposal no longer fits the current\n * document: …\"), which is exactly this situation: no new catalog string, and the\n * editor is told WHICH rules the translation would have broken rather than a bare\n * \"failed\".\n */\n const [refusal, setRefusal] = useState<string | null>(null)\n const abortRef = useRef<AbortController | null>(null)\n const cms = useCms()\n // The session registry the produced draft is judged against. Built here rather\n // than added to the CMS store's public surface for one consumer; the memo means\n // once per open, not once per render.\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n useEffect(() => () => abortRef.current?.abort(), [])\n\n const base = localeConfig.default\n const targets = localeConfig.locales.filter((l) => l !== base)\n\n const run = useCallback(async () => {\n if (projectId === undefined) {\n setPhase('error')\n return\n }\n const resolved = resolveAiConfig(resolveDeploymentAi())\n const provider = resolved.resolved?.provider\n if (!provider || !resolved.ok) {\n setPhase('error')\n return\n }\n const controller = new AbortController()\n abortRef.current = controller\n setRefusal(null)\n setPhase('running')\n await flushDraft()\n setProgress({ completed: 0, total: 0 })\n try {\n const knowledge = await resolveKnowledge(resolved.resolved?.knowledge ?? [])\n const result = await translateDocument({\n provider,\n collection,\n document,\n localeConfig,\n knowledge,\n signal: controller.signal,\n onProgress: setProgress,\n // EC-497 — this action used to hand `translateDocument`'s output STRAIGHT to\n // `documents.upsert` with nothing in between: no gate, no proposal card, no\n // re-validation, so model-generated content reached the document completely\n // unvalidated while the chat path validated twice. It now validates with the\n // session's canonical bag (`cms.validateOptions`) — the editor's own rules —\n // and refuses below on what the translation INTRODUCED.\n registry,\n validateOptions: cms.validateOptions,\n })\n if (result.introducedErrors.length > 0) {\n setRefusal(\n translate('ai.error.applyInvalid', {\n issues: result.introducedErrors\n .slice(0, 3)\n .map((issue) => cmsValidationIssueMessage(issue))\n .join('; '),\n }),\n )\n setPhase('error')\n return\n }\n if (result.translated > 0 || result.slugsDerived > 0) {\n // EC-595 A2: the translation persists as the patches of the buckets it filled\n // (never the whole document) — through the session's queue, at the revision\n // the editor's view is at; a bucket a colleague changed meanwhile is refused on\n // that field and shown there.\n const outcome = await cms.commitPatches(docRef, result.patches)\n if (outcome?.status === 'failed') {\n setPhase('error')\n return\n }\n const { operations } = await getStudioOperations()\n // Provenance: an AI-tagged version snapshot (best-effort — the draft already landed).\n try {\n await operations.documents.snapshot({\n projectId,\n ref: docRef,\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n }\n setSummary({\n translated: result.translated,\n slugs: result.slugsDerived,\n locales: Object.keys(result.perLocale).length,\n cancelled: result.cancelled,\n nothing: result.total === 0 && result.skipped.length === 0,\n skipped: result.skipped,\n })\n setPhase('done')\n } catch {\n setPhase('error')\n } finally {\n abortRef.current = null\n }\n }, [projectId, collection, document, localeConfig, docRef, flushDraft, registry, cms])\n\n const close = () => {\n if (phase === 'running') return // never close mid-run — Cancel aborts instead\n onOpenChange(false)\n }\n\n const doneMessage = (s: RunSummary): string => {\n if (s.nothing) return translate('ai.translate.nothing')\n if (s.cancelled) return translate('ai.translate.cancelled')\n return translate('ai.translate.applied', { count: s.translated, locales: s.locales })\n }\n\n return (\n <Dialog open={open} onOpenChange={(next) => (!next ? close() : undefined)}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n <Row gap=\"2\" align=\"center\">\n <Languages className=\"size-4\" /> {translate('ai.translate.dialogTitle')}\n </Row>\n </DialogTitle>\n <DialogDescription>\n {translate('ai.translate.dialogDescription', { base: localeChipLabel(base) })}\n </DialogDescription>\n </DialogHeader>\n\n {phase === 'confirm' ? (\n <Stack gap=\"4\" className=\"py-2\">\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.targetsLabel')}\n </Text>\n <Row gap=\"1\" wrap>\n {targets.map((l) => (\n <Badge key={l} variant=\"outline\">\n {localeChipLabel(l)}\n </Badge>\n ))}\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.scopeLabel')}\n </Text>\n <Row gap=\"2\" align=\"center\">\n <Badge variant=\"default\">{translate('ai.translate.fillMissing')}</Badge>\n <Button size=\"sm\" variant=\"ghost\" disabled title={translate('ai.translate.retranslateSoon')}>\n {translate('ai.translate.retranslateAll')}\n </Button>\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.retranslateSoon')}\n </Text>\n </Row>\n </Stack>\n </Stack>\n ) : null}\n\n {phase === 'running' ? (\n <Stack gap=\"3\" align=\"center\" className=\"py-6\">\n <Loader2 className=\"size-6 animate-spin text-primary\" />\n <Text size=\"sm\" tone=\"muted\" data-testid=\"translate-progress\">\n {progress && progress.total > 0\n ? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })\n : translate('ai.translate.preparing')}\n </Text>\n {progress && progress.total > 0 ? (\n <div className=\"h-1.5 w-56 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${Math.round((progress.completed / progress.total) * 100)}%` }}\n />\n </div>\n ) : null}\n </Stack>\n ) : null}\n\n {phase === 'done' && summary ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <Check className=\"size-6 text-success\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-summary\">\n {doneMessage(summary)}\n </Text>\n <TranslateSkipped skipped={summary.skipped} collection={collection} />\n </Stack>\n ) : null}\n\n {phase === 'error' ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <X className=\"size-6 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-error\">\n {refusal ?? translate('ai.translate.failed')}\n </Text>\n </Stack>\n ) : null}\n\n <DialogFooter className=\"mt-2\">\n {phase === 'confirm' ? (\n <>\n <Button variant=\"ghost\" size=\"sm\" onClick={close}>\n {translate('common.cancel')}\n </Button>\n <Button size=\"sm\" onClick={() => void run()} data-testid=\"translate-confirm\">\n <Languages /> {translate('ai.translate.confirm')}\n </Button>\n </>\n ) : phase === 'running' ? (\n <Button variant=\"outline\" size=\"sm\" onClick={() => abortRef.current?.abort()} data-testid=\"translate-cancel\">\n {translate('common.cancel')}\n </Button>\n ) : (\n <Button size=\"sm\" onClick={close} data-testid=\"translate-close\">\n {translate('ai.translate.close')}\n </Button>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { CollectionDef } from '@elytracms/core/cms-core';
2
+ import type { TranslateSkip } from '@elytracms/studio/lib/ai';
3
+ /** One skip as the editor reads it: where (field label › component › position), which language, why. */
4
+ export interface TranslateSkipLine {
5
+ where: string;
6
+ locale: string;
7
+ reason: string;
8
+ }
9
+ /**
10
+ * The where-label of a skipped position: the field's label, then — inside a component —
11
+ * the component and its prop path, else the position below the field (an item counted
12
+ * from 1). Index-bearing segments the editor does not see (`content`, `props`) are left out.
13
+ */
14
+ export declare function translateSkipLines(skipped: readonly TranslateSkip[], collection: CollectionDef | undefined): TranslateSkipLine[];
15
+ /** The list; renders nothing when everything was translated. */
16
+ export declare function TranslateSkipped({ skipped, collection, }: {
17
+ skipped: readonly TranslateSkip[];
18
+ collection: CollectionDef | undefined;
19
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { resolveLabel } from '@elytracms/core/cms-core';
3
+ import { AlertTriangle } from 'lucide-react';
4
+ import { Row, Stack, Text } from '@elytracms/studio/ui';
5
+ import { translate } from '@elytracms/studio/lib/i18n';
6
+ import { localeChipLabel } from '@elytracms/studio/lib/locale';
7
+ /**
8
+ * EC-593 (ratified 4e) — what a translation run could NOT translate, shown where the
9
+ * editor reads the outcome: the "Translate document" dialog and the chat's translation
10
+ * proposal card. Before, a localized object or composition was skipped with no word,
11
+ * and a garbled rich-text block silently kept its original.
12
+ */
13
+ const REASON_KEY = {
14
+ unreadable: 'ai.translate.skipped.unreadable',
15
+ 'not-returned': 'ai.translate.skipped.notReturned',
16
+ 'structure-changed': 'ai.translate.skipped.structureChanged',
17
+ };
18
+ /**
19
+ * The where-label of a skipped position: the field's label, then — inside a component —
20
+ * the component and its prop path, else the position below the field (an item counted
21
+ * from 1). Index-bearing segments the editor does not see (`content`, `props`) are left out.
22
+ */
23
+ export function translateSkipLines(skipped, collection) {
24
+ return skipped.map((skip) => {
25
+ const field = collection?.fields.find((f) => f.name === skip.field);
26
+ const label = resolveLabel(field?.form?.label) ?? skip.field;
27
+ const below = skip.component
28
+ ? [skip.component.componentId, ...skip.at.slice(skip.at.lastIndexOf('props') + 1)]
29
+ : skip.at;
30
+ const segments = below
31
+ .filter((segment) => segment !== 'content' && segment !== 'props')
32
+ .map((segment) => (typeof segment === 'number' ? `#${segment + 1}` : segment));
33
+ return {
34
+ where: [label, ...segments].join(' › '),
35
+ locale: localeChipLabel(skip.locale),
36
+ reason: translate(REASON_KEY[skip.reason]),
37
+ };
38
+ });
39
+ }
40
+ /** The list; renders nothing when everything was translated. */
41
+ export function TranslateSkipped({ skipped, collection, }) {
42
+ if (skipped.length === 0)
43
+ return null;
44
+ return (_jsxs(Stack, { gap: "1", "data-testid": "translate-skipped", children: [_jsxs(Row, { gap: "1", align: "center", children: [_jsx(AlertTriangle, { className: "size-3.5 text-warning" }), _jsx(Text, { size: "xs", weight: "medium", children: translate('ai.translate.skippedTitle', { count: skipped.length }) })] }), translateSkipLines(skipped, collection).map((line, index) => (_jsxs(Text, { size: "xs", tone: "muted", "data-translate-skip": true, children: [line.where, " (", line.locale, ") \u2014 ", line.reason] }, index)))] }));
45
+ }
46
+ //# sourceMappingURL=translate-skipped.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate-skipped.js","sourceRoot":"","sources":["../../../src/components/ai/translate-skipped.tsx"],"names":[],"mappings":"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","sourcesContent":["import { resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { AlertTriangle } from 'lucide-react'\nimport { Row, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { localeChipLabel } from '@elytracms/studio/lib/locale'\nimport type { TranslateSkip, TranslateSkipReason } from '@elytracms/studio/lib/ai'\n\n/**\n * EC-593 (ratified 4e) — what a translation run could NOT translate, shown where the\n * editor reads the outcome: the \"Translate document\" dialog and the chat's translation\n * proposal card. Before, a localized object or composition was skipped with no word,\n * and a garbled rich-text block silently kept its original.\n */\n\nconst REASON_KEY: Record<TranslateSkipReason, string> = {\n unreadable: 'ai.translate.skipped.unreadable',\n 'not-returned': 'ai.translate.skipped.notReturned',\n 'structure-changed': 'ai.translate.skipped.structureChanged',\n}\n\n/** One skip as the editor reads it: where (field label › component › position), which language, why. */\nexport interface TranslateSkipLine {\n where: string\n locale: string\n reason: string\n}\n\n/**\n * The where-label of a skipped position: the field's label, then — inside a component —\n * the component and its prop path, else the position below the field (an item counted\n * from 1). Index-bearing segments the editor does not see (`content`, `props`) are left out.\n */\nexport function translateSkipLines(\n skipped: readonly TranslateSkip[],\n collection: CollectionDef | undefined,\n): TranslateSkipLine[] {\n return skipped.map((skip) => {\n const field = collection?.fields.find((f) => f.name === skip.field)\n const label = resolveLabel(field?.form?.label) ?? skip.field\n const below = skip.component\n ? [skip.component.componentId, ...skip.at.slice(skip.at.lastIndexOf('props') + 1)]\n : skip.at\n const segments = below\n .filter((segment) => segment !== 'content' && segment !== 'props')\n .map((segment) => (typeof segment === 'number' ? `#${segment + 1}` : segment))\n return {\n where: [label, ...segments].join(' › '),\n locale: localeChipLabel(skip.locale),\n reason: translate(REASON_KEY[skip.reason]),\n }\n })\n}\n\n/** The list; renders nothing when everything was translated. */\nexport function TranslateSkipped({\n skipped,\n collection,\n}: {\n skipped: readonly TranslateSkip[]\n collection: CollectionDef | undefined\n}) {\n if (skipped.length === 0) return null\n return (\n <Stack gap=\"1\" data-testid=\"translate-skipped\">\n <Row gap=\"1\" align=\"center\">\n <AlertTriangle className=\"size-3.5 text-warning\" />\n <Text size=\"xs\" weight=\"medium\">\n {translate('ai.translate.skippedTitle', { count: skipped.length })}\n </Text>\n </Row>\n {translateSkipLines(skipped, collection).map((line, index) => (\n <Text key={index} size=\"xs\" tone=\"muted\" data-translate-skip>\n {line.where} ({line.locale}) — {line.reason}\n </Text>\n ))}\n </Stack>\n )\n}\n"]}