@elytracms/studio 0.0.57 → 0.0.59

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 (109) hide show
  1. package/dist/components/ai/ai-chat-panel.js +44 -6
  2. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  3. package/dist/components/ai/chat-ui.d.ts +1 -1
  4. package/dist/components/ai/chat-ui.js +11 -2
  5. package/dist/components/ai/chat-ui.js.map +1 -1
  6. package/dist/components/ai/proposal-card.d.ts +30 -7
  7. package/dist/components/ai/proposal-card.js +81 -21
  8. package/dist/components/ai/proposal-card.js.map +1 -1
  9. package/dist/components/builder/blocks-model.js +12 -7
  10. package/dist/components/builder/blocks-model.js.map +1 -1
  11. package/dist/components/builder/canvas-dnd.d.ts +59 -0
  12. package/dist/components/builder/canvas-dnd.js +207 -0
  13. package/dist/components/builder/canvas-dnd.js.map +1 -0
  14. package/dist/components/builder/canvas-dom.d.ts +11 -0
  15. package/dist/components/builder/canvas-dom.js +17 -0
  16. package/dist/components/builder/canvas-dom.js.map +1 -0
  17. package/dist/components/builder/canvas-inline-editing.d.ts +91 -0
  18. package/dist/components/builder/canvas-inline-editing.js +242 -0
  19. package/dist/components/builder/canvas-inline-editing.js.map +1 -0
  20. package/dist/components/builder/canvas-render-context.d.ts +83 -0
  21. package/dist/components/builder/canvas-render-context.js +196 -0
  22. package/dist/components/builder/canvas-render-context.js.map +1 -0
  23. package/dist/components/builder/canvas.d.ts +4 -1
  24. package/dist/components/builder/canvas.js +32 -567
  25. package/dist/components/builder/canvas.js.map +1 -1
  26. package/dist/components/builder/composition-field-editor.js +14 -4
  27. package/dist/components/builder/composition-field-editor.js.map +1 -1
  28. package/dist/components/builder/insert-context.d.ts +5 -2
  29. package/dist/components/builder/insert-context.js +1 -3
  30. package/dist/components/builder/insert-context.js.map +1 -1
  31. package/dist/components/builder/node-factory.d.ts +12 -0
  32. package/dist/components/builder/node-factory.js +18 -1
  33. package/dist/components/builder/node-factory.js.map +1 -1
  34. package/dist/components/cms/asset-field.d.ts +22 -0
  35. package/dist/components/cms/asset-field.js +300 -0
  36. package/dist/components/cms/asset-field.js.map +1 -0
  37. package/dist/components/cms/detail-editor.js +12 -0
  38. package/dist/components/cms/detail-editor.js.map +1 -1
  39. package/dist/components/cms/document-diff-view.d.ts +12 -1
  40. package/dist/components/cms/document-diff-view.js +119 -42
  41. package/dist/components/cms/document-diff-view.js.map +1 -1
  42. package/dist/components/cms/field-control-lib.d.ts +43 -0
  43. package/dist/components/cms/field-control-lib.js +98 -0
  44. package/dist/components/cms/field-control-lib.js.map +1 -0
  45. package/dist/components/cms/field-control.d.ts +7 -35
  46. package/dist/components/cms/field-control.js +13 -556
  47. package/dist/components/cms/field-control.js.map +1 -1
  48. package/dist/components/cms/object-array-field.d.ts +29 -0
  49. package/dist/components/cms/object-array-field.js +205 -0
  50. package/dist/components/cms/object-array-field.js.map +1 -0
  51. package/dist/components/cms/rich-text-preview.d.ts +15 -0
  52. package/dist/components/cms/rich-text-preview.js +81 -0
  53. package/dist/components/cms/rich-text-preview.js.map +1 -0
  54. package/dist/components/cms/version-compare-text.d.ts +26 -0
  55. package/dist/components/cms/version-compare-text.js +60 -0
  56. package/dist/components/cms/version-compare-text.js.map +1 -1
  57. package/dist/components/project-session.d.ts +19 -0
  58. package/dist/components/project-session.js +29 -2
  59. package/dist/components/project-session.js.map +1 -1
  60. package/dist/components/use-hydration-phase.d.ts +6 -4
  61. package/dist/components/use-hydration-phase.js.map +1 -1
  62. package/dist/lib/ai/apply-content.js +52 -10
  63. package/dist/lib/ai/apply-content.js.map +1 -1
  64. package/dist/lib/ai/chat-context.d.ts +19 -3
  65. package/dist/lib/ai/chat-context.js +33 -4
  66. package/dist/lib/ai/chat-context.js.map +1 -1
  67. package/dist/lib/ai/chat-state.d.ts +6 -3
  68. package/dist/lib/ai/chat-state.js.map +1 -1
  69. package/dist/lib/ai/fake-provider.d.ts +60 -5
  70. package/dist/lib/ai/fake-provider.js +74 -6
  71. package/dist/lib/ai/fake-provider.js.map +1 -1
  72. package/dist/lib/ai/index.d.ts +2 -0
  73. package/dist/lib/ai/index.js +2 -0
  74. package/dist/lib/ai/index.js.map +1 -1
  75. package/dist/lib/ai/rich-text-input.d.ts +27 -0
  76. package/dist/lib/ai/rich-text-input.js +25 -0
  77. package/dist/lib/ai/rich-text-input.js.map +1 -0
  78. package/dist/lib/ai/shape.d.ts +12 -1
  79. package/dist/lib/ai/shape.js +46 -20
  80. package/dist/lib/ai/shape.js.map +1 -1
  81. package/dist/lib/ai/tools.d.ts +88 -13
  82. package/dist/lib/ai/tools.js +253 -14
  83. package/dist/lib/ai/tools.js.map +1 -1
  84. package/dist/lib/ai/types.d.ts +20 -0
  85. package/dist/lib/ai/types.js.map +1 -1
  86. package/dist/lib/ai/update-fields.d.ts +68 -0
  87. package/dist/lib/ai/update-fields.js +296 -0
  88. package/dist/lib/ai/update-fields.js.map +1 -0
  89. package/dist/lib/ai-current-document.d.ts +53 -0
  90. package/dist/lib/ai-current-document.js +54 -0
  91. package/dist/lib/ai-current-document.js.map +1 -0
  92. package/dist/lib/cms-views/view-model.d.ts +6 -6
  93. package/dist/lib/data/collection-content.d.ts +12 -7
  94. package/dist/lib/data/collection-content.js +6 -4
  95. package/dist/lib/data/collection-content.js.map +1 -1
  96. package/dist/lib/i18n/messages.en.js +10 -1
  97. package/dist/lib/i18n/messages.en.js.map +1 -1
  98. package/dist/lib/operations/session-hydration.d.ts +4 -1
  99. package/dist/lib/operations/session-hydration.js +4 -1
  100. package/dist/lib/operations/session-hydration.js.map +1 -1
  101. package/dist/lib/persistence/index.js +13 -1
  102. package/dist/lib/persistence/index.js.map +1 -1
  103. package/dist/lib/workspace/index.d.ts +1 -1
  104. package/dist/lib/workspace/index.js +1 -1
  105. package/dist/lib/workspace/index.js.map +1 -1
  106. package/dist/lib/workspace/workspace-source.d.ts +17 -22
  107. package/dist/lib/workspace/workspace-source.js +1 -49
  108. package/dist/lib/workspace/workspace-source.js.map +1 -1
  109. package/package.json +2 -2
@@ -68,24 +68,79 @@ export interface FakeCreateFlowChatModelOptions {
68
68
  modelId?: string;
69
69
  /** The collection the first stage searches (a relation target; default `author`). */
70
70
  searchCollection?: string;
71
+ /**
72
+ * EC-381: when set, the create flow inserts a `listAssets` stage after the
73
+ * relation search and before `createDocument`, so a collection with an image
74
+ * field gets it filled from a REAL library asset (the enum leash). Optional
75
+ * search text passed to `listAssets` (omit to list all).
76
+ */
77
+ searchAssets?: boolean;
78
+ /** Optional query passed to the scripted `listAssets` stage (EC-381). */
79
+ assetQuery?: string;
71
80
  /** The collection the terminal stage creates in (default `post`). */
72
81
  createCollection?: string;
73
82
  /** The final answer streamed word-by-word after the proposal lands. */
74
83
  answerText?: string;
84
+ /**
85
+ * EC-378: the field the scripted UPDATE flow changes (default `title`). A user
86
+ * turn matching {@link FakeCreateFlowChatModelOptions.updatePattern} routes to
87
+ * the update flow instead of the create flow.
88
+ */
89
+ updateField?: string;
90
+ /** Routes a user turn to the update flow (default {@link FAKE_UPDATE_PATTERN}). */
91
+ updatePattern?: RegExp;
92
+ /** The final answer of the scripted update flow. */
93
+ updateAnswerText?: string;
75
94
  }
76
95
  /** Default final answer of the scripted create flow. */
77
96
  export declare const FAKE_CREATE_FLOW_ANSWER = "Done \u2014 review the proposal below and apply it as a draft.";
