@elytracms/studio 0.0.92 → 0.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/convex/asset-alt-migration.test.ts +127 -0
  2. package/convex/asset-replace.test.ts +168 -0
  3. package/convex/asset-revalidate.test.ts +6 -6
  4. package/convex/assets.ts +41 -5
  5. package/convex/cms.ts +25 -11
  6. package/convex/contract-parity.test.ts +18 -3
  7. package/convex/document-revalidate.test.ts +10 -12
  8. package/convex/orphan-hydration.test.ts +116 -0
  9. package/convex/rail-write-parity.test.ts +11 -8
  10. package/convex/schema.ts +6 -0
  11. package/dist/components/ai/proposal-card.js +2 -1
  12. package/dist/components/ai/proposal-card.js.map +1 -1
  13. package/dist/components/ai/translate-document-dialog.js +4 -2
  14. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  15. package/dist/components/ai/translate-skipped.d.ts +19 -0
  16. package/dist/components/ai/translate-skipped.js +46 -0
  17. package/dist/components/ai/translate-skipped.js.map +1 -0
  18. package/dist/components/assets/asset-detail.js +18 -8
  19. package/dist/components/assets/asset-detail.js.map +1 -1
  20. package/dist/components/assets/asset-metadata.d.ts +15 -4
  21. package/dist/components/assets/asset-metadata.js +15 -2
  22. package/dist/components/assets/asset-metadata.js.map +1 -1
  23. package/dist/components/assets/asset-search.js +4 -1
  24. package/dist/components/assets/asset-search.js.map +1 -1
  25. package/dist/components/assets/asset-selection.d.ts +0 -8
  26. package/dist/components/assets/asset-selection.js +4 -23
  27. package/dist/components/assets/asset-selection.js.map +1 -1
  28. package/dist/components/assets/asset-thumb.js +3 -2
  29. package/dist/components/assets/asset-thumb.js.map +1 -1
  30. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  31. package/dist/components/assets/assets-collection-view-source.js +9 -5
  32. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  33. package/dist/components/assets/assets-store.d.ts +14 -0
  34. package/dist/components/assets/assets-store.js +23 -1
  35. package/dist/components/assets/assets-store.js.map +1 -1
  36. package/dist/components/assets/replace-file.d.ts +22 -0
  37. package/dist/components/assets/replace-file.js +59 -0
  38. package/dist/components/assets/replace-file.js.map +1 -0
  39. package/dist/components/builder/blocks-model.d.ts +14 -26
  40. package/dist/components/builder/blocks-model.js +35 -41
  41. package/dist/components/builder/blocks-model.js.map +1 -1
  42. package/dist/components/builder/blocks-panel.d.ts +7 -3
  43. package/dist/components/builder/blocks-panel.js +6 -5
  44. package/dist/components/builder/blocks-panel.js.map +1 -1
  45. package/dist/components/builder/builder-store.d.ts +104 -15
  46. package/dist/components/builder/builder-store.js +156 -31
  47. package/dist/components/builder/builder-store.js.map +1 -1
  48. package/dist/components/builder/canvas-body.d.ts +30 -0
  49. package/dist/components/builder/canvas-body.js +38 -0
  50. package/dist/components/builder/canvas-body.js.map +1 -0
  51. package/dist/components/builder/canvas-chrome-css.js +7 -0
  52. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  53. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  54. package/dist/components/builder/canvas-dnd.js +5 -5
  55. package/dist/components/builder/canvas-dnd.js.map +1 -1
  56. package/dist/components/builder/canvas-render-context.d.ts +27 -19
  57. package/dist/components/builder/canvas-render-context.js +42 -43
  58. package/dist/components/builder/canvas-render-context.js.map +1 -1
  59. package/dist/components/builder/canvas-runs.d.ts +45 -0
  60. package/dist/components/builder/canvas-runs.js +51 -0
  61. package/dist/components/builder/canvas-runs.js.map +1 -0
  62. package/dist/components/builder/canvas.js +32 -5
  63. package/dist/components/builder/canvas.js.map +1 -1
  64. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  65. package/dist/components/builder/clipboard-actions.js +44 -7
  66. package/dist/components/builder/clipboard-actions.js.map +1 -1
  67. package/dist/components/builder/composition-field-editor.d.ts +3 -15
  68. package/dist/components/builder/composition-field-editor.js +29 -40
  69. package/dist/components/builder/composition-field-editor.js.map +1 -1
  70. package/dist/components/builder/delete-key.d.ts +5 -0
  71. package/dist/components/builder/delete-key.js +4 -0
  72. package/dist/components/builder/delete-key.js.map +1 -1
  73. package/dist/components/builder/insert-context.d.ts +17 -25
  74. package/dist/components/builder/insert-context.js +45 -92
  75. package/dist/components/builder/insert-context.js.map +1 -1
  76. package/dist/components/builder/page-tree.d.ts +3 -3
  77. package/dist/components/builder/page-tree.js +3 -3
  78. package/dist/components/builder/page-tree.js.map +1 -1
  79. package/dist/components/builder/placement-refusal.d.ts +18 -0
  80. package/dist/components/builder/placement-refusal.js +46 -0
  81. package/dist/components/builder/placement-refusal.js.map +1 -0
  82. package/dist/components/builder/property-inspector.js +4 -0
  83. package/dist/components/builder/property-inspector.js.map +1 -1
  84. package/dist/components/builder/reuse-context.js +65 -27
  85. package/dist/components/builder/reuse-context.js.map +1 -1
  86. package/dist/components/builder/reuse.d.ts +79 -1
  87. package/dist/components/builder/reuse.js +69 -2
  88. package/dist/components/builder/reuse.js.map +1 -1
  89. package/dist/components/builder/run-inspector.d.ts +11 -0
  90. package/dist/components/builder/run-inspector.js +33 -0
  91. package/dist/components/builder/run-inspector.js.map +1 -0
  92. package/dist/components/cms/asset-field.d.ts +38 -1
  93. package/dist/components/cms/asset-field.js +182 -58
  94. package/dist/components/cms/asset-field.js.map +1 -1
  95. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  96. package/dist/components/cms/asset-picker-core.js +20 -0
  97. package/dist/components/cms/asset-picker-core.js.map +1 -1
  98. package/dist/components/cms/asset-picker.js +12 -18
  99. package/dist/components/cms/asset-picker.js.map +1 -1
  100. package/dist/components/cms/bulk-actions.d.ts +3 -19
  101. package/dist/components/cms/bulk-actions.js +3 -24
  102. package/dist/components/cms/bulk-actions.js.map +1 -1
  103. package/dist/components/cms/cell.js.map +1 -1
  104. package/dist/components/cms/cms-store.d.ts +5 -4
  105. package/dist/components/cms/cms-store.js +16 -36
  106. package/dist/components/cms/cms-store.js.map +1 -1
  107. package/dist/components/cms/detail-editor.js +0 -1
  108. package/dist/components/cms/detail-editor.js.map +1 -1
  109. package/dist/components/cms/document-diff-view.d.ts +3 -1
  110. package/dist/components/cms/document-diff-view.js +12 -7
  111. package/dist/components/cms/document-diff-view.js.map +1 -1
  112. package/dist/components/cms/field-anchors.d.ts +7 -4
  113. package/dist/components/cms/field-anchors.js +9 -7
  114. package/dist/components/cms/field-anchors.js.map +1 -1
  115. package/dist/components/cms/field-control-lib.d.ts +15 -1
  116. package/dist/components/cms/field-control-lib.js +22 -3
  117. package/dist/components/cms/field-control-lib.js.map +1 -1
  118. package/dist/components/cms/gate-validation.d.ts +21 -30
  119. package/dist/components/cms/gate-validation.js +11 -32
  120. package/dist/components/cms/gate-validation.js.map +1 -1
  121. package/dist/components/cms/object-array-field.js +4 -4
  122. package/dist/components/cms/object-array-field.js.map +1 -1
  123. package/dist/components/cms/prop-fields.d.ts +7 -2
  124. package/dist/components/cms/prop-fields.js +13 -6
  125. package/dist/components/cms/prop-fields.js.map +1 -1
  126. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  127. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  128. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  129. package/dist/components/cms/rich-text/insert.js +25 -17
  130. package/dist/components/cms/rich-text/insert.js.map +1 -1
  131. package/dist/components/cms/use-publish-controller.d.ts +1 -2
  132. package/dist/components/cms/use-publish-controller.js +7 -19
  133. package/dist/components/cms/use-publish-controller.js.map +1 -1
  134. package/dist/components/cms/versions-pane.js +8 -1
  135. package/dist/components/cms/versions-pane.js.map +1 -1
  136. package/dist/components/collection-view/cms-collection-view-source.js +3 -1
  137. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  138. package/dist/components/components/transfer-dialog.js +2 -1
  139. package/dist/components/components/transfer-dialog.js.map +1 -1
  140. package/dist/components/components/transfer.d.ts +10 -10
  141. package/dist/components/components/transfer.js +13 -12
  142. package/dist/components/components/transfer.js.map +1 -1
  143. package/dist/lib/ai/apply-content.js +56 -116
  144. package/dist/lib/ai/apply-content.js.map +1 -1
  145. package/dist/lib/ai/chat-context.d.ts +8 -0
  146. package/dist/lib/ai/chat-context.js +37 -6
  147. package/dist/lib/ai/chat-context.js.map +1 -1
  148. package/dist/lib/ai/chat-state.d.ts +6 -0
  149. package/dist/lib/ai/chat-state.js.map +1 -1
  150. package/dist/lib/ai/constraints.d.ts +20 -5
  151. package/dist/lib/ai/constraints.js +125 -33
  152. package/dist/lib/ai/constraints.js.map +1 -1
  153. package/dist/lib/ai/fake-provider.d.ts +16 -3
  154. package/dist/lib/ai/fake-provider.js +22 -6
  155. package/dist/lib/ai/fake-provider.js.map +1 -1
  156. package/dist/lib/ai/index.d.ts +2 -0
  157. package/dist/lib/ai/index.js +2 -0
  158. package/dist/lib/ai/index.js.map +1 -1
  159. package/dist/lib/ai/leash.d.ts +34 -0
  160. package/dist/lib/ai/leash.js +28 -0
  161. package/dist/lib/ai/leash.js.map +1 -0
  162. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  163. package/dist/lib/ai/rich-text-input.js +2 -1
  164. package/dist/lib/ai/rich-text-input.js.map +1 -1
  165. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  166. package/dist/lib/ai/rich-text-translate.js +28 -10
  167. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  168. package/dist/lib/ai/shape.d.ts +13 -13
  169. package/dist/lib/ai/shape.js +69 -93
  170. package/dist/lib/ai/shape.js.map +1 -1
  171. package/dist/lib/ai/tools.d.ts +36 -41
  172. package/dist/lib/ai/tools.js +83 -87
  173. package/dist/lib/ai/tools.js.map +1 -1
  174. package/dist/lib/ai/translate-document.d.ts +32 -4
  175. package/dist/lib/ai/translate-document.js +144 -46
  176. package/dist/lib/ai/translate-document.js.map +1 -1
  177. package/dist/lib/ai/types.d.ts +34 -23
  178. package/dist/lib/ai/types.js.map +1 -1
  179. package/dist/lib/ai/update-fields.d.ts +15 -12
  180. package/dist/lib/ai/update-fields.js +112 -188
  181. package/dist/lib/ai/update-fields.js.map +1 -1
  182. package/dist/lib/ai/writer-form.d.ts +116 -0
  183. package/dist/lib/ai/writer-form.js +429 -0
  184. package/dist/lib/ai/writer-form.js.map +1 -0
  185. package/dist/lib/assets/upload.d.ts +31 -2
  186. package/dist/lib/assets/upload.js +61 -6
  187. package/dist/lib/assets/upload.js.map +1 -1
  188. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  189. package/dist/lib/clipboard/os-clipboard.js +8 -4
  190. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  191. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  192. package/dist/lib/clipboard/paste-target.js +8 -7
  193. package/dist/lib/clipboard/paste-target.js.map +1 -1
  194. package/dist/lib/cms/field-patch.d.ts +0 -10
  195. package/dist/lib/cms/field-patch.js +4 -30
  196. package/dist/lib/cms/field-patch.js.map +1 -1
  197. package/dist/lib/cms-views/view-model.d.ts +3 -3
  198. package/dist/lib/collection-view/collection-view-source.d.ts +7 -3
  199. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  200. package/dist/lib/data/collection-content.js +3 -2
  201. package/dist/lib/data/collection-content.js.map +1 -1
  202. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  203. package/dist/lib/dnd/drop-rules.js +53 -59
  204. package/dist/lib/dnd/drop-rules.js.map +1 -1
  205. package/dist/lib/engine-version.d.ts +1 -1
  206. package/dist/lib/engine-version.js +1 -1
  207. package/dist/lib/engine-version.js.map +1 -1
  208. package/dist/lib/i18n/messages.de.js +40 -3
  209. package/dist/lib/i18n/messages.de.js.map +1 -1
  210. package/dist/lib/i18n/messages.en.d.ts +34 -2
  211. package/dist/lib/i18n/messages.en.js +43 -3
  212. package/dist/lib/i18n/messages.en.js.map +1 -1
  213. package/dist/lib/operations/project-runtime-config.d.ts +25 -6
  214. package/dist/lib/operations/project-runtime-config.js +37 -10
  215. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  216. package/dist/lib/operations/publish-with-revision.d.ts +33 -1
  217. package/dist/lib/operations/publish-with-revision.js +32 -0
  218. package/dist/lib/operations/publish-with-revision.js.map +1 -1
  219. package/dist/lib/operations/runtime-validator.d.ts +19 -68
  220. package/dist/lib/operations/runtime-validator.js +12 -138
  221. package/dist/lib/operations/runtime-validator.js.map +1 -1
  222. package/dist/lib/operations/studio-operations.d.ts +9 -2
  223. package/dist/lib/operations/studio-operations.js +13 -11
  224. package/dist/lib/operations/studio-operations.js.map +1 -1
  225. package/dist/lib/persistence/convex-adapter.js +6 -1
  226. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  227. package/dist/lib/persona/labels.js +3 -1
  228. package/dist/lib/persona/labels.js.map +1 -1
  229. package/dist/lib/workspace/index.d.ts +8 -11
  230. package/dist/lib/workspace/index.js +12 -24
  231. package/dist/lib/workspace/index.js.map +1 -1
  232. package/package.json +3 -3
  233. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  234. package/dist/components/cms/cms-composition-validators.js +0 -133
  235. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  236. package/dist/components/cms/publish-clean.d.ts +0 -28
  237. package/dist/components/cms/publish-clean.js +0 -38
  238. package/dist/components/cms/publish-clean.js.map +0 -1
  239. package/dist/lib/assets/asset-placements.d.ts +0 -28
  240. package/dist/lib/assets/asset-placements.js +0 -83
  241. package/dist/lib/assets/asset-placements.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/ai/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — headless AI platform seam types (EC-331, phase 1).\n *\n * Pure types only: this module imports nothing at runtime (every import is\n * `import type`), so referencing `StudioAiConfig` from the studio-config type does\n * NOT pull the `ai` / `@ai-sdk/*` runtime onto the critical path. The runtime lives\n * in the sibling modules (`config`, `anthropic-provider`, `fake-provider`,\n * `knowledge`, `shape`, `apply-content`, `route`, `provenance`), reached only when\n * the `ai` block is configured and the AI subsystem actually boots. The Anthropic\n * provider additionally loads `ai`/`@ai-sdk/anthropic` via dynamic `import()`, so\n * even importing this barrel never eagerly loads the SDK.\n */\nimport type {\n CollectionDef,\n CollectionRegistry,\n CmsValidationIssue,\n FieldValues,\n JsonSchema,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\n// Type-only on purpose (erased at compile; `verbatimModuleSyntax`): referencing the\n// SDK's `LanguageModel` here creates NO runtime edge — pinned in `bundle.test.ts`.\nimport type { LanguageModel } from 'ai'\n\n// --- Provider seam ----------------------------------------------------------------\n\n/**\n * The capabilities the engine requires of a provider. Both must be `true` — the\n * capability gate (`resolveAiConfig`) fails loud otherwise (tool-calling +\n * structured output are the non-negotiable floor for schema-guaranteed authoring).\n */\nexport interface AiProviderCapabilities {\n /** The provider can call tools (function/tool calling). */\n toolCalling: boolean\n /** The provider can return schema-constrained structured output. */\n structuredOutput: boolean\n}\n\n/** A request for one schema-constrained structured object — the engine's core primitive. */\nexport interface GenerateStructuredRequest {\n /** System prompt: rules, injection stance, closed-world constraints. */\n system?: string\n /** The task prompt plus assembled context (knowledge, existing values, prior issues). */\n prompt: string\n /** The target shape as a JSON Schema (draft 2020-12 subset) the output must satisfy. */\n schema: JsonSchema\n /** A short name for the schema (some providers surface it as the tool/output name). */\n schemaName?: string\n /** A one-line description of what the schema represents. */\n schemaDescription?: string\n}\n\n/** Token usage a provider may report (best-effort; recorded for soft spend counters). */\nexport interface AiUsage {\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface GenerateStructuredResult {\n /** The structured object the provider produced (best-effort match to the request schema). */\n object: unknown\n usage?: AiUsage\n}\n\n/**\n * A provider adapter over a tool-calling, structured-output model. Anthropic (or\n * the Vercel AI Gateway) in production, {@link FakeProvider} (deterministic,\n * keyless) in tests/playground — all implement this identical surface so the whole\n * loop runs without a key.\n */\nexport interface AiProvider {\n /** Stable provider id (e.g. `anthropic`, `fake`); recorded in provenance. */\n readonly id: string\n /** The model id the provider targets, when fixed; recorded in provenance. */\n readonly model?: string\n /** The provider's capabilities — validated by the capability gate at config load. */\n capabilities(): AiProviderCapabilities\n /** Produce a single schema-constrained structured object. */\n generateStructured(request: GenerateStructuredRequest): Promise<GenerateStructuredResult>\n /**\n * The AI-SDK `LanguageModel` behind this provider — the seam the route's native\n * chat branch consumes (EC-334: `streamText` over UIMessages, tools declared\n * without `execute` so they forward to the client). Optional: a pure structured\n * proxy (the client-side route provider) exposes none — the route answers 501\n * for chat requests then.\n */\n languageModel?(): Promise<LanguageModel>\n}\n\n// --- Config seam ------------------------------------------------------------------\n\n/** Soft, advisory limits for the route counter and the engine repair loop. */\nexport interface AiLimits {\n /** Soft per-project ceiling on route requests per minute (advisory; route-enforced). */\n maxRequestsPerMinute?: number\n /** Cap on repair attempts after the first generation (engine default when omitted). */\n maxRepairAttempts?: number\n}\n\n/**\n * The `ai` block of `defineStudio({ ai })` (EC-331). Studio-only live code — like\n * `fieldRenderers` / `i18n`, it never reaches the delivery bundle. The block's\n * ABSENCE means the feature is fully absent (no AI UI, no AI deps on the critical\n * path); `enabled: false` is a temporary kill-switch that keeps the config.\n */\nexport interface StudioAiConfig {\n /**\n * Temporary kill-switch. Presence of the whole `ai` block enables the feature;\n * `enabled: false` disables it without removing the config. Omitted ⇒ enabled.\n */\n enabled?: boolean\n /** The provider adapter (live code): Anthropic in production, FakeProvider in tests/playground. */\n provider: AiProvider\n /** Project knowledge sources assembled into the model's context. */\n knowledge?: KnowledgeSource[]\n /** Soft rate / repair limits. */\n limits?: AiLimits\n}\n\n// --- Knowledge seam ---------------------------------------------------------------\n\n/** An inline knowledge literal (markdown prose versioned with the repo). */\nexport interface KnowledgeItem {\n title?: string\n content: string\n}\n\n/**\n * A `fromCollection('<id>')` source: studio-editable knowledge whose *content* is\n * prose held in the DB (data), while the mechanism stays code. Resolved by a\n * caller-supplied collection reader (starter-kit / studio wires it to operations).\n */\nexport interface KnowledgeCollectionSource {\n readonly kind: 'collection'\n /** Collection id whose documents supply editable knowledge prose. */\n readonly collectionId: string\n /** Field carrying the prose; when omitted the reader picks the collection's title/first text field. */\n readonly field?: string\n}\n\n/** An async (or sync) function producing knowledge items at resolve time. */\nexport type KnowledgeFn = () =>\n | KnowledgeItem\n | KnowledgeItem[]\n | Promise<KnowledgeItem | KnowledgeItem[]>\n\n/** A knowledge source: inline literal, async function, or `fromCollection(...)` sugar. */\nexport type KnowledgeSource = KnowledgeItem | KnowledgeFn | KnowledgeCollectionSource\n\n/** The assembled knowledge context — a flat, deterministic list of resolved items. */\nexport interface ResolvedKnowledge {\n items: KnowledgeItem[]\n}\n\n/** Reads knowledge prose out of a `fromCollection` source (wired to the operations layer). */\nexport type KnowledgeCollectionReader = (\n source: KnowledgeCollectionSource,\n) => KnowledgeItem[] | Promise<KnowledgeItem[]>\n\n// --- Engine seam ------------------------------------------------------------------\n\n/**\n * A relation candidate id (from a `searchDocuments`/tool result). Relation fields may\n * ONLY use ids present here — the target shape emits them as an enum, so a\n * hallucinated id is not even representable as valid structured output.\n */\nexport interface RelationCandidate {\n id: string\n /** Human label (title) for the model to choose against. */\n label?: string\n}\n\n/** Relation candidate ids keyed by field name. A field absent here is omitted from the shape. */\nexport type RelationCandidates = Record<string, RelationCandidate[]>\n\n/**\n * An asset candidate id (from a `listAssets` tool result). Asset/image fields may\n * ONLY use ids present here — the target shape emits them as an enum, so a\n * hallucinated (or invented / \"to-be-uploaded\") asset id is not even representable\n * as valid structured output. From the EXISTING media library ONLY (EC-381): the\n * assistant never generates or uploads an asset; empty beats wrong.\n */\nexport interface AssetCandidate {\n id: string\n /** Human label (title / filename) for the model to choose against. */\n label?: string\n}\n\n/** Asset candidate ids keyed by field name. A field absent here is omitted from the shape. */\nexport type AssetCandidates = Record<string, AssetCandidate[]>\n\n/** The typed draft-values patch the engine emits — applied by the draft-write path. */\nexport interface ContentPatch {\n collection: string\n id: string\n /** Non-localized field values. */\n values: FieldValues\n /** Per-locale field values (localized fields, under the target locale). */\n localized?: Record<string, FieldValues>\n}\n\n/** Origin marker the draft-write path records so an AI-authored draft is attributable. */\nexport interface AiProvenance {\n origin: 'assistant'\n /** Provider id (e.g. `anthropic`, `fake`). */\n provider: string\n /** Model id, when the provider fixes one. */\n model?: string\n /** ISO timestamp when the content was generated. */\n at: string\n}\n\nexport interface ApplyContentInput {\n provider: AiProvider\n registry: CollectionRegistry\n collection: CollectionDef\n /** Document id the patch targets (an existing doc for updates, or a fresh id for creation). */\n documentId: string\n /** The author's instruction / prompt. */\n instruction: string\n /** Assembled knowledge context (from `resolveKnowledge`). */\n knowledge?: ResolvedKnowledge\n /** Component manifests — context for `allow`-leashed richText/blocks fields. */\n components?: readonly ComponentManifest[]\n /** Relation candidate ids by field — the ONLY ids a relation field may use. */\n candidates?: RelationCandidates\n /**\n * Asset candidate ids by field — the ONLY ids an asset/image field may use\n * (EC-381). Emitted as an enum; an asset field with none is omitted from the\n * shape (so a create proposal simply leaves it empty — empty beats wrong).\n */\n assetCandidates?: AssetCandidates\n /** Existing draft values (updates): the model output merges over these before validation. */\n existing?: FieldValues\n /**\n * Target locale for localized fields (their values land under `localized[locale]`);\n * absent → the project's default locale (`validateOptions.localeConfig.default`).\n */\n locale?: string\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, passed WHOLE rather than reassembled from\n * individual keys here. EC-473 had already established that the assistant must\n * judge its output by the editor's rules; this makes that structural: there is\n * one bag, and a surface cannot quietly take a subset of it. EC-580 removed the\n * one deviation it used to carry (a per-generation `{ default: locale }` config):\n * a create in a non-default locale is judged by the project's own config, so a\n * required localized field needs its default-locale value here exactly as at\n * publish.\n */\n validateOptions?: ValidateDocumentOptions\n /** Max repair attempts after the first generation (default {@link DEFAULT_MAX_REPAIR_ATTEMPTS}). */\n maxRepairAttempts?: number\n}\n\n/** The engine's outcome: a valid, leash-compliant patch, or the blocking issues. */\nexport type ApplyContentResult =\n | {\n ok: true\n patch: ContentPatch\n provenance: AiProvenance\n /** Total generation attempts made (1 = happy path, >1 = repaired). */\n attempts: number\n /** Non-error issues surfaced by the accepted document (e.g. warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n ok: false\n /** The error-severity issues from the final failed attempt. */\n issues: CmsValidationIssue[]\n attempts: number\n provenance: AiProvenance\n }\n\n// --- Route seam -------------------------------------------------------------------\n\n/** A validated studio session, from the host-wired `getSession`. Opaque beyond `userId`. */\nexport interface AiSession {\n /** Authenticated principal id. */\n userId: string\n [key: string]: unknown\n}\n\nexport interface AiRouteHandlerOptions {\n /** The provider (holds the host-env key). The route is a thin proxy over it. */\n provider: AiProvider\n /**\n * Server-side session check (reject anonymous callers). Returns a session for an\n * authenticated caller, `null` otherwise. Wired by the host to its own auth — the\n * route consumes it, never re-implements it.\n */\n getSession: (request: Request) => AiSession | null | Promise<AiSession | null>\n /** Soft per-project rate limit (advisory spend guard). */\n limits?: AiLimits\n /** Derive the per-project rate-bucket key from a request (default: one global bucket). */\n projectKey?: (request: Request) => string\n /**\n * Millisecond clock the rate window is decided against (default `Date.now`).\n * Injected so the 60 s boundary is testable with a fixed clock (EC-602, the\n * AGENTS.md §4 determinism rule). Milliseconds rather than the house ISO\n * `Clock` (`@elytracms/core/determinism`): that one stamps records; this one\n * does window arithmetic.\n */\n now?: () => number\n}\n\n/** The mounted route handler: a framework-agnostic Web `Request → Response`. */\nexport type AiRouteHandler = (request: Request) => Promise<Response>\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/ai/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — headless AI platform seam types (EC-331, phase 1).\n *\n * Pure types only: this module imports nothing at runtime (every import is\n * `import type`), so referencing `StudioAiConfig` from the studio-config type does\n * NOT pull the `ai` / `@ai-sdk/*` runtime onto the critical path. The runtime lives\n * in the sibling modules (`config`, `anthropic-provider`, `fake-provider`,\n * `knowledge`, `shape`, `apply-content`, `route`, `provenance`), reached only when\n * the `ai` block is configured and the AI subsystem actually boots. The Anthropic\n * provider additionally loads `ai`/`@ai-sdk/anthropic` via dynamic `import()`, so\n * even importing this barrel never eagerly loads the SDK.\n */\nimport type {\n AssetFieldDef,\n CollectionDef,\n CollectionRegistry,\n CmsValidationIssue,\n FieldValues,\n JsonSchema,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\n// Type-only on purpose (erased at compile; `verbatimModuleSyntax`): referencing the\n// SDK's `LanguageModel` here creates NO runtime edge — pinned in `bundle.test.ts`.\nimport type { LanguageModel } from 'ai'\n\n// --- Provider seam ----------------------------------------------------------------\n\n/**\n * The capabilities the engine requires of a provider. Both must be `true` — the\n * capability gate (`resolveAiConfig`) fails loud otherwise (tool-calling +\n * structured output are the non-negotiable floor for schema-guaranteed authoring).\n */\nexport interface AiProviderCapabilities {\n /** The provider can call tools (function/tool calling). */\n toolCalling: boolean\n /** The provider can return schema-constrained structured output. */\n structuredOutput: boolean\n}\n\n/** A request for one schema-constrained structured object — the engine's core primitive. */\nexport interface GenerateStructuredRequest {\n /** System prompt: rules, injection stance, closed-world constraints. */\n system?: string\n /** The task prompt plus assembled context (knowledge, existing values, prior issues). */\n prompt: string\n /** The target shape as a JSON Schema (draft 2020-12 subset) the output must satisfy. */\n schema: JsonSchema\n /** A short name for the schema (some providers surface it as the tool/output name). */\n schemaName?: string\n /** A one-line description of what the schema represents. */\n schemaDescription?: string\n}\n\n/** Token usage a provider may report (best-effort; recorded for soft spend counters). */\nexport interface AiUsage {\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface GenerateStructuredResult {\n /** The structured object the provider produced (best-effort match to the request schema). */\n object: unknown\n usage?: AiUsage\n}\n\n/**\n * A provider adapter over a tool-calling, structured-output model. Anthropic (or\n * the Vercel AI Gateway) in production, {@link FakeProvider} (deterministic,\n * keyless) in tests/playground — all implement this identical surface so the whole\n * loop runs without a key.\n */\nexport interface AiProvider {\n /** Stable provider id (e.g. `anthropic`, `fake`); recorded in provenance. */\n readonly id: string\n /** The model id the provider targets, when fixed; recorded in provenance. */\n readonly model?: string\n /** The provider's capabilities — validated by the capability gate at config load. */\n capabilities(): AiProviderCapabilities\n /** Produce a single schema-constrained structured object. */\n generateStructured(request: GenerateStructuredRequest): Promise<GenerateStructuredResult>\n /**\n * The AI-SDK `LanguageModel` behind this provider — the seam the route's native\n * chat branch consumes (EC-334: `streamText` over UIMessages, tools declared\n * without `execute` so they forward to the client). Optional: a pure structured\n * proxy (the client-side route provider) exposes none — the route answers 501\n * for chat requests then.\n */\n languageModel?(): Promise<LanguageModel>\n}\n\n// --- Config seam ------------------------------------------------------------------\n\n/** Soft, advisory limits for the route counter and the engine repair loop. */\nexport interface AiLimits {\n /** Soft per-project ceiling on route requests per minute (advisory; route-enforced). */\n maxRequestsPerMinute?: number\n /** Cap on repair attempts after the first generation (engine default when omitted). */\n maxRepairAttempts?: number\n}\n\n/**\n * The `ai` block of `defineStudio({ ai })` (EC-331). Studio-only live code — like\n * `fieldRenderers` / `i18n`, it never reaches the delivery bundle. The block's\n * ABSENCE means the feature is fully absent (no AI UI, no AI deps on the critical\n * path); `enabled: false` is a temporary kill-switch that keeps the config.\n */\nexport interface StudioAiConfig {\n /**\n * Temporary kill-switch. Presence of the whole `ai` block enables the feature;\n * `enabled: false` disables it without removing the config. Omitted ⇒ enabled.\n */\n enabled?: boolean\n /** The provider adapter (live code): Anthropic in production, FakeProvider in tests/playground. */\n provider: AiProvider\n /** Project knowledge sources assembled into the model's context. */\n knowledge?: KnowledgeSource[]\n /** Soft rate / repair limits. */\n limits?: AiLimits\n}\n\n// --- Knowledge seam ---------------------------------------------------------------\n\n/** An inline knowledge literal (markdown prose versioned with the repo). */\nexport interface KnowledgeItem {\n title?: string\n content: string\n}\n\n/**\n * A `fromCollection('<id>')` source: studio-editable knowledge whose *content* is\n * prose held in the DB (data), while the mechanism stays code. Resolved by a\n * caller-supplied collection reader (starter-kit / studio wires it to operations).\n */\nexport interface KnowledgeCollectionSource {\n readonly kind: 'collection'\n /** Collection id whose documents supply editable knowledge prose. */\n readonly collectionId: string\n /** Field carrying the prose; when omitted the reader picks the collection's title/first text field. */\n readonly field?: string\n}\n\n/** An async (or sync) function producing knowledge items at resolve time. */\nexport type KnowledgeFn = () =>\n | KnowledgeItem\n | KnowledgeItem[]\n | Promise<KnowledgeItem | KnowledgeItem[]>\n\n/** A knowledge source: inline literal, async function, or `fromCollection(...)` sugar. */\nexport type KnowledgeSource = KnowledgeItem | KnowledgeFn | KnowledgeCollectionSource\n\n/** The assembled knowledge context — a flat, deterministic list of resolved items. */\nexport interface ResolvedKnowledge {\n items: KnowledgeItem[]\n}\n\n/** Reads knowledge prose out of a `fromCollection` source (wired to the operations layer). */\nexport type KnowledgeCollectionReader = (\n source: KnowledgeCollectionSource,\n) => KnowledgeItem[] | Promise<KnowledgeItem[]>\n\n// --- Engine seam ------------------------------------------------------------------\n\n/**\n * A relation candidate id (from a `searchDocuments` tool result). A relation may ONLY\n * use a searched id, or one the document already references — at every position\n * (EC-593): the create shape emits them as an enum, so a hallucinated id is not even\n * representable as valid structured output, and the writer-form converter refuses (edit)\n * or drops (create) any other.\n */\nexport interface RelationCandidate {\n id: string\n /** Human label (title) for the model to choose against. */\n label?: string\n}\n\n/**\n * An asset candidate id (from a `listAssets` tool result). An asset position may ONLY\n * hold a searched library asset that fits its `accept` (EC-588), or one the document\n * already references — at every position (EC-593). From the EXISTING media library ONLY\n * (EC-381): the assistant never generates or uploads an asset; empty beats wrong.\n */\nexport interface AssetCandidate {\n id: string\n /** Human label (title / filename) for the model to choose against. */\n label?: string\n}\n\n/**\n * THE id leash (EC-381, at every position since EC-593): what the model searched this\n * conversation, asked per POSITION rather than per top-level field name — a relation by\n * its target collection, an asset by its declaration (its `accept`). A nested relation\n * in an object, an asset prop of a component inside a composition and a top-level field\n * are judged by the same two questions. The chat's per-conversation candidate store is\n * one (`createChatCandidateStore`); `writerLeash` builds one from plain search results.\n */\nexport interface WriterLeash {\n /** The searched documents a reference to `target` may use. */\n relationCandidates(target: string): readonly RelationCandidate[]\n /** The searched library assets an asset position declared by `field` may hold (its `accept` applied). */\n assetCandidates(field: AssetFieldDef): readonly AssetCandidate[]\n}\n\n/** The typed draft-values patch the engine emits — applied by the draft-write path. */\nexport interface ContentPatch {\n collection: string\n id: string\n /** Non-localized field values. */\n values: FieldValues\n /** Per-locale field values (localized fields, under the target locale). */\n localized?: Record<string, FieldValues>\n}\n\n/** Origin marker the draft-write path records so an AI-authored draft is attributable. */\nexport interface AiProvenance {\n origin: 'assistant'\n /** Provider id (e.g. `anthropic`, `fake`). */\n provider: string\n /** Model id, when the provider fixes one. */\n model?: string\n /** ISO timestamp when the content was generated. */\n at: string\n}\n\nexport interface ApplyContentInput {\n provider: AiProvider\n registry: CollectionRegistry\n collection: CollectionDef\n /** Document id the patch targets (an existing doc for updates, or a fresh id for creation). */\n documentId: string\n /** The author's instruction / prompt. */\n instruction: string\n /** Assembled knowledge context (from `resolveKnowledge`). */\n knowledge?: ResolvedKnowledge\n /**\n * THE id leash at every position (EC-381 → EC-593): the ONLY ids a relation or an\n * asset may use, wherever it sits. Emitted as an enum on the shape; a position with\n * none is omitted (so a create proposal simply leaves it empty — empty beats wrong),\n * and an id outside it is dropped and reported as a warning. Absent → no ids at all.\n */\n leash?: WriterLeash\n /**\n * Existing draft values (updates): the model output merges over these before\n * validation, and an asset position carries its placement sub-fields from the value\n * here (EC-570 #3 — the one carry rule, `carryAssetPlacements`).\n */\n existing?: FieldValues\n /**\n * Target locale for localized fields (their values land under `localized[locale]`);\n * absent → the project's default locale (`validateOptions.localeConfig.default`).\n */\n locale?: string\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, passed WHOLE rather than reassembled from\n * individual keys here. EC-473 had already established that the assistant must\n * judge its output by the editor's rules; this makes that structural: there is\n * one bag, and a surface cannot quietly take a subset of it. EC-580 removed the\n * one deviation it used to carry (a per-generation `{ default: locale }` config):\n * a create in a non-default locale is judged by the project's own config, so a\n * required localized field needs its default-locale value here exactly as at\n * publish.\n */\n validateOptions?: ValidateDocumentOptions\n /** Max repair attempts after the first generation (default {@link DEFAULT_MAX_REPAIR_ATTEMPTS}). */\n maxRepairAttempts?: number\n}\n\n/** The engine's outcome: a valid, leash-compliant patch, or the blocking issues. */\nexport type ApplyContentResult =\n | {\n ok: true\n patch: ContentPatch\n provenance: AiProvenance\n /** Total generation attempts made (1 = happy path, >1 = repaired). */\n attempts: number\n /** Non-error issues surfaced by the accepted document (e.g. warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n ok: false\n /** The error-severity issues from the final failed attempt. */\n issues: CmsValidationIssue[]\n attempts: number\n provenance: AiProvenance\n }\n\n// --- Route seam -------------------------------------------------------------------\n\n/** A validated studio session, from the host-wired `getSession`. Opaque beyond `userId`. */\nexport interface AiSession {\n /** Authenticated principal id. */\n userId: string\n [key: string]: unknown\n}\n\nexport interface AiRouteHandlerOptions {\n /** The provider (holds the host-env key). The route is a thin proxy over it. */\n provider: AiProvider\n /**\n * Server-side session check (reject anonymous callers). Returns a session for an\n * authenticated caller, `null` otherwise. Wired by the host to its own auth — the\n * route consumes it, never re-implements it.\n */\n getSession: (request: Request) => AiSession | null | Promise<AiSession | null>\n /** Soft per-project rate limit (advisory spend guard). */\n limits?: AiLimits\n /** Derive the per-project rate-bucket key from a request (default: one global bucket). */\n projectKey?: (request: Request) => string\n /**\n * Millisecond clock the rate window is decided against (default `Date.now`).\n * Injected so the 60 s boundary is testable with a fixed clock (EC-602, the\n * AGENTS.md §4 determinism rule). Milliseconds rather than the house ISO\n * `Clock` (`@elytracms/core/determinism`): that one stamps records; this one\n * does window arithmetic.\n */\n now?: () => number\n}\n\n/** The mounted route handler: a framework-agnostic Web `Request → Response`. */\nexport type AiRouteHandler = (request: Request) => Promise<Response>\n"]}
@@ -1,17 +1,17 @@
1
1
  import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentPatch, FieldValues, ValidateDocumentOptions } from '@elytracms/core/cms-core';
2
- import type { AssetCandidates, ContentPatch, RelationCandidates } from './types.js';
2
+ import type { ContentPatch, WriterLeash } from './types.js';
3
3
  /**
4
4
  * One field change of an `updateFields` proposal, addressed with the engine's
5
5
  * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a
6
- * localized field), and for a `blocks` field optionally `blockId` + `prop` to
7
- * change one component prop of one root block. `value` is the new value
8
- * (`null`/omitted clears the field or prop).
6
+ * localized field), and for a component inside a composition optionally
7
+ * `blockId` + `prop` to change one prop of that component — at any depth.
8
+ * `value` is the new value (`null`/omitted clears the field or prop).
9
9
  */
10
10
  export interface UpdateFieldChange {
11
11
  field: string;
12
12
  /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */
13
13
  locale?: string;
14
- /** For a `blocks` field: the root block's stable node id (from `getDocument`). */
14
+ /** A component's stable node id (from `getDocument`) — at any depth: slots, rich-text embeds. */
15
15
  blockId?: string;
16
16
  /** With `blockId`: the component prop to change. */
17
17
  prop?: string;
@@ -26,15 +26,18 @@ export interface BuildUpdateProposalInput {
26
26
  changes: readonly UpdateFieldChange[];
27
27
  /** The editor's working locale — the default bucket for localized fields. */
28
28
  locale?: string;
29
- /** Relation candidates (searched ids) keyed by field name — the id leash. */
30
- candidates?: RelationCandidates;
31
- /** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */
32
- assetCandidates?: AssetCandidates;
29
+ /**
30
+ * THE id leash at every position (EC-381 → EC-593): what the model searched this
31
+ * conversation. An id that is neither searched nor already on the document fails.
32
+ */
33
+ leash?: WriterLeash;
33
34
  /**
34
35
  * THE validation option bag (EC-497) — the studio session's
35
36
  * `gateValidateOptions(...)`, whole. It also carries the project's
36
- * `localeConfig`, which is what the unknown-locale targeting check reads: one
37
- * source for "which locales exist", not two that can disagree.
37
+ * `localeConfig` — what the unknown-locale targeting check and the displayed-value
38
+ * carry read — and the `lookupManifest` the converter reaches component props
39
+ * through: one source for "which locales exist" and "which props a component
40
+ * declares", not two that can disagree.
38
41
  */
39
42
  validateOptions?: ValidateDocumentOptions;
40
43
  }
@@ -50,7 +53,7 @@ export type BuildUpdateProposalResult = {
50
53
  patch: ContentPatch;
51
54
  /**
52
55
  * EC-595 A2 — the proposal as the positions it changes (a field's bucket, or one
53
- * block prop by node id): what an accepted proposal persists, so applying it
56
+ * component prop by node path): what an accepted proposal persists, so applying it
54
57
  * never overwrites a change made to any other position since it was built.
55
58
  */
56
59
  patches: DocumentPatch[];
@@ -11,15 +11,25 @@
11
11
  * never a whole-document rewrite; the patch is the merged document, the diff
12
12
  * (`diffDocuments`) is what the editor reviews (changed fields only).
13
13
  * - **targeting is addressed with the engine's existing conventions** — field
14
- * name + optional locale bucket (the diff / `RestoreScope` shape) and, inside
15
- * a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,
16
- * root blocks by stable node id). No new path grammar.
14
+ * name + optional locale bucket (the diff / `RestoreScope` shape) and, for a
15
+ * component inside a composition, `blockId` + `prop`: the component's node id
16
+ * at ANY depth the value-position traversal reaches (a top-level block, a child
17
+ * in a slot, a component embedded in rich text — EC-593). No new path grammar.
18
+ * - **one converter** (EC-593, closes EC-571): every value goes from its writer
19
+ * form to its stored form through `writer-form.ts` — the same fold create,
20
+ * translate and the read form use — so a nested markdown sub-field, a nested
21
+ * asset swap and a block's asset prop are converted, carried and leashed exactly
22
+ * as a top-level field is.
17
23
  * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND
18
24
  * asset ids (EC-381) must come from searched candidates (`searchDocuments` /
19
- * `listAssets`) or already be on the document; every targeting failure is an
20
- * explicit issue — never a silent drop (unlike the create shape, which makes bad
21
- * ids structurally unrepresentable, an update change is imperative — so it FAILS
22
- * instead of being filtered).
25
+ * `listAssets`) or already be on the document — at every position; every
26
+ * targeting failure is an explicit issue — never a silent drop (unlike the
27
+ * create shape, which makes bad ids structurally unrepresentable, an update
28
+ * change is imperative — so it FAILS instead of being filtered).
29
+ * - **content, never structure** (ratified 4c): a composition's components are
30
+ * never added, removed or moved by the assistant — a whole-value write of a
31
+ * `blocks` field is refused with the way to change a prop instead, and a
32
+ * rich-text value must keep the components embedded in it.
23
33
  * - **validation parity**: the merged document runs through `validateDocument`