97
+ /** Default final answer of the scripted update flow (EC-378). */
98
+ export declare const FAKE_UPDATE_FLOW_ANSWER = "Done \u2014 review the proposed change below and apply it as a draft.";
99
+ /** Default edit-request detector of the scripted assistant (EC-378). */
100
+ export declare const FAKE_UPDATE_PATTERN: RegExp;
101
+ /**
102
+ * The scripted update flow's new value for the target field: the text after the
103
+ * FIRST colon when present (`Set the title to: New Title` → `New Title`), else
104
+ * the whole user text — deterministic either way. First (not last) colon so a
105
+ * markdown body value may itself contain colons (EC-379).
106
+ */
107
+ export declare function fakeUpdateValueFrom(userText: string): string;
78
108
  /**
79
- * A scripted, STATELESS fake chat model that drives the FULL create flow over the
80
- * native loop (EC-334): keyless playground / CI exercise user message → search
81
- * tool → `createDocument` terminal tool (the client runs `applyContentWithShape`)
82
- * → streamed final answer. Stage is read from the CURRENT user turn's tool
109
+ * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the
110
+ * <field> to: …` names it (so the playground can drive a markdown-authored
111
+ * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything
112
+ * else falls back to the configured default field.
113
+ */
114
+ export declare function fakeUpdateFieldFrom(userText: string, fallback: string): string;
115
+ /**
116
+ * A scripted, STATELESS fake chat model that drives the FULL create flow — and,
117
+ * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the
118
+ * native loop (EC-334): keyless playground / CI exercise the whole client-tool
119
+ * round-trip deterministically. Stage is read from the CURRENT user turn's tool
83
120
  * results (prompt suffix after the last user message), so every follow-up turn
84
- * starts a fresh flow — multi-turn works without model state:
121
+ * starts a fresh flow — multi-turn works without model state.
85
122
  *
123
+ * Create flow (default):
86
124
  * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
87
125
  * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`
88
126
  * 3. create result present → the answer text, word by word.
127
+ *
128
+ * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:
129
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
130
+ * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`
131
+ * 3. listAssets done, no create → `createDocument({ collection, instruction })`
132
+ * 4. create result present → the answer text, word by word.
133
+ *
134
+ * Update flow (user text matches `updatePattern`, e.g. "Change the title to: X"):
135
+ * 1. no tool result yet → `getDocument({})` — target omitted on purpose:
136
+ * the executor resolves the document OPEN in the
137
+ * editor (the EC-378 current-document seam)
138
+ * 2. document read, no update → `updateFields({ changes: [{ field:
139
+ * fakeUpdateFieldFrom(<text>, updateField),
140
+ * value: fakeUpdateValueFrom(<last user text>) }] })`
141
+ * — "Set the body to: ## Neu" drives a markdown
142
+ * rich-text update deterministically (EC-379)
143
+ * 3. update result present → the update answer, word by word.
89
144
  */
90
145
  export declare function createFakeCreateFlowChatModel(options?: FakeCreateFlowChatModelOptions): FakeChatModel;