24
34
  * with the studio's injected leashes. Only errors the change INTRODUCES block
25
35
  * the proposal — pre-existing brokenness on the document is not the
@@ -28,19 +38,21 @@
28
38
  *
29
39
  * Locale-awareness: a localized field's change lands in the change's `locale`,
30
40
  * defaulting to the editor's working locale (`input.locale`). Non-localized
31
- * fields reject a `locale` loudly.
41
+ * fields reject a `locale` loudly. The value a change replaces is the DISPLAYED
42
+ * value of that bucket (ratified 2c): a new locale bucket carries its placement
43
+ * sub-fields — and a component prop change its whole composition — from the
44
+ * fallback, exactly as the pickers do.
32
45
  *
33
46
  * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires
34
47
  * it to `getDocument` + the candidate store and hands the result to the review
35
48
  * sink.
36
49
  */
37
- import { cmsIssue, isFieldLocalized, nodePropPath, readAssetId, referenceNode, validateDocument, walkReferences, writeField, } from '@elytracms/core/cms-core';
38
- import { blockRootsOf, editBlockRoots, staticProp } from '@elytracms/core/project-graph';
50
+ import { cmsIssue, isFieldLocalized, isSequenceValue, resolveField, validateDocument, writeField, } from '@elytracms/core/cms-core';
51
+ import { editBlockRoots } from '@elytracms/core/project-graph';
39
52
  import { diffDocuments } from '@elytracms/core/content';
40
- import { carryAssetPlacements } from '../assets/asset-placements.js';
41
53
  import { introducedErrors, warningsOf } from './proposal-validation.js';
42
54
  import { fieldWritePatch } from '../cms/field-patch.js';
43
- import { richTextValueFromModelInput } from './rich-text-input.js';
55
+ import { fromWriterFieldValue, fromWriterFormAtNode, heldReferencesOf } from './writer-form.js';
44
56
  function isNonEmptyString(value) {
45
57
  return typeof value === 'string' && value.length > 0;
46
58
  }
@@ -49,175 +61,34 @@ function isNonEmptyString(value) {
49
61
  // imported)", MODE F5b.bkt). The locale is resolved per change below: a localized
50
62
  // field always arrives with one (a missing one is an explicit issue first).
51
63
  /**
52
- * The read twin of core's `writeField`: the field's value in the resolved
53
- * `locale` bucket, or in the shared `values` for a non-localized field. The exact
54
- * bucket only, with no default-locale fallback, because that bucket is the one the
55
- * write replaces.
64
+ * The value a change REPLACES (ratified 2c, EC-593): the DISPLAYED value of the field in
65
+ * the resolved bucket — core's `resolveField`, the rule every editor surface reads — so a
66
+ * new locale bucket carries its placement sub-fields (and a component prop change its
67
+ * composition) from the fallback, as the pickers do. It used to be the exact bucket only.
68
+ * Without a locale config there is no fallback to display: the bucket itself.
56
69
  */
57
- function readFieldValue(doc, fieldName, locale) {
58
- return locale !== undefined ? doc.localized?.[locale]?.[fieldName] : doc.values[fieldName];
70
+ function displayedFieldValue(input, doc, fieldName, locale) {
71
+ if (locale === undefined)
72
+ return doc.values[fieldName];
73
+ const config = input.validateOptions?.localeConfig;
74
+ return config
75
+ ? resolveField(input.collection, doc, fieldName, locale, config).value
76
+ : doc.localized?.[locale]?.[fieldName];
59
77
  }
60
- /**
61
- * Relation ids CURRENTLY on the document for a field (any locale bucket) — re-usable
62
- * without a search. EC-589: read off THE reference walk — the hits AT the field's own
63
- * position (the field, or one item of a list-valued field), never one nested deeper.
64
- */
65
- function currentRelationIds(doc, fieldName) {
66
- const ids = new Set();
67
- walkReferences(doc, (hit) => {
68
- if (hit.kind !== 'relation-field' || hit.target.type !== 'document')
69
- return;
70
- const [field, second] = hit.position;
71
- if (field?.kind !== 'field' || field.name !== fieldName)
72
- return;
73
- if (hit.position.length > (second?.kind === 'locale' ? 2 : 1))
74
- return;
75
- if (hit.within.some((key) => typeof key === 'string'))
76
- return;
77
- ids.add(hit.target.ref.id);
78
- });
79
- return ids;
80
- }
81
- /** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */
82
- function currentAssetIds(doc, fieldName) {
83
- const ids = new Set();
84
- const collect = (value) => {
85
- for (const entry of Array.isArray(value) ? value : [value]) {
86
- const id = readAssetId(entry);
87
- if (id)
88
- ids.add(id);
89
- }
90
- };
91
- collect(doc.values[fieldName]);
92
- for (const bucket of Object.values(doc.localized ?? {}))
93
- collect(bucket[fieldName]);
94
- return ids;
95
- }
96
- /**
97
- * Map a change's raw value into the stored value shape (envelopes; loud leash).
98
- * `document` is the ORIGINAL document, which is what the id leash reads. `current`
99
- * is the value this change replaces, taken from the resolved locale bucket of the
100
- * working copy. The asset sub-field carry reads it (EC-558).
101
- */
102
- function mapUpdateValue(field, change, document, current, input, issue) {
103
- const raw = change.value;
104
- if (raw === undefined || raw === null)
105
- return { value: undefined }; // clear
106
- switch (field.type) {
107
- case 'relation': {
108
- const ids = Array.isArray(raw) ? raw : [raw];
109
- if (!ids.every(isNonEmptyString)) {
110
- return issue('invalid-field-value', `Relation "${field.name}" takes document id string(s) from searchDocuments results.`);
111
- }
112
- // The id leash (same stance as the create enum, but LOUD): only searched
113
- // candidate ids or ids already on the document are usable.
114
- const allowed = new Set([
115
- ...(input.candidates?.[field.name] ?? []).map((c) => c.id),
116
- ...currentRelationIds(document, field.name),
117
- ]);
118
- const unknown = ids.filter((id) => !allowed.has(id));
119
- if (unknown.length > 0) {
120
- return issue('unknown-relation-target', `Relation "${field.name}" may only use ids from searchDocuments results — search "${field.target}" first (got: ${unknown.join(', ')}).`);
121
- }
122
- if (!field.multiple && ids.length > 1) {
123
- return issue('invalid-field-value', `Field "${field.name}" holds a single reference — pass one id.`);
124
- }
125
- const refs = ids.map((id) => referenceNode({ collection: field.target, id }));
126
- return { value: field.multiple ? refs : refs[0] };
127
- }
128
- case 'asset': {
129
- const ids = Array.isArray(raw) ? raw : [raw];
130
- if (!ids.every(isNonEmptyString)) {
131
- return issue('invalid-field-value', `Asset field "${field.name}" takes asset id string(s) from listAssets results.`);
132
- }
133
- // EC-381 — the asset id leash (same stance as relations, but LOUD): only
134
- // searched library candidate ids or ids already on the document are usable;
135
- // the assistant never invents or uploads an asset.
136
- const allowed = new Set([
137
- ...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),
138
- ...currentAssetIds(document, field.name),
139
- ]);
140
- const unknown = ids.filter((id) => !allowed.has(id));
141
- if (unknown.length > 0) {
142
- return issue('unknown-asset', `Asset field "${field.name}" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`);
143
- }
144
- if (!field.multiple && ids.length > 1) {
145
- return issue('invalid-field-value', `Field "${field.name}" holds a single asset — pass one id.`);
146
- }
147
- // EC-558: replace-with-list through the one carry rule. A held id keeps its
148
- // placement's per-use sub-fields (a caption, say), a single swap carries
149
- // them to the new image, and a NEW id is an envelope (EC-381). Data about
150
- // the image itself (alt, an AI flag) belongs on the asset's `meta`.
151
- return {
152
- value: carryAssetPlacements(current, field.multiple ? { multiple: true, ids } : { multiple: false, id: ids[0] ?? null }),
153
- };
154
- }
155
- case 'richText': {
156
- // EC-379: a string value is MARKDOWN, converted through the engine's strict
157
- // subset converter; out-of-subset constructs fail LOUD (the model retries
158
- // in-conversation). A `{ version, doc }` envelope object stays accepted for
159
- // robustness (getDocument may have shown one) — see rich-text-input.ts.
160
- const converted = richTextValueFromModelInput(raw);
161
- if (!converted.ok) {
162
- return issue('invalid-field-value', `Field "${field.name}": ${converted.problems.join(' ')}`);
163
- }
164
- return { value: converted.value };
165
- }
166
- default:
167
- // text / number / boolean / date / select / object / blocks: as sent. The
168
- // write below (`writeField`) canonicalises it (EC-586: a date into one UTC
169
- // instant, a multi-select scalar into a list, an empty list absent), and the
170
- // merged document's validateDocument run is the shape leash.
171
- return { value: raw };
172
- }
173
- }
174
- /** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */
175
- function applyBlockPropChange(collection, doc, field, change, locale, issue) {
176
- if (field.type !== 'blocks') {
177
- return issue('invalid-field-value', `"${change.field}" is not a blocks field — omit "blockId"/"prop" (set the field value directly).`);
178
- }
179
- if (!isNonEmptyString(change.blockId)) {
180
- return issue('invalid-field-value', `Changing a block on "${change.field}" needs "blockId" — block ids come from getDocument.`);
181
- }
182
- if (!isNonEmptyString(change.prop)) {
183
- return issue('invalid-field-value', `Changing block "${change.blockId}" needs "prop" — the component property to change.`);
184
- }
185
- const original = readFieldValue(doc, change.field, locale);
186
- const blocks = blockRootsOf(original);
187
- const index = blocks.findIndex((block) => block.id === change.blockId);
188
- if (index < 0) {
189
- return issue('invalid-field-value', `Block "${change.blockId}" was not found on "${change.field}" — read the document first; block ids come from getDocument.`);
190
- }
191
- const block = blocks[index];
192
- const props = { ...(block.props ?? {}) };
193
- if (change.value === undefined || change.value === null)
194
- delete props[change.prop];
195
- else
196
- props[change.prop] = staticProp(change.value);
197
- const nextBlock = { ...block, props };
198
- // EC-576: through the core write path — the other roots and every typography
199
- // item keep their place, and the write is the v2 sequence. (`blocks` is
200
- // `editBlockRoots`' own root list, so `index` addresses the same root.)
201
- const value = editBlockRoots(original, (roots) => roots.map((b, i) => (i === index ? nextBlock : b)));
202
- // EC-595 A2: one block prop, by node id — the position this change wrote.
203
- const path = nodePropPath(change.field, locale, change.blockId, change.prop);
204
- const patch = change.value === undefined || change.value === null
205
- ? { op: 'unset', path }
206
- : { op: 'set', path, value: change.value };
207
- return { doc: writeField(collection, doc, change.field, locale, value), patch };
208
- }
209
- /** Apply ONE change to a (copy of the) document, or report the targeting issue. */
210
- function applyOneChange(input, doc, change) {
78
+ /** Apply ONE change to a (copy of the) document, or report the targeting issues. */
79
+ function applyOneChange(input, ctx, doc, change) {
211
80
  const { collection } = input;
212
- const issue = (code, message) => ({
213
- issue: cmsIssue({
214
- code,
215
- message,
216
- path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],
217
- collectionId: collection.id,
218
- documentId: doc.id,
219
- meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },
220
- }),
81
+ const issueAt = (code, message, at = []) => cmsIssue({
82
+ code,
83
+ message,
84
+ path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field, ...at],
85
+ collectionId: collection.id,
86
+ documentId: doc.id,
87
+ meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },
88
+ });
89
+ const issue = (code, message) => ({ issues: [issueAt(code, message)] });
90
+ const fromProblems = (problems) => ({
91
+ issues: problems.map((problem) => issueAt(problem.code, problem.message, problem.at)),
221
92
  });
222
93
  const field = collection.fields.find((f) => f.name === change.field);
223
94
  if (!field) {
@@ -239,15 +110,61 @@ function applyOneChange(input, doc, change) {
239
110
  if (localized && locale === undefined) {
240
111
  return issue('missing-localized-value', `Field "${change.field}" is localized — provide "locale".`);
241
112
  }
113
+ const displayed = displayedFieldValue(input, doc, change.field, locale);
114
+ // The converter already wrote every value it changed in its stored form. The write
115
+ // does not re-canonicalise component props (no manifest lookup): that would rewrite
116
+ // the prop wrappers of every UNTOUCHED node too, and a proposal must change only what
117
+ // it changes (the persister canonicalises on save, as for every editor write).
118
+ const write = (value) => {
119
+ const written = writeField(collection, doc, change.field, locale, value);
120
+ return { doc: written, patch: fieldWritePatch(collection, written, change.field, locale) };
121
+ };
242
122
  if (change.blockId !== undefined || change.prop !== undefined) {
243
- return applyBlockPropChange(collection, doc, field, change, locale, issue);
123
+ // A bucket that does not hold the field yet is written whole (from its displayed
124
+ // fallback): a node path into it would address a node that is not there.
125
+ const ownsBucket = locale === undefined || change.field in (doc.localized?.[locale] ?? {});
126
+ return applyNodePropChange(field, change, displayed, locale, ownsBucket, ctx, issue, fromProblems, write);
244
127
  }
245
- const current = readFieldValue(doc, change.field, locale);
246
- const mapped = mapUpdateValue(field, change, input.document, current, input, issue);
247
- if ('issue' in mapped)
248
- return mapped;
249
- const written = writeField(collection, doc, change.field, locale, mapped.value);
250
- return { doc: written, patch: fieldWritePatch(collection, written, change.field, locale) };
128
+ // EC-593: the ONE writer-form converter, at every depth (EC-571: this path used to
129
+ // convert top-level rich text only and store a nested markdown string raw).
130
+ const converted = fromWriterFieldValue(field, change.value, displayed, ctx, locale);
131
+ if (converted.problems.length > 0)
132
+ return fromProblems(converted.problems);
133
+ return write(converted.value);
134
+ }
135
+ /**
136
+ * Change one component prop, addressed by node id at any depth (EC-593: it used to be a
137
+ * ROOT block of a `blocks` field only, written verbatim — no carry, no leash, no
138
+ * markdown conversion, and the persisted patch carried the RAW model value).
139
+ */
140
+ function applyNodePropChange(field, change, displayed, locale, ownsBucket, ctx, issue, fromProblems, write) {
141
+ if (!isNonEmptyString(change.blockId)) {
142
+ return issue('invalid-field-value', `Changing a component on "${change.field}" needs "blockId" — component ids come from getDocument.`);
143
+ }
144
+ if (!isNonEmptyString(change.prop)) {
145
+ return issue('invalid-field-value', `Changing component "${change.blockId}" needs "prop" — the component property to change.`);
146
+ }
147
+ // EC-576 tolerant read (reseed gate, EC-576 step 4 deletes it with the other legacy
148
+ // blocks readers): a pre-0.0.84 bare node / node list is lifted into the v2 sequence
149
+ // the traversal reads — and the write is that sequence (no writer re-emits the bare shape).
150
+ const composition = field.type === 'blocks' && displayed !== undefined && !isSequenceValue(displayed)
151
+ ? editBlockRoots(displayed, (roots) => roots)
152
+ : displayed;
153
+ const result = fromWriterFormAtNode(field, composition, change.blockId, change.prop, change.value, ctx, locale);
154
+ if (!result.ok)
155
+ return issue('invalid-field-value', result.message);
156
+ if (result.problems.length > 0)
157
+ return fromProblems(result.problems);
158
+ const written = write(result.value);
159
+ // EC-595 A2: the position this change wrote — the prop by its node path, or (where the
160
+ // grammar does not reach the node, the bucket is new, or a legacy value was lifted)
161
+ // the field bucket, whole: a coarser path, never a wrong one.
162
+ if (!result.exact || !ownsBucket || composition !== displayed)
163
+ return written;
164
+ const patch = result.propValue === undefined
165
+ ? { op: 'unset', path: result.path }
166
+ : { op: 'set', path: result.path, value: result.propValue };
167
+ return { doc: written.doc, patch };
251
168
  }
252
169
  /**
253
170
  * Build the reviewable update proposal: apply every change over a copy of the
@@ -260,10 +177,17 @@ export function buildUpdateProposal(input) {
260
177
  let next = structuredClone(document);
261
178
  const targetingIssues = [];
262
179
  const patches = [];
180
+ // EC-593: one converter context for every change — the validator's own prop lookup,
181
+ // the conversation's leash, and the ids the ORIGINAL document already references.
182
+ const ctx = {
183
+ ...(input.validateOptions?.lookupManifest ? { lookupManifest: input.validateOptions.lookupManifest } : {}),
184
+ ...(input.leash ? { leash: input.leash } : {}),
185
+ held: heldReferencesOf(document),
186
+ };
263
187
  for (const change of input.changes) {
264
- const outcome = applyOneChange(input, next, change);
265
- if ('issue' in outcome)
266
- targetingIssues.push(outcome.issue);
188
+ const outcome = applyOneChange(input, ctx, next, change);
189
+ if ('issues' in outcome)
190
+ targetingIssues.push(...outcome.issues);
267
191
  else {
268
192
  next = outcome.doc;
269
193
  patches.push(outcome.patch);
@@ -1 +1 @@
1
- {"version":3,"file":"update-fields.js","sourceRoot":"","sources":["../../../src/lib/ai/update-fields.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow\n * counterpart of `applyContentWithShape`, minus the generation step).\n *\n * The MODEL authors the field changes directly in the `updateFields` tool input\n * (it has the conversation + `getDocument` output as context); this module turns\n * those changes into a reviewable proposal with the SAME engine discipline the\n * create flow has:\n *\n * - **merge semantics**: changes apply per field over the EXISTING document —\n * never a whole-document rewrite; the patch is the merged document, the diff\n * (`diffDocuments`) is what the editor reviews (changed fields only).\n * - **targeting is addressed with the engine's existing conventions** — field\n * name + optional locale bucket (the diff / `RestoreScope` shape) and, inside\n * a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,\n * root blocks by stable node id). No new path grammar.\n * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND\n * asset ids (EC-381) must come from searched candidates (`searchDocuments` /\n * `listAssets`) or already be on the document; every targeting failure is an\n * explicit issue — never a silent drop (unlike the create shape, which makes bad\n * ids structurally unrepresentable, an update change is imperative — so it FAILS\n * instead of being filtered).\n * - **validation parity**: the merged document runs through `validateDocument`\n * with the studio's injected leashes. Only errors the change INTRODUCES block\n * the proposal — pre-existing brokenness on the document is not the\n * assistant's doing and must not force a whole-document repair (the editor's\n * own save path tolerates it the same way; errors gate publish, not drafts).\n *\n * Locale-awareness: a localized field's change lands in the change's `locale`,\n * defaulting to the editor's working locale (`input.locale`). Non-localized\n * fields reject a `locale` loudly.\n *\n * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires\n * it to `getDocument` + the candidate store and hands the result to the review\n * sink.\n */\nimport {\n cmsIssue,\n isFieldLocalized,\n nodePropPath,\n readAssetId,\n referenceNode,\n validateDocument,\n walkReferences,\n writeField,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n DocumentPatch,\n FieldDef,\n FieldValues,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport { blockRootsOf, editBlockRoots, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { carryAssetPlacements } from '../assets/asset-placements'\nimport { introducedErrors, warningsOf } from './proposal-validation'\nimport { fieldWritePatch } from '../cms/field-patch'\nimport { richTextValueFromModelInput } from './rich-text-input'\nimport type { AssetCandidates, ContentPatch, RelationCandidates } from './types'\n\n/**\n * One field change of an `updateFields` proposal, addressed with the engine's\n * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a\n * localized field), and for a `blocks` field optionally `blockId` + `prop` to\n * change one component prop of one root block. `value` is the new value\n * (`null`/omitted clears the field or prop).\n */\nexport interface UpdateFieldChange {\n field: string\n /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */\n locale?: string\n /** For a `blocks` field: the root block's stable node id (from `getDocument`). */\n blockId?: string\n /** With `blockId`: the component prop to change. */\n prop?: string\n /** The new value; `null` / omitted clears the field or prop. */\n value?: unknown\n}\n\nexport interface BuildUpdateProposalInput {\n registry: CollectionRegistry\n collection: CollectionDef\n /** The CURRENT document — the proposal's \"before\" side. Never mutated. */\n document: CmsDocument\n changes: readonly UpdateFieldChange[]\n /** The editor's working locale — the default bucket for localized fields. */\n locale?: string\n /** Relation candidates (searched ids) keyed by field name — the id leash. */\n candidates?: RelationCandidates\n /** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */\n assetCandidates?: AssetCandidates\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, whole. It also carries the project's\n * `localeConfig`, which is what the unknown-locale targeting check reads: one\n * source for \"which locales exist\", not two that can disagree.\n */\n validateOptions?: ValidateDocumentOptions\n}\n\n/** The `before` snapshot the review diff renders against. */\nexport interface UpdateBeforeSnapshot {\n values: FieldValues\n localized?: Record<string, FieldValues>\n}\n\nexport type BuildUpdateProposalResult =\n | {\n ok: true\n kind: 'proposal'\n /** The MERGED document as a patch (full values; the diff shows changed fields only). */\n patch: ContentPatch\n /**\n * EC-595 A2 — the proposal as the positions it changes (a field's bucket, or one\n * block prop by node id): what an accepted proposal persists, so applying it\n * never overwrites a change made to any other position since it was built.\n */\n patches: DocumentPatch[]\n before: UpdateBeforeSnapshot\n /** Human-readable descriptors of the changed fields (e.g. `title (de)`). */\n changedFields: string[]\n /** Non-error issues on the accepted result (warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n /** Every change resolved to the current values — nothing to propose. */\n ok: true\n kind: 'no-change'\n }\n | { ok: false; issues: CmsValidationIssue[] }\n\ntype ChangeIssue = { issue: CmsValidationIssue }\ntype ChangeOutcome = { doc: CmsDocument; patch: DocumentPatch } | ChangeIssue\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0\n}\n\n// The present/absent-key field write is core's `writeField` (EC-580) — this module\n// used to carry a local copy of the editor's `setFieldValue` (\"Local here (not\n// imported)\", MODE F5b.bkt). The locale is resolved per change below: a localized\n// field always arrives with one (a missing one is an explicit issue first).\n\n/**\n * The read twin of core's `writeField`: the field's value in the resolved\n * `locale` bucket, or in the shared `values` for a non-localized field. The exact\n * bucket only, with no default-locale fallback, because that bucket is the one the\n * write replaces.\n */\nfunction readFieldValue(doc: CmsDocument, fieldName: string, locale: string | undefined): unknown {\n return locale !== undefined ? doc.localized?.[locale]?.[fieldName] : doc.values[fieldName]\n}\n\n/**\n * Relation ids CURRENTLY on the document for a field (any locale bucket) — re-usable\n * without a search. EC-589: read off THE reference walk — the hits AT the field's own\n * position (the field, or one item of a list-valued field), never one nested deeper.\n */\nfunction currentRelationIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n walkReferences(doc, (hit) => {\n if (hit.kind !== 'relation-field' || hit.target.type !== 'document') return\n const [field, second] = hit.position\n if (field?.kind !== 'field' || field.name !== fieldName) return\n if (hit.position.length > (second?.kind === 'locale' ? 2 : 1)) return\n if (hit.within.some((key) => typeof key === 'string')) return\n ids.add(hit.target.ref.id)\n })\n return ids\n}\n\n/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */\nfunction currentAssetIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const id = readAssetId(entry)\n if (id) ids.add(id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/**\n * Map a change's raw value into the stored value shape (envelopes; loud leash).\n * `document` is the ORIGINAL document, which is what the id leash reads. `current`\n * is the value this change replaces, taken from the resolved locale bucket of the\n * working copy. The asset sub-field carry reads it (EC-558).\n */\nfunction mapUpdateValue(\n field: FieldDef,\n change: UpdateFieldChange,\n document: CmsDocument,\n current: unknown,\n input: BuildUpdateProposalInput,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): { value: unknown } | ChangeIssue {\n const raw = change.value\n if (raw === undefined || raw === null) return { value: undefined } // clear\n switch (field.type) {\n case 'relation': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Relation \"${field.name}\" takes document id string(s) from searchDocuments results.`,\n )\n }\n // The id leash (same stance as the create enum, but LOUD): only searched\n // candidate ids or ids already on the document are usable.\n const allowed = new Set([\n ...(input.candidates?.[field.name] ?? []).map((c) => c.id),\n ...currentRelationIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-relation-target',\n `Relation \"${field.name}\" may only use ids from searchDocuments results — search \"${field.target}\" first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single reference — pass one id.`)\n }\n const refs = ids.map((id) => referenceNode({ collection: field.target, id }))\n return { value: field.multiple ? refs : refs[0] }\n }\n case 'asset': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Asset field \"${field.name}\" takes asset id string(s) from listAssets results.`,\n )\n }\n // EC-381 — the asset id leash (same stance as relations, but LOUD): only\n // searched library candidate ids or ids already on the document are usable;\n // the assistant never invents or uploads an asset.\n const allowed = new Set([\n ...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),\n ...currentAssetIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-asset',\n `Asset field \"${field.name}\" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single asset — pass one id.`)\n }\n // EC-558: replace-with-list through the one carry rule. A held id keeps its\n // placement's per-use sub-fields (a caption, say), a single swap carries\n // them to the new image, and a NEW id is an envelope (EC-381). Data about\n // the image itself (alt, an AI flag) belongs on the asset's `meta`.\n return {\n value: carryAssetPlacements(\n current,\n field.multiple ? { multiple: true, ids } : { multiple: false, id: ids[0] ?? null },\n ),\n }\n }\n case 'richText': {\n // EC-379: a string value is MARKDOWN, converted through the engine's strict\n // subset converter; out-of-subset constructs fail LOUD (the model retries\n // in-conversation). A `{ version, doc }` envelope object stays accepted for\n // robustness (getDocument may have shown one) — see rich-text-input.ts.\n const converted = richTextValueFromModelInput(raw)\n if (!converted.ok) {\n return issue('invalid-field-value', `Field \"${field.name}\": ${converted.problems.join(' ')}`)\n }\n return { value: converted.value }\n }\n default:\n // text / number / boolean / date / select / object / blocks: as sent. The\n // write below (`writeField`) canonicalises it (EC-586: a date into one UTC\n // instant, a multi-select scalar into a list, an empty list absent), and the\n // merged document's validateDocument run is the shape leash.\n return { value: raw }\n }\n}\n\n/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */\nfunction applyBlockPropChange(\n collection: CollectionDef,\n doc: CmsDocument,\n field: FieldDef,\n change: UpdateFieldChange,\n locale: string | undefined,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): ChangeOutcome {\n if (field.type !== 'blocks') {\n return issue(\n 'invalid-field-value',\n `\"${change.field}\" is not a blocks field — omit \"blockId\"/\"prop\" (set the field value directly).`,\n )\n }\n if (!isNonEmptyString(change.blockId)) {\n return issue(\n 'invalid-field-value',\n `Changing a block on \"${change.field}\" needs \"blockId\" — block ids come from getDocument.`,\n )\n }\n if (!isNonEmptyString(change.prop)) {\n return issue(\n 'invalid-field-value',\n `Changing block \"${change.blockId}\" needs \"prop\" — the component property to change.`,\n )\n }\n const original = readFieldValue(doc, change.field, locale)\n const blocks = blockRootsOf(original)\n const index = blocks.findIndex((block) => block.id === change.blockId)\n if (index < 0) {\n return issue(\n 'invalid-field-value',\n `Block \"${change.blockId}\" was not found on \"${change.field}\" — read the document first; block ids come from getDocument.`,\n )\n }\n const block = blocks[index]!\n const props = { ...(block.props ?? {}) }\n if (change.value === undefined || change.value === null) delete props[change.prop]\n else props[change.prop] = staticProp(change.value as JsonValue)\n const nextBlock: ComponentNode = { ...block, props }\n // EC-576: through the core write path — the other roots and every typography\n // item keep their place, and the write is the v2 sequence. (`blocks` is\n // `editBlockRoots`' own root list, so `index` addresses the same root.)\n const value = editBlockRoots(original, (roots) => roots.map((b, i) => (i === index ? nextBlock : b)))\n // EC-595 A2: one block prop, by node id — the position this change wrote.\n const path = nodePropPath(change.field, locale, change.blockId, change.prop)\n const patch: DocumentPatch =\n change.value === undefined || change.value === null\n ? { op: 'unset', path }\n : { op: 'set', path, value: change.value }\n return { doc: writeField(collection, doc, change.field, locale, value), patch }\n}\n\n/** Apply ONE change to a (copy of the) document, or report the targeting issue. */\nfunction applyOneChange(\n input: BuildUpdateProposalInput,\n doc: CmsDocument,\n change: UpdateFieldChange,\n): ChangeOutcome {\n const { collection } = input\n const issue = (\n code: Parameters<typeof cmsIssue>[0]['code'],\n message: string,\n ): ChangeIssue => ({\n issue: cmsIssue({\n code,\n message,\n path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],\n collectionId: collection.id,\n documentId: doc.id,\n meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },\n }),\n })\n\n const field = collection.fields.find((f) => f.name === change.field)\n if (!field) {\n return issue(\n 'invalid-field-value',\n `Unknown field \"${change.field}\" on collection \"${collection.id}\" — use a field name from the schema.`,\n )\n }\n if (field.ai?.exclude) {\n return issue('invalid-field-value', `Field \"${change.field}\" must not be edited by the assistant.`)\n }\n\n const localized = isFieldLocalized(collection, change.field)\n if (!localized && change.locale !== undefined) {\n return issue('unknown-locale', `Field \"${change.field}\" is not localized — omit \"locale\".`)\n }\n if (\n change.locale !== undefined &&\n input.validateOptions?.localeConfig &&\n !input.validateOptions.localeConfig.locales.includes(change.locale)\n ) {\n return issue(\n 'unknown-locale',\n `Unknown locale \"${change.locale}\" — this project has: ${input.validateOptions?.localeConfig?.locales.join(', ')}.`,\n )\n }\n const locale = localized ? change.locale ?? input.locale : undefined\n if (localized && locale === undefined) {\n return issue('missing-localized-value', `Field \"${change.field}\" is localized — provide \"locale\".`)\n }\n\n if (change.blockId !== undefined || change.prop !== undefined) {\n return applyBlockPropChange(collection, doc, field, change, locale, issue)\n }\n\n const current = readFieldValue(doc, change.field, locale)\n const mapped = mapUpdateValue(field, change, input.document, current, input, issue)\n if ('issue' in mapped) return mapped\n const written = writeField(collection, doc, change.field, locale, mapped.value)\n return { doc: written, patch: fieldWritePatch(collection, written, change.field, locale) }\n}\n\n\n/**\n * Build the reviewable update proposal: apply every change over a copy of the\n * document (loud targeting), validate the merged result with the injected\n * leashes (only errors the change INTRODUCES block), and report the semantic\n * diff. `no-change` when the changes resolve to the current values.\n */\nexport function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult {\n const { registry, collection, document } = input\n\n let next: CmsDocument = structuredClone(document)\n const targetingIssues: CmsValidationIssue[] = []\n const patches: DocumentPatch[] = []\n for (const change of input.changes) {\n const outcome = applyOneChange(input, next, change)\n if ('issue' in outcome) targetingIssues.push(outcome.issue)\n else {\n next = outcome.doc\n patches.push(outcome.patch)\n }\n }\n if (targetingIssues.length > 0) return { ok: false, issues: targetingIssues }\n\n // EC-497: THE bag, whole — no per-call-site reassembly.\n const validateOptions: ValidateDocumentOptions = input.validateOptions ?? {}\n const before = validateDocument(registry, document, validateOptions)\n const issues = validateDocument(registry, next, validateOptions)\n const introduced = introducedErrors(before, issues)\n if (introduced.length > 0) return { ok: false, issues: introduced }\n\n const diff = diffDocuments(collection, document, next)\n if (diff.changes.length === 0) return { ok: true, kind: 'no-change' }\n\n return {\n ok: true,\n kind: 'proposal',\n patch: {\n collection: collection.id,\n id: document.id,\n values: next.values,\n ...(next.localized && Object.keys(next.localized).length > 0\n ? { localized: next.localized }\n : {}),\n },\n patches,\n before: {\n values: document.values,\n ...(document.localized ? { localized: document.localized } : {}),\n },\n changedFields: diff.changes.map((change) =>\n change.locale !== undefined ? `${change.field} (${change.locale})` : change.field,\n ),\n issues: warningsOf(issues),\n }\n}\n"]}
1
+ {"version":3,"file":"update-fields.js","sourceRoot":"","sources":["../../../src/lib/ai/update-fields.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow\n * counterpart of `applyContentWithShape`, minus the generation step).\n *\n * The MODEL authors the field changes directly in the `updateFields` tool input\n * (it has the conversation + `getDocument` output as context); this module turns\n * those changes into a reviewable proposal with the SAME engine discipline the\n * create flow has:\n *\n * - **merge semantics**: changes apply per field over the EXISTING document —\n * never a whole-document rewrite; the patch is the merged document, the diff\n * (`diffDocuments`) is what the editor reviews (changed fields only).\n * - **targeting is addressed with the engine's existing conventions** — field\n * name + optional locale bucket (the diff / `RestoreScope` shape) and, for a\n * component inside a composition, `blockId` + `prop`: the component's node id\n * at ANY depth the value-position traversal reaches (a top-level block, a child\n * in a slot, a component embedded in rich text — EC-593). No new path grammar.\n * - **one converter** (EC-593, closes EC-571): every value goes from its writer\n * form to its stored form through `writer-form.ts` — the same fold create,\n * translate and the read form use — so a nested markdown sub-field, a nested\n * asset swap and a block's asset prop are converted, carried and leashed exactly\n * as a top-level field is.\n * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND\n * asset ids (EC-381) must come from searched candidates (`searchDocuments` /\n * `listAssets`) or already be on the document — at every position; every\n * targeting failure is an explicit issue — never a silent drop (unlike the\n * create shape, which makes bad ids structurally unrepresentable, an update\n * change is imperative — so it FAILS instead of being filtered).\n * - **content, never structure** (ratified 4c): a composition's components are\n * never added, removed or moved by the assistant — a whole-value write of a\n * `blocks` field is refused with the way to change a prop instead, and a\n * rich-text value must keep the components embedded in it.\n * - **validation parity**: the merged document runs through `validateDocument`\n * with the studio's injected leashes. Only errors the change INTRODUCES block\n * the proposal — pre-existing brokenness on the document is not the\n * assistant's doing and must not force a whole-document repair (the editor's\n * own save path tolerates it the same way; errors gate publish, not drafts).\n *\n * Locale-awareness: a localized field's change lands in the change's `locale`,\n * defaulting to the editor's working locale (`input.locale`). Non-localized\n * fields reject a `locale` loudly. The value a change replaces is the DISPLAYED\n * value of that bucket (ratified 2c): a new locale bucket carries its placement\n * sub-fields — and a component prop change its whole composition — from the\n * fallback, exactly as the pickers do.\n *\n * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires\n * it to `getDocument` + the candidate store and hands the result to the review\n * sink.\n */\nimport {\n cmsIssue,\n isFieldLocalized,\n isSequenceValue,\n resolveField,\n validateDocument,\n writeField,\n} from '@elytracms/core/cms-core'\nimport { editBlockRoots } from '@elytracms/core/project-graph'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n DocumentPatch,\n FieldDef,\n FieldValues,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { introducedErrors, warningsOf } from './proposal-validation'\nimport { fieldWritePatch } from '../cms/field-patch'\nimport { fromWriterFieldValue, fromWriterFormAtNode, heldReferencesOf } from './writer-form'\nimport type { WriterFormContext, WriterFormProblem } from './writer-form'\nimport type { ContentPatch, WriterLeash } from './types'\n\n/**\n * One field change of an `updateFields` proposal, addressed with the engine's\n * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a\n * localized field), and for a component inside a composition optionally\n * `blockId` + `prop` to change one prop of that component — at any depth.\n * `value` is the new value (`null`/omitted clears the field or prop).\n */\nexport interface UpdateFieldChange {\n field: string\n /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */\n locale?: string\n /** A component's stable node id (from `getDocument`) — at any depth: slots, rich-text embeds. */\n blockId?: string\n /** With `blockId`: the component prop to change. */\n prop?: string\n /** The new value; `null` / omitted clears the field or prop. */\n value?: unknown\n}\n\nexport interface BuildUpdateProposalInput {\n registry: CollectionRegistry\n collection: CollectionDef\n /** The CURRENT document — the proposal's \"before\" side. Never mutated. */\n document: CmsDocument\n changes: readonly UpdateFieldChange[]\n /** The editor's working locale — the default bucket for localized fields. */\n locale?: string\n /**\n * THE id leash at every position (EC-381 → EC-593): what the model searched this\n * conversation. An id that is neither searched nor already on the document fails.\n */\n leash?: WriterLeash\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, whole. It also carries the project's\n * `localeConfig` — what the unknown-locale targeting check and the displayed-value\n * carry read — and the `lookupManifest` the converter reaches component props\n * through: one source for \"which locales exist\" and \"which props a component\n * declares\", not two that can disagree.\n */\n validateOptions?: ValidateDocumentOptions\n}\n\n/** The `before` snapshot the review diff renders against. */\nexport interface UpdateBeforeSnapshot {\n values: FieldValues\n localized?: Record<string, FieldValues>\n}\n\nexport type BuildUpdateProposalResult =\n | {\n ok: true\n kind: 'proposal'\n /** The MERGED document as a patch (full values; the diff shows changed fields only). */\n patch: ContentPatch\n /**\n * EC-595 A2 — the proposal as the positions it changes (a field's bucket, or one\n * component prop by node path): what an accepted proposal persists, so applying it\n * never overwrites a change made to any other position since it was built.\n */\n patches: DocumentPatch[]\n before: UpdateBeforeSnapshot\n /** Human-readable descriptors of the changed fields (e.g. `title (de)`). */\n changedFields: string[]\n /** Non-error issues on the accepted result (warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n /** Every change resolved to the current values — nothing to propose. */\n ok: true\n kind: 'no-change'\n }\n | { ok: false; issues: CmsValidationIssue[] }\n\ntype ChangeIssues = { issues: CmsValidationIssue[] }\ntype ChangeOutcome = { doc: CmsDocument; patch: DocumentPatch } | ChangeIssues\ntype IssueCode = Parameters<typeof cmsIssue>[0]['code']\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0\n}\n\n// The present/absent-key field write is core's `writeField` (EC-580) — this module\n// used to carry a local copy of the editor's `setFieldValue` (\"Local here (not\n// imported)\", MODE F5b.bkt). The locale is resolved per change below: a localized\n// field always arrives with one (a missing one is an explicit issue first).\n\n/**\n * The value a change REPLACES (ratified 2c, EC-593): the DISPLAYED value of the field in\n * the resolved bucket — core's `resolveField`, the rule every editor surface reads — so a\n * new locale bucket carries its placement sub-fields (and a component prop change its\n * composition) from the fallback, as the pickers do. It used to be the exact bucket only.\n * Without a locale config there is no fallback to display: the bucket itself.\n */\nfunction displayedFieldValue(\n input: BuildUpdateProposalInput,\n doc: CmsDocument,\n fieldName: string,\n locale: string | undefined,\n): unknown {\n if (locale === undefined) return doc.values[fieldName]\n const config = input.validateOptions?.localeConfig\n return config\n ? resolveField(input.collection, doc, fieldName, locale, config).value\n : doc.localized?.[locale]?.[fieldName]\n}\n\n/** Apply ONE change to a (copy of the) document, or report the targeting issues. */\nfunction applyOneChange(\n input: BuildUpdateProposalInput,\n ctx: WriterFormContext,\n doc: CmsDocument,\n change: UpdateFieldChange,\n): ChangeOutcome {\n const { collection } = input\n const issueAt = (code: IssueCode, message: string, at: readonly (string | number)[] = []): CmsValidationIssue =>\n cmsIssue({\n code,\n message,\n path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field, ...at],\n collectionId: collection.id,\n documentId: doc.id,\n meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },\n })\n const issue = (code: IssueCode, message: string): ChangeIssues => ({ issues: [issueAt(code, message)] })\n const fromProblems = (problems: readonly WriterFormProblem[]): ChangeIssues => ({\n issues: problems.map((problem) => issueAt(problem.code, problem.message, problem.at)),\n })\n\n const field = collection.fields.find((f) => f.name === change.field)\n if (!field) {\n return issue(\n 'invalid-field-value',\n `Unknown field \"${change.field}\" on collection \"${collection.id}\" — use a field name from the schema.`,\n )\n }\n if (field.ai?.exclude) {\n return issue('invalid-field-value', `Field \"${change.field}\" must not be edited by the assistant.`)\n }\n\n const localized = isFieldLocalized(collection, change.field)\n if (!localized && change.locale !== undefined) {\n return issue('unknown-locale', `Field \"${change.field}\" is not localized — omit \"locale\".`)\n }\n if (\n change.locale !== undefined &&\n input.validateOptions?.localeConfig &&\n !input.validateOptions.localeConfig.locales.includes(change.locale)\n ) {\n return issue(\n 'unknown-locale',\n `Unknown locale \"${change.locale}\" — this project has: ${input.validateOptions?.localeConfig?.locales.join(', ')}.`,\n )\n }\n const locale = localized ? change.locale ?? input.locale : undefined\n if (localized && locale === undefined) {\n return issue('missing-localized-value', `Field \"${change.field}\" is localized — provide \"locale\".`)\n }\n\n const displayed = displayedFieldValue(input, doc, change.field, locale)\n // The converter already wrote every value it changed in its stored form. The write\n // does not re-canonicalise component props (no manifest lookup): that would rewrite\n // the prop wrappers of every UNTOUCHED node too, and a proposal must change only what\n // it changes (the persister canonicalises on save, as for every editor write).\n const write = (value: unknown): { doc: CmsDocument; patch: DocumentPatch } => {\n const written = writeField(collection, doc, change.field, locale, value)\n return { doc: written, patch: fieldWritePatch(collection, written, change.field, locale) }\n }\n\n if (change.blockId !== undefined || change.prop !== undefined) {\n // A bucket that does not hold the field yet is written whole (from its displayed\n // fallback): a node path into it would address a node that is not there.\n const ownsBucket = locale === undefined || change.field in (doc.localized?.[locale] ?? {})\n return applyNodePropChange(field, change, displayed, locale, ownsBucket, ctx, issue, fromProblems, write)\n }\n\n // EC-593: the ONE writer-form converter, at every depth (EC-571: this path used to\n // convert top-level rich text only and store a nested markdown string raw).\n const converted = fromWriterFieldValue(field, change.value, displayed, ctx, locale)\n if (converted.problems.length > 0) return fromProblems(converted.problems)\n return write(converted.value)\n}\n\n/**\n * Change one component prop, addressed by node id at any depth (EC-593: it used to be a\n * ROOT block of a `blocks` field only, written verbatim — no carry, no leash, no\n * markdown conversion, and the persisted patch carried the RAW model value).\n */\nfunction applyNodePropChange(\n field: FieldDef,\n change: UpdateFieldChange,\n displayed: unknown,\n locale: string | undefined,\n ownsBucket: boolean,\n ctx: WriterFormContext,\n issue: (code: IssueCode, message: string) => ChangeIssues,\n fromProblems: (problems: readonly WriterFormProblem[]) => ChangeIssues,\n write: (value: unknown) => { doc: CmsDocument; patch: DocumentPatch },\n): ChangeOutcome {\n if (!isNonEmptyString(change.blockId)) {\n return issue(\n 'invalid-field-value',\n `Changing a component on \"${change.field}\" needs \"blockId\" — component ids come from getDocument.`,\n )\n }\n if (!isNonEmptyString(change.prop)) {\n return issue(\n 'invalid-field-value',\n `Changing component \"${change.blockId}\" needs \"prop\" — the component property to change.`,\n )\n }\n // EC-576 tolerant read (reseed gate, EC-576 step 4 deletes it with the other legacy\n // blocks readers): a pre-0.0.84 bare node / node list is lifted into the v2 sequence\n // the traversal reads — and the write is that sequence (no writer re-emits the bare shape).\n const composition =\n field.type === 'blocks' && displayed !== undefined && !isSequenceValue(displayed)\n ? editBlockRoots(displayed, (roots) => roots)\n : displayed\n const result = fromWriterFormAtNode(field, composition, change.blockId, change.prop, change.value, ctx, locale)\n if (!result.ok) return issue('invalid-field-value', result.message)\n if (result.problems.length > 0) return fromProblems(result.problems)\n const written = write(result.value)\n // EC-595 A2: the position this change wrote — the prop by its node path, or (where the\n // grammar does not reach the node, the bucket is new, or a legacy value was lifted)\n // the field bucket, whole: a coarser path, never a wrong one.\n if (!result.exact || !ownsBucket || composition !== displayed) return written\n const patch: DocumentPatch =\n result.propValue === undefined\n ? { op: 'unset', path: result.path }\n : { op: 'set', path: result.path, value: result.propValue }\n return { doc: written.doc, patch }\n}\n\n/**\n * Build the reviewable update proposal: apply every change over a copy of the\n * document (loud targeting), validate the merged result with the injected\n * leashes (only errors the change INTRODUCES block), and report the semantic\n * diff. `no-change` when the changes resolve to the current values.\n */\nexport function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult {\n const { registry, collection, document } = input\n\n let next: CmsDocument = structuredClone(document)\n const targetingIssues: CmsValidationIssue[] = []\n const patches: DocumentPatch[] = []\n // EC-593: one converter context for every change — the validator's own prop lookup,\n // the conversation's leash, and the ids the ORIGINAL document already references.\n const ctx: WriterFormContext = {\n ...(input.validateOptions?.lookupManifest ? { lookupManifest: input.validateOptions.lookupManifest } : {}),\n ...(input.leash ? { leash: input.leash } : {}),\n held: heldReferencesOf(document),\n }\n for (const change of input.changes) {\n const outcome = applyOneChange(input, ctx, next, change)\n if ('issues' in outcome) targetingIssues.push(...outcome.issues)\n else {\n next = outcome.doc\n patches.push(outcome.patch)\n }\n }\n if (targetingIssues.length > 0) return { ok: false, issues: targetingIssues }\n\n // EC-497: THE bag, whole — no per-call-site reassembly.\n const validateOptions: ValidateDocumentOptions = input.validateOptions ?? {}\n const before = validateDocument(registry, document, validateOptions)\n const issues = validateDocument(registry, next, validateOptions)\n const introduced = introducedErrors(before, issues)\n if (introduced.length > 0) return { ok: false, issues: introduced }\n\n const diff = diffDocuments(collection, document, next)\n if (diff.changes.length === 0) return { ok: true, kind: 'no-change' }\n\n return {\n ok: true,\n kind: 'proposal',\n patch: {\n collection: collection.id,\n id: document.id,\n values: next.values,\n ...(next.localized && Object.keys(next.localized).length > 0\n ? { localized: next.localized }\n : {}),\n },\n patches,\n before: {\n values: document.values,\n ...(document.localized ? { localized: document.localized } : {}),\n },\n changedFields: diff.changes.map((change) =>\n change.locale !== undefined ? `${change.field} (${change.locale})` : change.field,\n ),\n issues: warningsOf(issues),\n }\n}\n"]}