91
146
  export interface FakeProviderOptions {
@@ -158,30 +158,98 @@ export function createFakeChatModel(options = {}) {
158
158
  }
159
159
  /** Default final answer of the scripted create flow. */
160
160
  export const FAKE_CREATE_FLOW_ANSWER = 'Done — review the proposal below and apply it as a draft.';
161
+ /** Default final answer of the scripted update flow (EC-378). */
162
+ export const FAKE_UPDATE_FLOW_ANSWER = 'Done — review the proposed change below and apply it as a draft.';
163
+ /** Default edit-request detector of the scripted assistant (EC-378). */
164
+ export const FAKE_UPDATE_PATTERN = /\b(change|update|edit|remove|replace|rewrite|fix|set)\b/i;
161
165
  /**
162
- * A scripted, STATELESS fake chat model that drives the FULL create flow over the
163
- * native loop (EC-334): keyless playground / CI exercise user message → search
164
- * tool → `createDocument` terminal tool (the client runs `applyContentWithShape`)
165
- * → streamed final answer. Stage is read from the CURRENT user turn's tool
166
+ * The scripted update flow's new value for the target field: the text after the
167
+ * FIRST colon when present (`Set the title to: New Title` → `New Title`), else
168
+ * the whole user text — deterministic either way. First (not last) colon so a
169
+ * markdown body value may itself contain colons (EC-379).
170
+ */
171
+ export function fakeUpdateValueFrom(userText) {
172
+ const colon = userText.indexOf(':');
173
+ const value = colon >= 0 ? userText.slice(colon + 1) : userText;
174
+ return value.trim() || 'Updated by the assistant';
175
+ }
176
+ /**
177
+ * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the
178
+ * <field> to: …` names it (so the playground can drive a markdown-authored
179
+ * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything
180
+ * else falls back to the configured default field.
181
+ */
182
+ export function fakeUpdateFieldFrom(userText, fallback) {
183
+ const match = /\b(?:set|change|update)\s+the\s+([A-Za-z]\w*)\s+to\s*:/i.exec(userText);
184
+ return match?.[1] ?? fallback;
185
+ }
186
+ /**
187
+ * A scripted, STATELESS fake chat model that drives the FULL create flow — and,
188
+ * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the
189
+ * native loop (EC-334): keyless playground / CI exercise the whole client-tool
190
+ * round-trip deterministically. Stage is read from the CURRENT user turn's tool
166
191
  * results (prompt suffix after the last user message), so every follow-up turn
167
- * starts a fresh flow — multi-turn works without model state:
192
+ * starts a fresh flow — multi-turn works without model state.
168
193
  *
194
+ * Create flow (default):
169
195
  * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
170
196
  * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`
171
197
  * 3. create result present → the answer text, word by word.
198
+ *
199
+ * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:
200
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
201
+ * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`
202
+ * 3. listAssets done, no create → `createDocument({ collection, instruction })`
203
+ * 4. create result present → the answer text, word by word.
204
+ *
205
+ * Update flow (user text matches `updatePattern`, e.g. "Change the title to: X"):
206
+ * 1. no tool result yet → `getDocument({})` — target omitted on purpose:
207
+ * the executor resolves the document OPEN in the
208
+ * editor (the EC-378 current-document seam)
209
+ * 2. document read, no update → `updateFields({ changes: [{ field:
210
+ * fakeUpdateFieldFrom(<text>, updateField),
211
+ * value: fakeUpdateValueFrom(<last user text>) }] })`
212
+ * — "Set the body to: ## Neu" drives a markdown
213
+ * rich-text update deterministically (EC-379)
214
+ * 3. update result present → the update answer, word by word.
172
215
  */
173
216
  export function createFakeCreateFlowChatModel(options = {}) {
174
217
  const searchCollection = options.searchCollection ?? 'author';
175
218
  const createCollection = options.createCollection ?? 'post';
219
+ const searchAssets = options.searchAssets ?? false;
176
220
  const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER;
221
+ const updateField = options.updateField ?? 'title';
222
+ const updatePattern = options.updatePattern ?? FAKE_UPDATE_PATTERN;
223
+ const updateAnswer = options.updateAnswerText ?? FAKE_UPDATE_FLOW_ANSWER;
177
224
  return createFakeChatModel({
178
225
  modelId: options.modelId ?? 'fake-create-flow-model',
179
226
  script: (callOptions) => {
180
227
  const done = toolResultNamesInCurrentTurn(callOptions.prompt);
228
+ const userText = lastUserText(callOptions.prompt);
229
+ if (updatePattern.test(userText)) {
230
+ if (done.has('updateFields'))
231
+ return answerTurnParts(updateAnswer);
232
+ if (done.has('getDocument')) {
233
+ return toolTurnParts('Proposing the change. ', 'updateFields', {
234
+ changes: [
235
+ {
236
+ field: fakeUpdateFieldFrom(userText, updateField),
237
+ value: fakeUpdateValueFrom(userText),
238
+ },
239
+ ],
240
+ }, 'call-update-1');
241
+ }
242
+ return toolTurnParts('Reading the document. ', 'getDocument', {}, 'call-get-1');
243
+ }
181
244
  if (done.has('createDocument'))
182
245
  return answerTurnParts(answer);
183
246
  if (done.has('searchDocuments')) {
184
- const instruction = lastUserText(callOptions.prompt) || 'Write a short document.';
247
+ // EC-381 — search the media library before creating (a topically fitting,
248
+ // REAL asset fills the image field via the enum leash).
249
+ if (searchAssets && !done.has('listAssets')) {
250
+ return toolTurnParts('Looking for a fitting image. ', 'listAssets', { ...(options.assetQuery ? { query: options.assetQuery } : {}) }, 'call-assets-1');
251
+ }
252
+ const instruction = userText || 'Write a short document.';
185
253
  return toolTurnParts('Drafting a proposal. ', 'createDocument', { collection: createCollection, instruction }, 'call-create-1');
186
254
  }
187
255
  return toolTurnParts(FAKE_CHAT_PREAMBLE, 'searchDocuments', { collection: searchCollection }, 'call-search-1');
@@ -1 +1 @@
1
- {"version":3,"file":"fake-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/fake-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * FakeProvider (EC-331): a deterministic, fixture-driven {@link AiProvider} that\n * implements the same interface as the Anthropic provider, so playground / demo /\n * tests run the WHOLE loop (config gate → shape → generate → validate → repair →\n * patch) without a key. Determinism is a project principle: the same fixtures always\n * yield the same result. Imports no AI SDK at runtime — the `LanguageModelV4` spec\n * below is implemented STRUCTURALLY from type-only imports (erased at compile;\n * pinned in `bundle.test.ts`).\n *\n * For the repair loop, pass `objects: [invalidDoc, validDoc]` — the provider returns\n * them in call order (the last is reused once the script is exhausted). Or pass\n * `respond` to compute the object from the request.\n *\n * EC-334 adds {@link createFakeChatModel}: a scripted fake `LanguageModel` served by\n * the ROUTE's native chat branch, so the keyless playground / CI drive the FULL\n * `streamText` → SSE wire → client-tool loop. It is a STATELESS prompt-inspecting\n * responder (state read from the prompt, the FakeProvider trick): a prompt without a\n * tool result emits a tool call; a prompt carrying one streams a text answer.\n */\n// Type-only (erased): the fake implements the SDK's v4 model spec structurally.\nimport type { LanguageModel } from 'ai'\nimport type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest } from './types'\n\n// --- Fake chat model (EC-334) -------------------------------------------------------\n\n/** The SDK's v4 language-model spec, extracted structurally from the type-only union. */\nexport type FakeLanguageModelV4 = Extract<LanguageModel, { specificationVersion: 'v4' }>\n\n/** The call options a fake chat model receives (`LanguageModelV4CallOptions`). */\nexport type FakeChatModelCallOptions = Parameters<FakeLanguageModelV4['doGenerate']>[0]\n\n/** One provider-spec stream part the fake emits (`LanguageModelV4StreamPart`). */\nexport type FakeChatModelStreamPart =\n Awaited<ReturnType<FakeLanguageModelV4['doStream']>>['stream'] extends ReadableStream<infer PART>\n ? PART\n : never\n\ntype FakeFinishPart = Extract<FakeChatModelStreamPart, { type: 'finish' }>\ntype FakeGenerateResult = Awaited<ReturnType<FakeLanguageModelV4['doGenerate']>>\n\n/** Zero-usage report (the fake spends nothing). Shape = `LanguageModelV4Usage`. */\nconst FAKE_USAGE: FakeFinishPart['usage'] = {\n inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },\n outputTokens: { total: 0, text: 0, reasoning: 0 },\n}\n\n/** Default preamble streamed before the scripted tool call. */\nexport const FAKE_CHAT_PREAMBLE = 'Let me look that up. '\n/** Default final answer streamed once the prompt carries a tool result. */\nexport const FAKE_CHAT_ANSWER = 'Found it — the tool result is in, and this is the answer.'\n\nexport interface FakeChatModelOptions {\n /** Model id reported to the SDK (default `fake-chat-model`). */\n modelId?: string\n /** Text streamed before the tool call on the first (tool-result-free) turn. */\n preambleText?: string\n /** The tool the first turn requests (default `searchDocuments`). */\n toolName?: string\n /** The tool-call input (default `{ collection: 'post' }`). */\n toolInput?: Record<string, unknown>\n /** The answer streamed word-by-word once the prompt carries a tool result. */\n answerText?: string\n /**\n * Full override: script the exact provider-spec stream parts for a call\n * (zero-based call index). The default prompt-inspecting behavior is skipped.\n */\n script?: (options: FakeChatModelCallOptions, callIndex: number) => FakeChatModelStreamPart[]\n}\n\n/** A fake chat model that also records the calls it received, for assertions. */\nexport type FakeChatModel = FakeLanguageModelV4 & {\n /** Every `doStream` / `doGenerate` call options received, in order. */\n readonly calls: FakeChatModelCallOptions[]\n}\n\n/** Does any prompt message carry a tool RESULT part? (The fake's only \"state\".) */\nfunction promptHasToolResult(prompt: FakeChatModelCallOptions['prompt']): boolean {\n return prompt.some(\n (message) =>\n Array.isArray(message.content) &&\n message.content.some(\n (part) => (part as { type?: unknown }).type === 'tool-result',\n ),\n )\n}\n\n/** Index of the LAST user message in the prompt (`-1` when none). */\nfunction lastUserIndex(prompt: FakeChatModelCallOptions['prompt']): number {\n for (let i = prompt.length - 1; i >= 0; i -= 1) {\n if (prompt[i]?.role === 'user') return i\n }\n return -1\n}\n\n/** The last user message's text (the fake's \"instruction\" source). */\nfunction lastUserText(prompt: FakeChatModelCallOptions['prompt']): string {\n const index = lastUserIndex(prompt)\n const message = prompt[index]\n if (!message || !Array.isArray(message.content)) return ''\n return message.content\n .filter((part): part is { type: 'text'; text: string } => (part as { type?: unknown }).type === 'text')\n .map((part) => part.text)\n .join(' ')\n .trim()\n}\n\n/**\n * Tool names with a RESULT in the CURRENT user turn (prompt suffix after the\n * last user message) — the stateless stage detector for scripted flows.\n */\nfunction toolResultNamesInCurrentTurn(prompt: FakeChatModelCallOptions['prompt']): Set<string> {\n const names = new Set<string>()\n for (const message of prompt.slice(lastUserIndex(prompt) + 1)) {\n if (!Array.isArray(message.content)) continue\n for (const part of message.content) {\n const candidate = part as { type?: unknown; toolName?: unknown }\n if (candidate.type === 'tool-result' && typeof candidate.toolName === 'string') {\n names.add(candidate.toolName)\n }\n }\n }\n return names\n}\n\n/** Provider-spec parts for one text-preamble + tool-call turn (finish `tool-calls`). */\nfunction toolTurnParts(\n preamble: string,\n toolName: string,\n input: Record<string, unknown>,\n callId: string,\n): FakeChatModelStreamPart[] {\n const encoded = JSON.stringify(input)\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n { type: 'text-delta', id: 'text-1', delta: preamble },\n { type: 'text-end', id: 'text-1' },\n { type: 'tool-input-start', id: callId, toolName },\n { type: 'tool-input-delta', id: callId, delta: encoded },\n { type: 'tool-input-end', id: callId },\n { type: 'tool-call', toolCallId: callId, toolName, input: encoded },\n {\n type: 'finish',\n usage: FAKE_USAGE,\n finishReason: { unified: 'tool-calls', raw: 'fake-tool-calls' },\n },\n ]\n}\n\n/** Provider-spec parts for a word-by-word streamed text answer (finish `stop`). */\nfunction answerTurnParts(answer: string): FakeChatModelStreamPart[] {\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n ...wordDeltas(answer).map(\n (delta): FakeChatModelStreamPart => ({ type: 'text-delta', id: 'text-1', delta }),\n ),\n { type: 'text-end', id: 'text-1' },\n { type: 'finish', usage: FAKE_USAGE, finishReason: { unified: 'stop', raw: 'fake-stop' } },\n ]\n}\n\n/** Split text into word deltas (trailing spaces preserved) for visible streaming. */\nfunction wordDeltas(text: string): string[] {\n const words = text.split(' ')\n return words.map((word, index) => (index < words.length - 1 ? `${word} ` : word))\n}\n\n/**\n * Build a scripted, stateless fake `LanguageModel` (the `LanguageModelV4` spec,\n * implemented structurally — zero SDK runtime import). Default behavior:\n * - prompt WITHOUT a tool result → preamble text, then a `tool-call` for\n * {@link FakeChatModelOptions.toolName} (finish reason `tool-calls`);\n * - prompt WITH a tool result → the answer text streamed word-by-word (`stop`).\n * Chunk shapes follow the v4 spec: `finishReason` is `{ unified, raw }`, tool chunks\n * are `tool-input-start/delta/end` + `tool-call`.\n */\nexport function createFakeChatModel(options: FakeChatModelOptions = {}): FakeChatModel {\n const preamble = options.preambleText ?? FAKE_CHAT_PREAMBLE\n const toolName = options.toolName ?? 'searchDocuments'\n const toolInput = options.toolInput ?? { collection: 'post' }\n const answer = options.answerText ?? FAKE_CHAT_ANSWER\n const calls: FakeChatModelCallOptions[] = []\n let callIndex = 0\n\n function partsFor(callOptions: FakeChatModelCallOptions): FakeChatModelStreamPart[] {\n if (options.script) return options.script(callOptions, callIndex)\n if (!promptHasToolResult(callOptions.prompt)) {\n return toolTurnParts(preamble, toolName, toolInput, 'call-1')\n }\n return answerTurnParts(answer)\n }\n\n /** Fold the scripted stream parts into a non-streaming `doGenerate` result. */\n function materialize(parts: FakeChatModelStreamPart[]): FakeGenerateResult {\n const content: FakeGenerateResult['content'] = []\n const textById = new Map<string, string>()\n let finishReason: FakeFinishPart['finishReason'] = { unified: 'stop', raw: 'fake-stop' }\n for (const part of parts) {\n if (part.type === 'text-delta') {\n textById.set(part.id, (textById.get(part.id) ?? '') + part.delta)\n } else if (part.type === 'tool-call') {\n content.push(part)\n } else if (part.type === 'finish') {\n finishReason = part.finishReason\n }\n }\n for (const text of textById.values()) content.unshift({ type: 'text', text })\n return { content, finishReason, usage: FAKE_USAGE, warnings: [] }\n }\n\n return {\n specificationVersion: 'v4',\n provider: 'fake',\n modelId: options.modelId ?? 'fake-chat-model',\n supportedUrls: {},\n calls,\n doGenerate(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve(materialize(parts))\n },\n doStream(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve({\n stream: new ReadableStream<FakeChatModelStreamPart>({\n start(controller) {\n for (const part of parts) controller.enqueue(part)\n controller.close()\n },\n }),\n })\n },\n }\n}\n\n// --- Fake create-flow chat model (EC-334 slice 2) -------------------------------------\n\nexport interface FakeCreateFlowChatModelOptions {\n /** Model id reported to the SDK (default `fake-create-flow-model`). */\n modelId?: string\n /** The collection the first stage searches (a relation target; default `author`). */\n searchCollection?: string\n /** The collection the terminal stage creates in (default `post`). */\n createCollection?: string\n /** The final answer streamed word-by-word after the proposal lands. */\n answerText?: string\n}\n\n/** Default final answer of the scripted create flow. */\nexport const FAKE_CREATE_FLOW_ANSWER =\n 'Done — review the proposal below and apply it as a draft.'\n\n/**\n * A scripted, STATELESS fake chat model that drives the FULL create flow over the\n * native loop (EC-334): keyless playground / CI exercise user message → search\n * tool → `createDocument` terminal tool (the client runs `applyContentWithShape`)\n * → streamed final answer. Stage is read from the CURRENT user turn's tool\n * results (prompt suffix after the last user message), so every follow-up turn\n * starts a fresh flow — multi-turn works without model state:\n *\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`\n * 3. create result present → the answer text, word by word.\n */\nexport function createFakeCreateFlowChatModel(\n options: FakeCreateFlowChatModelOptions = {},\n): FakeChatModel {\n const searchCollection = options.searchCollection ?? 'author'\n const createCollection = options.createCollection ?? 'post'\n const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER\n return createFakeChatModel({\n modelId: options.modelId ?? 'fake-create-flow-model',\n script: (callOptions) => {\n const done = toolResultNamesInCurrentTurn(callOptions.prompt)\n if (done.has('createDocument')) return answerTurnParts(answer)\n if (done.has('searchDocuments')) {\n const instruction = lastUserText(callOptions.prompt) || 'Write a short document.'\n return toolTurnParts(\n 'Drafting a proposal. ',\n 'createDocument',\n { collection: createCollection, instruction },\n 'call-create-1',\n )\n }\n return toolTurnParts(\n FAKE_CHAT_PREAMBLE,\n 'searchDocuments',\n { collection: searchCollection },\n 'call-search-1',\n )\n },\n })\n}\n\n// --- Fake provider ------------------------------------------------------------------\n\nexport interface FakeProviderOptions {\n /** Provider id (default `fake`). */\n id?: string\n /** Model id recorded in provenance (default `fake-model`). */\n model?: string\n /** Reported capabilities (default: both true). Set to test the capability gate. */\n capabilities?: AiProviderCapabilities\n /** Scripted objects returned in call order; the last entry is reused when exhausted. */\n objects?: unknown[]\n /** Compute the object from the request + zero-based call index (overrides `objects`). */\n respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown\n /** The scripted chat model `languageModel()` serves (default {@link createFakeChatModel}). */\n chatModel?: FakeChatModel\n}\n\n/** A FakeProvider that also records the requests it received, for assertions. */\nexport interface FakeProvider extends AiProvider {\n /** Every `generateStructured` request received, in order. */\n readonly calls: GenerateStructuredRequest[]\n /** The scripted fake chat model behind `languageModel()` (records its calls). */\n readonly chatModel: FakeChatModel\n}\n\nexport function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {\n const calls: GenerateStructuredRequest[] = []\n const objects = options.objects ?? []\n const chatModel = options.chatModel ?? createFakeChatModel()\n let index = 0\n\n return {\n id: options.id ?? 'fake',\n model: options.model ?? 'fake-model',\n calls,\n chatModel,\n capabilities: (): AiProviderCapabilities =>\n options.capabilities ?? { toolCalling: true, structuredOutput: true },\n\n languageModel: () => Promise.resolve(chatModel),\n\n generateStructured(request) {\n calls.push(request)\n const callIndex = index\n index += 1\n const object = options.respond\n ? options.respond(request, callIndex)\n : objects.length > 0\n ? objects[Math.min(callIndex, objects.length - 1)]\n : {}\n return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } })\n },\n }\n}\n"]}
1
+ {"version":3,"file":"fake-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/fake-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * FakeProvider (EC-331): a deterministic, fixture-driven {@link AiProvider} that\n * implements the same interface as the Anthropic provider, so playground / demo /\n * tests run the WHOLE loop (config gate → shape → generate → validate → repair →\n * patch) without a key. Determinism is a project principle: the same fixtures always\n * yield the same result. Imports no AI SDK at runtime — the `LanguageModelV4` spec\n * below is implemented STRUCTURALLY from type-only imports (erased at compile;\n * pinned in `bundle.test.ts`).\n *\n * For the repair loop, pass `objects: [invalidDoc, validDoc]` — the provider returns\n * them in call order (the last is reused once the script is exhausted). Or pass\n * `respond` to compute the object from the request.\n *\n * EC-334 adds {@link createFakeChatModel}: a scripted fake `LanguageModel` served by\n * the ROUTE's native chat branch, so the keyless playground / CI drive the FULL\n * `streamText` → SSE wire → client-tool loop. It is a STATELESS prompt-inspecting\n * responder (state read from the prompt, the FakeProvider trick): a prompt without a\n * tool result emits a tool call; a prompt carrying one streams a text answer.\n */\n// Type-only (erased): the fake implements the SDK's v4 model spec structurally.\nimport type { LanguageModel } from 'ai'\nimport type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest } from './types'\n\n// --- Fake chat model (EC-334) -------------------------------------------------------\n\n/** The SDK's v4 language-model spec, extracted structurally from the type-only union. */\nexport type FakeLanguageModelV4 = Extract<LanguageModel, { specificationVersion: 'v4' }>\n\n/** The call options a fake chat model receives (`LanguageModelV4CallOptions`). */\nexport type FakeChatModelCallOptions = Parameters<FakeLanguageModelV4['doGenerate']>[0]\n\n/** One provider-spec stream part the fake emits (`LanguageModelV4StreamPart`). */\nexport type FakeChatModelStreamPart =\n Awaited<ReturnType<FakeLanguageModelV4['doStream']>>['stream'] extends ReadableStream<infer PART>\n ? PART\n : never\n\ntype FakeFinishPart = Extract<FakeChatModelStreamPart, { type: 'finish' }>\ntype FakeGenerateResult = Awaited<ReturnType<FakeLanguageModelV4['doGenerate']>>\n\n/** Zero-usage report (the fake spends nothing). Shape = `LanguageModelV4Usage`. */\nconst FAKE_USAGE: FakeFinishPart['usage'] = {\n inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },\n outputTokens: { total: 0, text: 0, reasoning: 0 },\n}\n\n/** Default preamble streamed before the scripted tool call. */\nexport const FAKE_CHAT_PREAMBLE = 'Let me look that up. '\n/** Default final answer streamed once the prompt carries a tool result. */\nexport const FAKE_CHAT_ANSWER = 'Found it — the tool result is in, and this is the answer.'\n\nexport interface FakeChatModelOptions {\n /** Model id reported to the SDK (default `fake-chat-model`). */\n modelId?: string\n /** Text streamed before the tool call on the first (tool-result-free) turn. */\n preambleText?: string\n /** The tool the first turn requests (default `searchDocuments`). */\n toolName?: string\n /** The tool-call input (default `{ collection: 'post' }`). */\n toolInput?: Record<string, unknown>\n /** The answer streamed word-by-word once the prompt carries a tool result. */\n answerText?: string\n /**\n * Full override: script the exact provider-spec stream parts for a call\n * (zero-based call index). The default prompt-inspecting behavior is skipped.\n */\n script?: (options: FakeChatModelCallOptions, callIndex: number) => FakeChatModelStreamPart[]\n}\n\n/** A fake chat model that also records the calls it received, for assertions. */\nexport type FakeChatModel = FakeLanguageModelV4 & {\n /** Every `doStream` / `doGenerate` call options received, in order. */\n readonly calls: FakeChatModelCallOptions[]\n}\n\n/** Does any prompt message carry a tool RESULT part? (The fake's only \"state\".) */\nfunction promptHasToolResult(prompt: FakeChatModelCallOptions['prompt']): boolean {\n return prompt.some(\n (message) =>\n Array.isArray(message.content) &&\n message.content.some(\n (part) => (part as { type?: unknown }).type === 'tool-result',\n ),\n )\n}\n\n/** Index of the LAST user message in the prompt (`-1` when none). */\nfunction lastUserIndex(prompt: FakeChatModelCallOptions['prompt']): number {\n for (let i = prompt.length - 1; i >= 0; i -= 1) {\n if (prompt[i]?.role === 'user') return i\n }\n return -1\n}\n\n/** The last user message's text (the fake's \"instruction\" source). */\nfunction lastUserText(prompt: FakeChatModelCallOptions['prompt']): string {\n const index = lastUserIndex(prompt)\n const message = prompt[index]\n if (!message || !Array.isArray(message.content)) return ''\n return message.content\n .filter((part): part is { type: 'text'; text: string } => (part as { type?: unknown }).type === 'text')\n .map((part) => part.text)\n .join(' ')\n .trim()\n}\n\n/**\n * Tool names with a RESULT in the CURRENT user turn (prompt suffix after the\n * last user message) — the stateless stage detector for scripted flows.\n */\nfunction toolResultNamesInCurrentTurn(prompt: FakeChatModelCallOptions['prompt']): Set<string> {\n const names = new Set<string>()\n for (const message of prompt.slice(lastUserIndex(prompt) + 1)) {\n if (!Array.isArray(message.content)) continue\n for (const part of message.content) {\n const candidate = part as { type?: unknown; toolName?: unknown }\n if (candidate.type === 'tool-result' && typeof candidate.toolName === 'string') {\n names.add(candidate.toolName)\n }\n }\n }\n return names\n}\n\n/** Provider-spec parts for one text-preamble + tool-call turn (finish `tool-calls`). */\nfunction toolTurnParts(\n preamble: string,\n toolName: string,\n input: Record<string, unknown>,\n callId: string,\n): FakeChatModelStreamPart[] {\n const encoded = JSON.stringify(input)\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n { type: 'text-delta', id: 'text-1', delta: preamble },\n { type: 'text-end', id: 'text-1' },\n { type: 'tool-input-start', id: callId, toolName },\n { type: 'tool-input-delta', id: callId, delta: encoded },\n { type: 'tool-input-end', id: callId },\n { type: 'tool-call', toolCallId: callId, toolName, input: encoded },\n {\n type: 'finish',\n usage: FAKE_USAGE,\n finishReason: { unified: 'tool-calls', raw: 'fake-tool-calls' },\n },\n ]\n}\n\n/** Provider-spec parts for a word-by-word streamed text answer (finish `stop`). */\nfunction answerTurnParts(answer: string): FakeChatModelStreamPart[] {\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n ...wordDeltas(answer).map(\n (delta): FakeChatModelStreamPart => ({ type: 'text-delta', id: 'text-1', delta }),\n ),\n { type: 'text-end', id: 'text-1' },\n { type: 'finish', usage: FAKE_USAGE, finishReason: { unified: 'stop', raw: 'fake-stop' } },\n ]\n}\n\n/** Split text into word deltas (trailing spaces preserved) for visible streaming. */\nfunction wordDeltas(text: string): string[] {\n const words = text.split(' ')\n return words.map((word, index) => (index < words.length - 1 ? `${word} ` : word))\n}\n\n/**\n * Build a scripted, stateless fake `LanguageModel` (the `LanguageModelV4` spec,\n * implemented structurally — zero SDK runtime import). Default behavior:\n * - prompt WITHOUT a tool result → preamble text, then a `tool-call` for\n * {@link FakeChatModelOptions.toolName} (finish reason `tool-calls`);\n * - prompt WITH a tool result → the answer text streamed word-by-word (`stop`).\n * Chunk shapes follow the v4 spec: `finishReason` is `{ unified, raw }`, tool chunks\n * are `tool-input-start/delta/end` + `tool-call`.\n */\nexport function createFakeChatModel(options: FakeChatModelOptions = {}): FakeChatModel {\n const preamble = options.preambleText ?? FAKE_CHAT_PREAMBLE\n const toolName = options.toolName ?? 'searchDocuments'\n const toolInput = options.toolInput ?? { collection: 'post' }\n const answer = options.answerText ?? FAKE_CHAT_ANSWER\n const calls: FakeChatModelCallOptions[] = []\n let callIndex = 0\n\n function partsFor(callOptions: FakeChatModelCallOptions): FakeChatModelStreamPart[] {\n if (options.script) return options.script(callOptions, callIndex)\n if (!promptHasToolResult(callOptions.prompt)) {\n return toolTurnParts(preamble, toolName, toolInput, 'call-1')\n }\n return answerTurnParts(answer)\n }\n\n /** Fold the scripted stream parts into a non-streaming `doGenerate` result. */\n function materialize(parts: FakeChatModelStreamPart[]): FakeGenerateResult {\n const content: FakeGenerateResult['content'] = []\n const textById = new Map<string, string>()\n let finishReason: FakeFinishPart['finishReason'] = { unified: 'stop', raw: 'fake-stop' }\n for (const part of parts) {\n if (part.type === 'text-delta') {\n textById.set(part.id, (textById.get(part.id) ?? '') + part.delta)\n } else if (part.type === 'tool-call') {\n content.push(part)\n } else if (part.type === 'finish') {\n finishReason = part.finishReason\n }\n }\n for (const text of textById.values()) content.unshift({ type: 'text', text })\n return { content, finishReason, usage: FAKE_USAGE, warnings: [] }\n }\n\n return {\n specificationVersion: 'v4',\n provider: 'fake',\n modelId: options.modelId ?? 'fake-chat-model',\n supportedUrls: {},\n calls,\n doGenerate(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve(materialize(parts))\n },\n doStream(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve({\n stream: new ReadableStream<FakeChatModelStreamPart>({\n start(controller) {\n for (const part of parts) controller.enqueue(part)\n controller.close()\n },\n }),\n })\n },\n }\n}\n\n// --- Fake create-flow chat model (EC-334 slice 2) -------------------------------------\n\nexport interface FakeCreateFlowChatModelOptions {\n /** Model id reported to the SDK (default `fake-create-flow-model`). */\n modelId?: string\n /** The collection the first stage searches (a relation target; default `author`). */\n searchCollection?: string\n /**\n * EC-381: when set, the create flow inserts a `listAssets` stage after the\n * relation search and before `createDocument`, so a collection with an image\n * field gets it filled from a REAL library asset (the enum leash). Optional\n * search text passed to `listAssets` (omit to list all).\n */\n searchAssets?: boolean\n /** Optional query passed to the scripted `listAssets` stage (EC-381). */\n assetQuery?: string\n /** The collection the terminal stage creates in (default `post`). */\n createCollection?: string\n /** The final answer streamed word-by-word after the proposal lands. */\n answerText?: string\n /**\n * EC-378: the field the scripted UPDATE flow changes (default `title`). A user\n * turn matching {@link FakeCreateFlowChatModelOptions.updatePattern} routes to\n * the update flow instead of the create flow.\n */\n updateField?: string\n /** Routes a user turn to the update flow (default {@link FAKE_UPDATE_PATTERN}). */\n updatePattern?: RegExp\n /** The final answer of the scripted update flow. */\n updateAnswerText?: string\n}\n\n/** Default final answer of the scripted create flow. */\nexport const FAKE_CREATE_FLOW_ANSWER =\n 'Done — review the proposal below and apply it as a draft.'\n\n/** Default final answer of the scripted update flow (EC-378). */\nexport const FAKE_UPDATE_FLOW_ANSWER =\n 'Done — review the proposed change below and apply it as a draft.'\n\n/** Default edit-request detector of the scripted assistant (EC-378). */\nexport const FAKE_UPDATE_PATTERN = /\\b(change|update|edit|remove|replace|rewrite|fix|set)\\b/i\n\n/**\n * The scripted update flow's new value for the target field: the text after the\n * FIRST colon when present (`Set the title to: New Title` → `New Title`), else\n * the whole user text — deterministic either way. First (not last) colon so a\n * markdown body value may itself contain colons (EC-379).\n */\nexport function fakeUpdateValueFrom(userText: string): string {\n const colon = userText.indexOf(':')\n const value = colon >= 0 ? userText.slice(colon + 1) : userText\n return value.trim() || 'Updated by the assistant'\n}\n\n/**\n * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the\n * <field> to: …` names it (so the playground can drive a markdown-authored\n * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything\n * else falls back to the configured default field.\n */\nexport function fakeUpdateFieldFrom(userText: string, fallback: string): string {\n const match = /\\b(?:set|change|update)\\s+the\\s+([A-Za-z]\\w*)\\s+to\\s*:/i.exec(userText)\n return match?.[1] ?? fallback\n}\n\n/**\n * A scripted, STATELESS fake chat model that drives the FULL create flow — and,\n * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the\n * native loop (EC-334): keyless playground / CI exercise the whole client-tool\n * round-trip deterministically. Stage is read from the CURRENT user turn's tool\n * results (prompt suffix after the last user message), so every follow-up turn\n * starts a fresh flow — multi-turn works without model state.\n *\n * Create flow (default):\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`\n * 3. create result present → the answer text, word by word.\n *\n * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`\n * 3. listAssets done, no create → `createDocument({ collection, instruction })`\n * 4. create result present → the answer text, word by word.\n *\n * Update flow (user text matches `updatePattern`, e.g. \"Change the title to: X\"):\n * 1. no tool result yet → `getDocument({})` — target omitted on purpose:\n * the executor resolves the document OPEN in the\n * editor (the EC-378 current-document seam)\n * 2. document read, no update → `updateFields({ changes: [{ field:\n * fakeUpdateFieldFrom(<text>, updateField),\n * value: fakeUpdateValueFrom(<last user text>) }] })`\n * — \"Set the body to: ## Neu\" drives a markdown\n * rich-text update deterministically (EC-379)\n * 3. update result present → the update answer, word by word.\n */\nexport function createFakeCreateFlowChatModel(\n options: FakeCreateFlowChatModelOptions = {},\n): FakeChatModel {\n const searchCollection = options.searchCollection ?? 'author'\n const createCollection = options.createCollection ?? 'post'\n const searchAssets = options.searchAssets ?? false\n const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER\n const updateField = options.updateField ?? 'title'\n const updatePattern = options.updatePattern ?? FAKE_UPDATE_PATTERN\n const updateAnswer = options.updateAnswerText ?? FAKE_UPDATE_FLOW_ANSWER\n return createFakeChatModel({\n modelId: options.modelId ?? 'fake-create-flow-model',\n script: (callOptions) => {\n const done = toolResultNamesInCurrentTurn(callOptions.prompt)\n const userText = lastUserText(callOptions.prompt)\n if (updatePattern.test(userText)) {\n if (done.has('updateFields')) return answerTurnParts(updateAnswer)\n if (done.has('getDocument')) {\n return toolTurnParts(\n 'Proposing the change. ',\n 'updateFields',\n {\n changes: [\n {\n field: fakeUpdateFieldFrom(userText, updateField),\n value: fakeUpdateValueFrom(userText),\n },\n ],\n },\n 'call-update-1',\n )\n }\n return toolTurnParts('Reading the document. ', 'getDocument', {}, 'call-get-1')\n }\n if (done.has('createDocument')) return answerTurnParts(answer)\n if (done.has('searchDocuments')) {\n // EC-381 — search the media library before creating (a topically fitting,\n // REAL asset fills the image field via the enum leash).\n if (searchAssets && !done.has('listAssets')) {\n return toolTurnParts(\n 'Looking for a fitting image. ',\n 'listAssets',\n { ...(options.assetQuery ? { query: options.assetQuery } : {}) },\n 'call-assets-1',\n )\n }\n const instruction = userText || 'Write a short document.'\n return toolTurnParts(\n 'Drafting a proposal. ',\n 'createDocument',\n { collection: createCollection, instruction },\n 'call-create-1',\n )\n }\n return toolTurnParts(\n FAKE_CHAT_PREAMBLE,\n 'searchDocuments',\n { collection: searchCollection },\n 'call-search-1',\n )\n },\n })\n}\n\n// --- Fake provider ------------------------------------------------------------------\n\nexport interface FakeProviderOptions {\n /** Provider id (default `fake`). */\n id?: string\n /** Model id recorded in provenance (default `fake-model`). */\n model?: string\n /** Reported capabilities (default: both true). Set to test the capability gate. */\n capabilities?: AiProviderCapabilities\n /** Scripted objects returned in call order; the last entry is reused when exhausted. */\n objects?: unknown[]\n /** Compute the object from the request + zero-based call index (overrides `objects`). */\n respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown\n /** The scripted chat model `languageModel()` serves (default {@link createFakeChatModel}). */\n chatModel?: FakeChatModel\n}\n\n/** A FakeProvider that also records the requests it received, for assertions. */\nexport interface FakeProvider extends AiProvider {\n /** Every `generateStructured` request received, in order. */\n readonly calls: GenerateStructuredRequest[]\n /** The scripted fake chat model behind `languageModel()` (records its calls). */\n readonly chatModel: FakeChatModel\n}\n\nexport function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {\n const calls: GenerateStructuredRequest[] = []\n const objects = options.objects ?? []\n const chatModel = options.chatModel ?? createFakeChatModel()\n let index = 0\n\n return {\n id: options.id ?? 'fake',\n model: options.model ?? 'fake-model',\n calls,\n chatModel,\n capabilities: (): AiProviderCapabilities =>\n options.capabilities ?? { toolCalling: true, structuredOutput: true },\n\n languageModel: () => Promise.resolve(chatModel),\n\n generateStructured(request) {\n calls.push(request)\n const callIndex = index\n index += 1\n const object = options.respond\n ? options.respond(request, callIndex)\n : objects.length > 0\n ? objects[Math.min(callIndex, objects.length - 1)]\n : {}\n return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } })\n },\n }\n}\n"]}
@@ -23,8 +23,10 @@ export * from './types.js';
23
23
  export * from './config.js';
24
24
  export * from './provenance.js';
25
25
  export * from './knowledge.js';
26
+ export * from './rich-text-input.js';
26
27
  export * from './shape.js';
27
28
  export * from './apply-content.js';
29
+ export * from './update-fields.js';
28
30
  export * from './anthropic-provider.js';
29
31
  export * from './gateway-provider.js';
30
32
  export * from './fake-provider.js';
@@ -23,8 +23,10 @@ export * from './types.js';
23
23
  export * from './config.js';
24
24
  export * from './provenance.js';
25
25
  export * from './knowledge.js';
26
+ export * from './rich-text-input.js';
26
27
  export * from './shape.js';
27
28
  export * from './apply-content.js';
29
+ export * from './update-fields.js';
28
30
  export * from './anthropic-provider.js';
29
31
  export * from './gateway-provider.js';
30
32
  export * from './fake-provider.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
@@ -0,0 +1,27 @@
1
+ import type { RichTextValue } from '@elytracms/core/rich-text';
2
+ /**
3
+ * The model-input policy for rich-text field values (EC-379), shared by the
4
+ * create engine (`applyContentWithShape`) and the edit flow
5
+ * (`buildUpdateProposal`):
6
+ *
7
+ * - a **string is ALWAYS markdown** (never JSON-sniffed — deterministic): it is
8
+ * converted through the engine's strict subset converter, and out-of-subset
9
+ * constructs come back as explicit problems the caller feeds to the model for
10
+ * repair;
11
+ * - a `{ version, doc }` **envelope object is still accepted** for robustness
12
+ * (the model sees envelopes in `getDocument` output whenever a document is
13
+ * not losslessly expressible as markdown, and may echo them back) — it is
14
+ * schema-validated here and leash-validated downstream by `validateDocument`
15
+ * + the injected `validateRichTextEmbeds`;
16
+ * - anything else is a problem. The target SHAPE only ever advertises the
17
+ * markdown string form (`shape.ts`), so markdown is what the model is
18
+ * steered to author.
19
+ */
20
+ export type RichTextInputResult = {
21
+ ok: true;
22
+ value: RichTextValue;
23
+ } | {
24
+ ok: false;
25
+ problems: string[];
26
+ };
27
+ export declare function richTextValueFromModelInput(raw: unknown): RichTextInputResult;
@@ -0,0 +1,25 @@
1
+ import { MARKDOWN_PROSE_DESCRIPTION, markdownProseToRichTextValue, parseRichTextValue, } from '@elytracms/core/rich-text';
2
+ export function richTextValueFromModelInput(raw) {
3
+ if (typeof raw === 'string') {
4
+ const converted = markdownProseToRichTextValue(raw);
5
+ if (converted.ok)
6
+ return { ok: true, value: converted.value };
7
+ return { ok: false, problems: converted.issues.map((issue) => issue.message) };
8
+ }
9
+ if (typeof raw === 'object' && raw !== null) {
10
+ const parsed = parseRichTextValue(raw);
11
+ if (parsed.ok)
12
+ return { ok: true, value: parsed.value };
13
+ return {
14
+ ok: false,
15
+ problems: [
16
+ `Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}); the provided object is not a valid { version, doc } storage envelope either.`,
17
+ ],
18
+ };
19
+ }
20
+ return {
21
+ ok: false,
22
+ problems: [`Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}).`],
23
+ };
24
+ }
25
+ //# sourceMappingURL=rich-text-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text-input.js","sourceRoot":"","sources":["../../../src/lib/ai/rich-text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,2BAA2B,CAAA;AAyBlC,MAAM,UAAU,2BAA2B,CAAC,GAAY;IACtD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,4BAA4B,CAAC,GAAG,CAAC,CAAA;QACnD,IAAI,SAAS,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAA;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAA;IAChF,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;QACvD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE;gBACR,oDAAoD,0BAA0B,iFAAiF;aAChK;SACF,CAAA;IACH,CAAC;IACD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,QAAQ,EAAE,CAAC,oDAAoD,0BAA0B,IAAI,CAAC;KAC/F,CAAA;AACH,CAAC","sourcesContent":["import {\n MARKDOWN_PROSE_DESCRIPTION,\n markdownProseToRichTextValue,\n parseRichTextValue,\n} from '@elytracms/core/rich-text'\nimport type { RichTextValue } from '@elytracms/core/rich-text'\n\n/**\n * The model-input policy for rich-text field values (EC-379), shared by the\n * create engine (`applyContentWithShape`) and the edit flow\n * (`buildUpdateProposal`):\n *\n * - a **string is ALWAYS markdown** (never JSON-sniffed — deterministic): it is\n * converted through the engine's strict subset converter, and out-of-subset\n * constructs come back as explicit problems the caller feeds to the model for\n * repair;\n * - a `{ version, doc }` **envelope object is still accepted** for robustness\n * (the model sees envelopes in `getDocument` output whenever a document is\n * not losslessly expressible as markdown, and may echo them back) — it is\n * schema-validated here and leash-validated downstream by `validateDocument`\n * + the injected `validateRichTextEmbeds`;\n * - anything else is a problem. The target SHAPE only ever advertises the\n * markdown string form (`shape.ts`), so markdown is what the model is\n * steered to author.\n */\nexport type RichTextInputResult =\n | { ok: true; value: RichTextValue }\n | { ok: false; problems: string[] }\n\nexport function richTextValueFromModelInput(raw: unknown): RichTextInputResult {\n if (typeof raw === 'string') {\n const converted = markdownProseToRichTextValue(raw)\n if (converted.ok) return { ok: true, value: converted.value }\n return { ok: false, problems: converted.issues.map((issue) => issue.message) }\n }\n if (typeof raw === 'object' && raw !== null) {\n const parsed = parseRichTextValue(raw)\n if (parsed.ok) return { ok: true, value: parsed.value }\n return {\n ok: false,\n problems: [\n `Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}); the provided object is not a valid { version, doc } storage envelope either.`,\n ],\n }\n }\n return {\n ok: false,\n problems: [`Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}).`],\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { CollectionDef, JsonSchema } from '@elytracms/core/cms-core';
2
- import type { RelationCandidates } from './types.js';
2
+ import type { AssetCandidates, RelationCandidates } from './types.js';
3
3
  /** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */
4
4
  export interface TargetRelationField {
5
5
  fieldName: string;
@@ -9,6 +9,13 @@ export interface TargetRelationField {
9
9
  /** The allowed candidate ids (the enum the model chose from). */
10
10
  candidateIds: string[];
11
11
  }
12
+ /** An asset field's resolved candidate set — how the engine maps ids → asset envelopes (EC-381). */
13
+ export interface TargetAssetField {
14
+ fieldName: string;
15
+ multiple: boolean;
16
+ /** The allowed library asset ids (the enum the model chose from). */
17
+ candidateIds: string[];
18
+ }
12
19
  /** The derived target shape + the metadata the engine needs to post-process the output. */
13
20
  export interface TargetShape {
14
21
  /** The JSON Schema the structured output must satisfy. */
@@ -19,11 +26,15 @@ export interface TargetShape {
19
26
  localizedFields: string[];
20
27
  /** Relation fields with their candidate enums. */
21
28
  relationFields: TargetRelationField[];
29
+ /** Asset fields with their candidate enums (EC-381). */
30
+ assetFields: TargetAssetField[];
22
31
  }
23
32
  export interface BuildTargetShapeInput {
24
33
  collection: CollectionDef;
25
34
  /** Relation candidate ids by field — a relation field with none is omitted from the shape. */
26
35
  candidates?: RelationCandidates;
36
+ /** Asset candidate ids by field — an asset field with none is omitted from the shape (EC-381). */
37
+ assetCandidates?: AssetCandidates;
27
38
  /** UI locale for resolving field labels into descriptions. */
28
39
  locale?: string;
29
40
  }
@@ -10,30 +10,33 @@
10
10
  * - relation fields become an ENUM over the provided candidate ids (or are omitted
11
11
  * when no candidates exist) — so a hallucinated relation id is not representable
12
12
  * as valid structured output.
13
- * - rich-text fields are emitted in the `{ version, doc }` ProseMirror storage shape
14
- * (the `allow` leash on embeds is enforced later by `validateRichTextEmbeds`).
13
+ * - asset/image fields become an ENUM over the searched library asset ids (or are
14
+ * omitted when none were searched) — EC-381, the SAME leash as relations: a
15
+ * proposed asset id must be a real, `listAssets`-returned library asset, and a
16
+ * create shape with no searched assets simply has no asset property (empty beats
17
+ * wrong; the assistant never generates or uploads).
18
+ * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the
19
+ * allowed subset, and the engine converts md → the `{ version, doc }` storage
20
+ * envelope at proposal-build time. The field's `allow` config leashes component
21
+ * EMBEDS only — markdown cannot express an embed, so the leash is satisfied by
22
+ * construction (and still enforced downstream by `validateRichTextEmbeds`).
15
23
  *
16
24
  * Localized fields stay flat in the shape (the model authors one locale at a time);
17
25
  * the engine routes their values under `localized[locale]`.
18
26
  */
19
27
  import { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core';
20
- /** The rich-text `{ version, doc }` storage envelope, as a JSON Schema. */
21
- function richTextEnvelopeSchema() {
28
+ import { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text';
29
+ /**
30
+ * A rich-text field's model-facing schema (EC-379): a MARKDOWN string. The
31
+ * description carries the allowed subset (from the engine converter — one source
32
+ * of truth), so the model has something concrete to aim at; the engine converts
33
+ * the markdown into the `{ version, doc }` storage envelope at proposal-build
34
+ * time and rejects out-of-subset constructs with explicit issues.
35
+ */
36
+ function richTextMarkdownSchema() {
22
37
  return {
23
- type: 'object',
24
- description: 'Rich text in the ProseMirror storage shape: { version, doc: { type: "doc", content: [...] } }.',
25
- properties: {
26
- version: { type: 'number' },
27
- doc: {
28
- type: 'object',
29
- properties: {
30
- type: { type: 'string', const: 'doc' },
31
- content: { type: 'array', items: { type: 'object' } },
32
- },
33
- required: ['type', 'content'],
34
- },
35
- },
36
- required: ['version', 'doc'],
38
+ type: 'string',
39
+ description: `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`,
37
40
  };
38
41
  }
39
42
  /** A field's model-facing description: resolved label + optional `ai.hint`. */
@@ -56,6 +59,7 @@ export function buildTargetShape(input) {
56
59
  const excluded = [];
57
60
  const localizedFields = [];
58
61
  const relationFields = [];
62
+ const assetFields = [];
59
63
  for (const field of input.collection.fields) {
60
64
  if (field.localized)
61
65
  localizedFields.push(field.name);
@@ -89,8 +93,30 @@ export function buildTargetShape(input) {
89
93
  candidateIds: ids,
90
94
  });
91
95
  }
96
+ else if (field.type === 'asset') {
97
+ // EC-381 — same leash as relations: an asset field is an enum over the ids
98
+ // the model actually searched in the media library (`listAssets`). No
99
+ // searched assets ⇒ omit the field, so the proposal simply leaves it empty
100
+ // (an invented / to-be-uploaded id is structurally unrepresentable).
101
+ const candidates = input.assetCandidates?.[field.name] ?? [];
102
+ if (candidates.length === 0) {
103
+ delete properties[field.name];
104
+ continue;
105
+ }
106
+ const ids = candidates.map((c) => c.id);
107
+ const label = candidates
108
+ .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))
109
+ .join(', ');
110
+ const idSchema = {
111
+ type: 'string',
112
+ enum: ids,
113
+ description: `Choose an EXISTING library asset id, one of: ${label}. Leave empty if none fits.`,
114
+ };
115
+ properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema;
116
+ assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds: ids });
117
+ }
92
118
  else if (field.type === 'richText') {
93
- properties[field.name] = richTextEnvelopeSchema();
119
+ properties[field.name] = richTextMarkdownSchema();
94
120
  }
95
121
  const prop = properties[field.name];
96
122
  if (prop) {
@@ -108,6 +134,6 @@ export function buildTargetShape(input) {
108
134
  additionalProperties: false,
109
135
  ...(required.length > 0 ? { required } : {}),
110
136
  };
111
- return { schema, excluded, localizedFields, relationFields };
137
+ return { schema, excluded, localizedFields, relationFields, assetFields };
112
138
  }
113
139
  //# sourceMappingURL=shape.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shape.js","sourceRoot":"","sources":["../../../src/lib/ai/shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target-shape derivation for `applyContentWithShape` (EC-331).\n *\n * Reuses the existing collection→JSON-Schema derivation (`collectionJsonSchema`,\n * cms-core) rather than writing a new one, then applies the three leashes the AI\n * engine adds on top:\n *\n * - `ai.exclude` fields are removed entirely (the model never sees them → cannot\n * write them).\n * - relation fields become an ENUM over the provided candidate ids (or are omitted\n * when no candidates exist) — so a hallucinated relation id is not representable\n * as valid structured output.\n * - rich-text fields are emitted in the `{ version, doc }` ProseMirror storage shape\n * (the `allow` leash on embeds is enforced later by `validateRichTextEmbeds`).\n *\n * Localized fields stay flat in the shape (the model authors one locale at a time);\n * the engine routes their values under `localized[locale]`.\n */\nimport { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport type { RelationCandidates } from './types'\n\n/** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */\nexport interface TargetRelationField {\n fieldName: string\n /** The relation's target collection id. */\n target: string\n multiple: boolean\n /** The allowed candidate ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** The derived target shape + the metadata the engine needs to post-process the output. */\nexport interface TargetShape {\n /** The JSON Schema the structured output must satisfy. */\n schema: JsonSchema\n /** Field names removed by `ai.exclude` (untouchable). */\n excluded: string[]\n /** Field names whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** Relation candidate ids by field — a relation field with none is omitted from the shape. */\n candidates?: RelationCandidates\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/** The rich-text `{ version, doc }` storage envelope, as a JSON Schema. */\nfunction richTextEnvelopeSchema(): JsonSchema {\n return {\n type: 'object',\n description:\n 'Rich text in the ProseMirror storage shape: { version, doc: { type: \"doc\", content: [...] } }.',\n properties: {\n version: { type: 'number' },\n doc: {\n type: 'object',\n properties: {\n type: { type: 'string', const: 'doc' },\n content: { type: 'array', items: { type: 'object' } },\n },\n required: ['type', 'content'],\n },\n },\n required: ['version', 'doc'],\n }\n}\n\n/** A field's model-facing description: resolved label + optional `ai.hint`. */\nfunction describe(field: FieldDef, locale?: string): string | undefined {\n const label = resolveLabel(field.form?.label, locale) ?? field.name\n const hint = field.ai?.hint\n const parts = [label, hint].filter((p): p is string => typeof p === 'string' && p.length > 0)\n return parts.length > 0 ? parts.join(' — ') : undefined\n}\n\n/**\n * Build the leash-aware target shape from a collection + candidates. Deterministic;\n * never throws. Reuses `collectionJsonSchema` as the base, then rewrites relations /\n * rich text and strips excluded fields.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\n const base = collectionJsonSchema(input.collection)\n const properties: Record<string, JsonSchema> = {\n ...((base.properties as Record<string, JsonSchema>) ?? {}),\n }\n const excluded: string[] = []\n const localizedFields: string[] = []\n const relationFields: TargetRelationField[] = []\n\n for (const field of input.collection.fields) {\n if (field.localized) localizedFields.push(field.name)\n\n if (field.ai?.exclude) {\n excluded.push(field.name)\n delete properties[field.name]\n continue\n }\n\n if (field.type === 'relation') {\n const candidates = input.candidates?.[field.name] ?? []\n if (candidates.length === 0) {\n // No candidate ids ⇒ omit the field: the model cannot emit a relation value\n // at all, so a hallucinated id is structurally impossible.\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose one of: ${label}`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n relationFields.push({\n fieldName: field.name,\n target: field.target,\n multiple: field.multiple,\n candidateIds: ids,\n })\n } else if (field.type === 'richText') {\n properties[field.name] = richTextEnvelopeSchema()\n }\n\n const prop = properties[field.name]\n if (prop) {\n const desc = describe(field, input.locale)\n // Preserve an existing description (e.g. relation enum / rich-text note).\n if (desc && typeof prop.description !== 'string') prop.description = desc\n }\n }\n\n const requiredIn = Array.isArray(base.required) ? (base.required as string[]) : []\n const required = requiredIn.filter((n) => !excluded.includes(n) && properties[n] !== undefined)\n\n const schema: JsonSchema = {\n type: 'object',\n properties,\n additionalProperties: false,\n ...(required.length > 0 ? { required } : {}),\n }\n return { schema, excluded, localizedFields, relationFields }\n}\n"]}
1
+ {"version":3,"file":"shape.js","sourceRoot":"","sources":["../../../src/lib/ai/shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target-shape derivation for `applyContentWithShape` (EC-331).\n *\n * Reuses the existing collection→JSON-Schema derivation (`collectionJsonSchema`,\n * cms-core) rather than writing a new one, then applies the three leashes the AI\n * engine adds on top:\n *\n * - `ai.exclude` fields are removed entirely (the model never sees them → cannot\n * write them).\n * - relation fields become an ENUM over the provided candidate ids (or are omitted\n * when no candidates exist) — so a hallucinated relation id is not representable\n * as valid structured output.\n * - asset/image fields become an ENUM over the searched library asset ids (or are\n * omitted when none were searched) — EC-381, the SAME leash as relations: a\n * proposed asset id must be a real, `listAssets`-returned library asset, and a\n * create shape with no searched assets simply has no asset property (empty beats\n * wrong; the assistant never generates or uploads).\n * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the\n * allowed subset, and the engine converts md → the `{ version, doc }` storage\n * envelope at proposal-build time. The field's `allow` config leashes component\n * EMBEDS only — markdown cannot express an embed, so the leash is satisfied by\n * construction (and still enforced downstream by `validateRichTextEmbeds`).\n *\n * Localized fields stay flat in the shape (the model authors one locale at a time);\n * the engine routes their values under `localized[locale]`.\n */\nimport { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text'\nimport type { AssetCandidates, RelationCandidates } from './types'\n\n/** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */\nexport interface TargetRelationField {\n fieldName: string\n /** The relation's target collection id. */\n target: string\n multiple: boolean\n /** The allowed candidate ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** An asset field's resolved candidate set — how the engine maps ids → asset envelopes (EC-381). */\nexport interface TargetAssetField {\n fieldName: string\n multiple: boolean\n /** The allowed library asset ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** The derived target shape + the metadata the engine needs to post-process the output. */\nexport interface TargetShape {\n /** The JSON Schema the structured output must satisfy. */\n schema: JsonSchema\n /** Field names removed by `ai.exclude` (untouchable). */\n excluded: string[]\n /** Field names whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n /** Asset fields with their candidate enums (EC-381). */\n assetFields: TargetAssetField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** Relation candidate ids by field — a relation field with none is omitted from the shape. */\n candidates?: RelationCandidates\n /** Asset candidate ids by field — an asset field with none is omitted from the shape (EC-381). */\n assetCandidates?: AssetCandidates\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/**\n * A rich-text field's model-facing schema (EC-379): a MARKDOWN string. The\n * description carries the allowed subset (from the engine converter — one source\n * of truth), so the model has something concrete to aim at; the engine converts\n * the markdown into the `{ version, doc }` storage envelope at proposal-build\n * time and rejects out-of-subset constructs with explicit issues.\n */\nfunction richTextMarkdownSchema(): JsonSchema {\n return {\n type: 'string',\n description: `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`,\n }\n}\n\n/** A field's model-facing description: resolved label + optional `ai.hint`. */\nfunction describe(field: FieldDef, locale?: string): string | undefined {\n const label = resolveLabel(field.form?.label, locale) ?? field.name\n const hint = field.ai?.hint\n const parts = [label, hint].filter((p): p is string => typeof p === 'string' && p.length > 0)\n return parts.length > 0 ? parts.join(' — ') : undefined\n}\n\n/**\n * Build the leash-aware target shape from a collection + candidates. Deterministic;\n * never throws. Reuses `collectionJsonSchema` as the base, then rewrites relations /\n * rich text and strips excluded fields.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\n const base = collectionJsonSchema(input.collection)\n const properties: Record<string, JsonSchema> = {\n ...((base.properties as Record<string, JsonSchema>) ?? {}),\n }\n const excluded: string[] = []\n const localizedFields: string[] = []\n const relationFields: TargetRelationField[] = []\n const assetFields: TargetAssetField[] = []\n\n for (const field of input.collection.fields) {\n if (field.localized) localizedFields.push(field.name)\n\n if (field.ai?.exclude) {\n excluded.push(field.name)\n delete properties[field.name]\n continue\n }\n\n if (field.type === 'relation') {\n const candidates = input.candidates?.[field.name] ?? []\n if (candidates.length === 0) {\n // No candidate ids ⇒ omit the field: the model cannot emit a relation value\n // at all, so a hallucinated id is structurally impossible.\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose one of: ${label}`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n relationFields.push({\n fieldName: field.name,\n target: field.target,\n multiple: field.multiple,\n candidateIds: ids,\n })\n } else if (field.type === 'asset') {\n // EC-381 — same leash as relations: an asset field is an enum over the ids\n // the model actually searched in the media library (`listAssets`). No\n // searched assets ⇒ omit the field, so the proposal simply leaves it empty\n // (an invented / to-be-uploaded id is structurally unrepresentable).\n const candidates = input.assetCandidates?.[field.name] ?? []\n if (candidates.length === 0) {\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose an EXISTING library asset id, one of: ${label}. Leave empty if none fits.`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds: ids })\n } else if (field.type === 'richText') {\n properties[field.name] = richTextMarkdownSchema()\n }\n\n const prop = properties[field.name]\n if (prop) {\n const desc = describe(field, input.locale)\n // Preserve an existing description (e.g. relation enum / rich-text note).\n if (desc && typeof prop.description !== 'string') prop.description = desc\n }\n }\n\n const requiredIn = Array.isArray(base.required) ? (base.required as string[]) : []\n const required = requiredIn.filter((n) => !excluded.includes(n) && properties[n] !== undefined)\n\n const schema: JsonSchema = {\n type: 'object',\n properties,\n additionalProperties: false,\n ...(required.length > 0 ? { required } : {}),\n }\n return { schema, excluded, localizedFields, relationFields, assetFields }\n}\n"]}