@case-framework/survey-assistant 0.8.0 → 0.9.0

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 (114) hide show
  1. package/dist/{authoring-references-DbuOKerg.mjs → authoring-references-CmwLc_C8.mjs} +40 -32
  2. package/dist/authoring-references-CmwLc_C8.mjs.map +1 -0
  3. package/dist/capabilities-D55dq-fO.mjs +582 -0
  4. package/dist/capabilities-D55dq-fO.mjs.map +1 -0
  5. package/dist/capabilities-default.d.mts +4 -12
  6. package/dist/capabilities-default.d.mts.map +1 -1
  7. package/dist/capabilities-default.mjs +2 -2
  8. package/dist/capabilities-z95e1mti.d.mts +117 -0
  9. package/dist/capabilities-z95e1mti.d.mts.map +1 -0
  10. package/dist/{constants-B6HzpEsx.d.mts → constants-HL_klgJl.d.mts} +4 -4
  11. package/dist/{constants-B6HzpEsx.d.mts.map → constants-HL_klgJl.d.mts.map} +1 -1
  12. package/dist/{controller-proxy-DEeFl9IA.mjs → controller-proxy-BQ1YsVxJ.mjs} +4 -3
  13. package/dist/controller-proxy-BQ1YsVxJ.mjs.map +1 -0
  14. package/dist/{controller-proxy-D7uQfm5f.d.mts → controller-proxy-Cmzdr9tV.d.mts} +4 -4
  15. package/dist/{controller-proxy-D7uQfm5f.d.mts.map → controller-proxy-Cmzdr9tV.d.mts.map} +1 -1
  16. package/dist/default-D22AKTqE.mjs +233 -0
  17. package/dist/default-D22AKTqE.mjs.map +1 -0
  18. package/dist/{digest-C39WyAWG.d.mts → digest-FnTTDBq2.d.mts} +2 -2
  19. package/dist/digest-FnTTDBq2.d.mts.map +1 -0
  20. package/dist/digest.d.mts +1 -1
  21. package/dist/engine-D950TfvC.mjs +3098 -0
  22. package/dist/engine-D950TfvC.mjs.map +1 -0
  23. package/dist/engine.d.mts +4 -3
  24. package/dist/engine.mjs +3 -3
  25. package/dist/{index-WB5kfnz0.d.mts → index-B8TLBd1G.d.mts} +8 -6
  26. package/dist/index-B8TLBd1G.d.mts.map +1 -0
  27. package/dist/index-CaX3hZmr.d.mts +23813 -0
  28. package/dist/index-CaX3hZmr.d.mts.map +1 -0
  29. package/dist/{index-r5riv4md.d.mts → index-CeUlGkoU.d.mts} +27 -6
  30. package/dist/index-CeUlGkoU.d.mts.map +1 -0
  31. package/dist/{index-D7wT6P3t.d.mts → index-DYHdvSE8.d.mts} +83 -41
  32. package/dist/index-DYHdvSE8.d.mts.map +1 -0
  33. package/dist/lifecycle-DFzxVxpq.d.mts +54346 -0
  34. package/dist/lifecycle-DFzxVxpq.d.mts.map +1 -0
  35. package/dist/{memory-thread-repository-DAnOGIB3.mjs → memory-thread-repository-DmLadygY.mjs} +15 -9
  36. package/dist/{memory-thread-repository-DAnOGIB3.mjs.map → memory-thread-repository-DmLadygY.mjs.map} +1 -1
  37. package/dist/protocol-BQ7uIDYT.mjs +569 -0
  38. package/dist/protocol-BQ7uIDYT.mjs.map +1 -0
  39. package/dist/protocol.d.mts +4 -4
  40. package/dist/protocol.mjs +3 -2
  41. package/dist/{react-C_GY-Fsc.mjs → react-DybJ5pWw.mjs} +17 -11
  42. package/dist/react-DybJ5pWw.mjs.map +1 -0
  43. package/dist/react-integration.d.mts +1 -1
  44. package/dist/react-integration.mjs +1 -1
  45. package/dist/react.d.mts +2 -2
  46. package/dist/react.mjs +2 -2
  47. package/dist/references/assistant-operations.md +190 -215
  48. package/dist/references/core-rules.md +2 -2
  49. package/dist/references/element-types.md +189 -0
  50. package/dist/references/expressions.md +215 -238
  51. package/dist/references/follow-ups.md +151 -0
  52. package/dist/references/localization.md +26 -12
  53. package/dist/references/response-variables.md +3 -3
  54. package/dist/references/rich-text-content.md +22 -43
  55. package/dist/references/source-material-surveys.md +20 -25
  56. package/dist/references/survey-data-model.md +36 -117
  57. package/dist/{request-context-Dg4jSwN1.mjs → request-context-KVh8VjgH.mjs} +4 -4
  58. package/dist/request-context-KVh8VjgH.mjs.map +1 -0
  59. package/dist/server-agent.d.mts +28 -7
  60. package/dist/server-agent.d.mts.map +1 -1
  61. package/dist/server-agent.mjs +33 -25
  62. package/dist/server-agent.mjs.map +1 -1
  63. package/dist/server-runtime.d.mts +6 -5
  64. package/dist/server-runtime.d.mts.map +1 -1
  65. package/dist/server-runtime.mjs +3 -3
  66. package/dist/server-tasks.d.mts +1 -1
  67. package/dist/server-tasks.mjs +1 -1
  68. package/dist/server-tools.d.mts +1 -1
  69. package/dist/server-tools.mjs +1 -1
  70. package/dist/server.d.mts +7 -6
  71. package/dist/server.d.mts.map +1 -1
  72. package/dist/server.mjs +3 -3
  73. package/dist/{tasks-BZEiwgxA.mjs → tasks-Coh5N027.mjs} +16 -15
  74. package/dist/tasks-Coh5N027.mjs.map +1 -0
  75. package/dist/{tasks-B9h3LPam.d.mts → tasks-wb9M-bak.d.mts} +11 -11
  76. package/dist/{tasks-B9h3LPam.d.mts.map → tasks-wb9M-bak.d.mts.map} +1 -1
  77. package/dist/{thread-handlers-Bef0td9N.d.mts → thread-handlers-C3Iya92v.d.mts} +6 -5
  78. package/dist/thread-handlers-C3Iya92v.d.mts.map +1 -0
  79. package/dist/{tools-CEsv-WoO.mjs → tools-D0KBDolR.mjs} +435 -408
  80. package/dist/tools-D0KBDolR.mjs.map +1 -0
  81. package/dist/turn-survey-draft-B34lggSG.d.mts +185 -0
  82. package/dist/turn-survey-draft-B34lggSG.d.mts.map +1 -0
  83. package/dist/ui.d.mts +3 -3
  84. package/dist/ui.d.mts.map +1 -1
  85. package/dist/ui.mjs +35 -23
  86. package/dist/ui.mjs.map +1 -1
  87. package/package.json +7 -7
  88. package/dist/authoring-references-DbuOKerg.mjs.map +0 -1
  89. package/dist/capabilities-CmeeAjEc.d.mts +0 -192
  90. package/dist/capabilities-CmeeAjEc.d.mts.map +0 -1
  91. package/dist/capabilities-DCMA71PP.mjs +0 -58
  92. package/dist/capabilities-DCMA71PP.mjs.map +0 -1
  93. package/dist/controller-proxy-DEeFl9IA.mjs.map +0 -1
  94. package/dist/default-fhSon2RM.mjs +0 -760
  95. package/dist/default-fhSon2RM.mjs.map +0 -1
  96. package/dist/digest-C39WyAWG.d.mts.map +0 -1
  97. package/dist/engine-nXoCWYpQ.mjs +0 -6888
  98. package/dist/engine-nXoCWYpQ.mjs.map +0 -1
  99. package/dist/index-Ciq7vHIs.d.mts +0 -716
  100. package/dist/index-Ciq7vHIs.d.mts.map +0 -1
  101. package/dist/index-D7wT6P3t.d.mts.map +0 -1
  102. package/dist/index-WB5kfnz0.d.mts.map +0 -1
  103. package/dist/index-r5riv4md.d.mts.map +0 -1
  104. package/dist/lifecycle-Dm8u7QFh.d.mts +0 -545
  105. package/dist/lifecycle-Dm8u7QFh.d.mts.map +0 -1
  106. package/dist/protocol-CF4Bh_Ch.mjs +0 -1255
  107. package/dist/protocol-CF4Bh_Ch.mjs.map +0 -1
  108. package/dist/react-C_GY-Fsc.mjs.map +0 -1
  109. package/dist/references/embedded-forms.md +0 -146
  110. package/dist/references/item-types.md +0 -641
  111. package/dist/request-context-Dg4jSwN1.mjs.map +0 -1
  112. package/dist/tasks-BZEiwgxA.mjs.map +0 -1
  113. package/dist/thread-handlers-Bef0td9N.d.mts.map +0 -1
  114. package/dist/tools-CEsv-WoO.mjs.map +0 -1
@@ -0,0 +1,3098 @@
1
+ import { f as surveyChangeSetSchema, l as surveyAssistantTurnFinalSchema } from "./protocol-BQ7uIDYT.mjs";
2
+ import { B as elementTransportSchema, H as surveyContentSchema, T as surveyOperationSchema, V as jsonValueSchema } from "./capabilities-D55dq-fO.mjs";
3
+ import { stableStringify } from "./digest.mjs";
4
+ import { a as assertSurveyAssistantCapabilities, n as defaultSurveyAssistantCapabilitySet } from "./default-D22AKTqE.mjs";
5
+ import { z } from "zod";
6
+ import { ContentType, DefinitionError, JsonPatchError, Survey, SurveyStateGraph, annotatedReferences, applyJsonPatch, collectExpressionDependencies, contentOwnerIds, encodeJsonPointerSegment, expressionIssues, expressionSchema, expressionSchema as expressionSchema$1, functionTypeIssues, generateId, getContentPlainText, getDefaultProjectionIssues, getDefinitionDiagnostics, getLocalizationEntryKey, hasLocalizationText, inferExpressionReturnType, inferExpressionReturnType as inferExpressionReturnType$1, inspectAnnotated, isContentEmpty, listLocalizationEntries, ownValue, ownerAtPath, parseJsonPointer, readLocalizationContent, resolveElementName, setOwnValue, slotPrefillSchema, stringRecord, templateValueSchema, transformExpression, unreachable, validationRuleSchema } from "@case-framework/survey-core";
7
+ import { defaultElementRegistry } from "@case-framework/survey-items";
8
+ import { SurveyEditor, getAnswerTypeChangeDiagnostics } from "@case-framework/survey-core/editor";
9
+ //#region src/engine/survey-model.ts
10
+ /** One trusted headless definition registry, shared with the editor/player/exporter.
11
+ * Unknown types reject; serialized capabilities never install slot or runtime behavior.
12
+ */
13
+ function createAssistantSurvey(raw, capabilities) {
14
+ const registry = capabilities?.registry ?? defaultElementRegistry;
15
+ return Survey.parse(raw, registry, { admission: "draft" });
16
+ }
17
+ function getAssistantVisibilityIssues(survey) {
18
+ try {
19
+ new SurveyStateGraph(survey, { admission: "draft" });
20
+ return [];
21
+ } catch (error) {
22
+ return error instanceof DefinitionError ? [...error.diagnostics] : [{
23
+ severity: "error",
24
+ code: "invalid-expression-graph",
25
+ path: [],
26
+ message: error instanceof Error ? error.message : String(error)
27
+ }];
28
+ }
29
+ }
30
+ function getAssistantResponseSchemaIssues(raw, capabilities) {
31
+ try {
32
+ const survey = createAssistantSurvey(raw, capabilities);
33
+ return [...getAssistantVisibilityIssues(survey), ...getDefaultProjectionIssues(survey).map((message) => ({
34
+ severity: "error",
35
+ code: "invalid-response-export",
36
+ message
37
+ }))];
38
+ } catch (error) {
39
+ return error instanceof DefinitionError ? [...error.diagnostics] : [{
40
+ severity: "error",
41
+ code: "invalid-response-schema",
42
+ message: error instanceof Error ? error.message : String(error)
43
+ }];
44
+ }
45
+ }
46
+ //#endregion
47
+ //#region src/engine/context.ts
48
+ const getChildIds = (item) => item.itemType === "group" ? item.config.items : [];
49
+ const deriveResponseSlotTypes = (survey, trustedCapabilities) => Object.fromEntries(createAssistantSurvey(survey, trustedCapabilities).slots.definitions.map((slot) => [slot.slotId, slot.valueType]));
50
+ const buildDerivedContext = (snapshot, trustedCapabilities, requestedFocusItemId) => {
51
+ const rawItems = snapshot.survey.surveyItems;
52
+ const itemsById = new Map(rawItems.map((item) => [item.id, item]));
53
+ const parentById = /* @__PURE__ */ new Map();
54
+ for (const item of rawItems) for (const childId of getChildIds(item)) if (itemsById.has(childId) && !parentById.has(childId)) parentById.set(childId, item.id);
55
+ const model = createAssistantSurvey(snapshot.survey, trustedCapabilities);
56
+ const name = (id) => model.resolveItemEditorName(id, snapshot.ui.selectedLocale);
57
+ const getNamePath = (id) => {
58
+ const names = [name(id)];
59
+ const seen = /* @__PURE__ */ new Set([id]);
60
+ let parent = parentById.get(id);
61
+ while (parent && !seen.has(parent)) {
62
+ seen.add(parent);
63
+ names.unshift(name(parent));
64
+ parent = parentById.get(parent);
65
+ }
66
+ return names;
67
+ };
68
+ const outline = [];
69
+ const visited = /* @__PURE__ */ new Set();
70
+ const visit = (itemId, depth) => {
71
+ if (visited.has(itemId)) return;
72
+ const item = itemsById.get(itemId);
73
+ if (!item) return;
74
+ visited.add(itemId);
75
+ outline.push({
76
+ itemId,
77
+ parentId: parentById.get(itemId),
78
+ depth,
79
+ itemType: item.itemType,
80
+ editorName: name(item.id),
81
+ breadcrumb: getNamePath(itemId).join(" / "),
82
+ itemLabel: item.metadata?.itemLabel,
83
+ childCount: getChildIds(item).length
84
+ });
85
+ for (const childId of getChildIds(item)) visit(childId, depth + 1);
86
+ };
87
+ const explicitRoot = rawItems.find((item) => item.itemType === "group" && item.config.isRoot === true);
88
+ if (explicitRoot) visit(explicitRoot.id, 0);
89
+ for (const item of [...rawItems].sort((left, right) => left.id.localeCompare(right.id))) if (!visited.has(item.id)) visit(item.id, 0);
90
+ const itemIndex = outline.map((node) => ({
91
+ itemId: node.itemId,
92
+ itemType: node.itemType,
93
+ editorName: node.editorName,
94
+ breadcrumb: node.breadcrumb,
95
+ itemLabel: node.itemLabel,
96
+ path: getNamePath(node.itemId)
97
+ }));
98
+ const buildFocusNode = (itemId, budget) => {
99
+ const item = itemsById.get(itemId);
100
+ budget.remaining -= 1;
101
+ const owners = [...model.owners.values()].filter((owner) => owner.itemId === itemId);
102
+ const translationSnippets = Object.entries(model.data.translations ?? {}).flatMap(([locale, translations]) => owners.flatMap((owner) => Object.entries(ownValue(translations.content, owner.id) ?? {}).map(([role, content]) => ({
103
+ locale,
104
+ ownerId: owner.id,
105
+ role,
106
+ text: getContentPlainText(content).slice(0, 120)
107
+ })))).slice(0, 40);
108
+ return {
109
+ itemId,
110
+ itemType: item.itemType,
111
+ editorName: name(item.id),
112
+ breadcrumb: getNamePath(itemId).join(" / "),
113
+ itemLabel: item.metadata?.itemLabel,
114
+ siblingNames: rawItems.filter((candidate) => candidate.id !== itemId && parentById.get(candidate.id) === parentById.get(itemId)).map((candidate) => name(candidate.id)),
115
+ translations: translationSnippets,
116
+ elements: [...model.elements.values()].filter((element) => element.itemId === itemId).map((element) => ({
117
+ elementId: element.node.id,
118
+ elementType: element.node.elementType,
119
+ owners: owners.filter((owner) => owner.elementId === element.node.id)
120
+ })),
121
+ descendants: getChildIds(item).filter((childId) => itemsById.has(childId) && budget.remaining > 0).map((childId) => buildFocusNode(childId, budget))
122
+ };
123
+ };
124
+ const focusItemId = requestedFocusItemId ?? snapshot.ui.focusItemId ?? snapshot.ui.selectedItemId;
125
+ let focus;
126
+ let focusItem;
127
+ let focusItemTreeTruncated = false;
128
+ if (focusItemId && itemsById.has(focusItemId)) {
129
+ const pathItemIds = [];
130
+ let currentId = focusItemId;
131
+ const pathSeen = /* @__PURE__ */ new Set();
132
+ while (currentId && !pathSeen.has(currentId)) {
133
+ pathSeen.add(currentId);
134
+ pathItemIds.unshift(currentId);
135
+ currentId = parentById.get(currentId);
136
+ }
137
+ const parentItemId = parentById.get(focusItemId);
138
+ focus = {
139
+ focusItemId,
140
+ parentItemId,
141
+ pathItemIds,
142
+ siblingItemIds: rawItems.filter((candidate) => candidate.id !== focusItemId && parentById.get(candidate.id) === parentItemId).map((candidate) => candidate.id),
143
+ childItemIds: getChildIds(itemsById.get(focusItemId))
144
+ };
145
+ const budget = { remaining: 120 };
146
+ focusItem = buildFocusNode(focusItemId, budget);
147
+ focusItemTreeTruncated = budget.remaining === 0;
148
+ }
149
+ const responseSlotTypes = Object.fromEntries(model.slots.definitions.map((slot) => [slot.slotId, slot.valueType]));
150
+ const locales = [snapshot.ui.selectedLocale, ...Object.keys(snapshot.survey.translations ?? {}).filter((locale) => locale !== snapshot.ui.selectedLocale)].filter((locale, index, values) => locale.length > 0 && values.indexOf(locale) === index);
151
+ return {
152
+ inspection: {
153
+ surveyKey: snapshot.survey.metadata?.surveyKey,
154
+ locales,
155
+ itemCount: rawItems.filter((item) => item.id !== explicitRoot?.id).length,
156
+ outline,
157
+ focus,
158
+ focusItem,
159
+ itemIndex,
160
+ responseSlots: model.slots.definitions.map((slot) => slot.slotId),
161
+ focusItemTreeTruncated
162
+ },
163
+ model,
164
+ responseSlotTypes,
165
+ responseVariables: model.slots.definitions
166
+ };
167
+ };
168
+ const createSurveyAssistantEngineContext = (document, trustedCapabilities, options) => {
169
+ assertSurveyAssistantCapabilities({
170
+ manifest: document.capabilities,
171
+ trustedCapabilities,
172
+ versionDigest: document.version.capabilityDigest
173
+ });
174
+ const { inspection, model, responseSlotTypes, responseVariables } = buildDerivedContext(document, trustedCapabilities, options?.focusItemId);
175
+ return {
176
+ document,
177
+ version: document.version,
178
+ survey: model.serialize(),
179
+ model,
180
+ ui: document.ui,
181
+ capabilities: document.capabilities,
182
+ responseSlotTypes,
183
+ responseVariables,
184
+ inspection,
185
+ trustedCapabilities
186
+ };
187
+ };
188
+ //#endregion
189
+ //#region src/engine/expression-validation.ts
190
+ const validateExpression = ({ expression, expectedReturnType, issues, operationIndex, responseSlotTypes, stateTargets, scope }) => {
191
+ const report = (code, message) => issues.push({
192
+ severity: "error",
193
+ code,
194
+ message: `${scope}: ${message}`,
195
+ operationIndex
196
+ });
197
+ if (!expression) {
198
+ report("missing-expression", "An expression is required.");
199
+ return;
200
+ }
201
+ const parsed = expressionSchema.safeParse(expression);
202
+ if (!parsed.success) {
203
+ report("invalid-expression", `Invalid expression JSON: ${parsed.error.message}`);
204
+ return;
205
+ }
206
+ for (const dependency of collectExpressionDependencies(parsed.data)) switch (dependency.kind) {
207
+ case "answer":
208
+ if (!Object.hasOwn(responseSlotTypes, dependency.slotId)) report("unknown-response-ref", `Unknown response slot "${dependency.slotId}". Inspect response slots before using it.`);
209
+ break;
210
+ case "state":
211
+ if (!stateTargets.has(dependency.targetRef.ownerId)) report("unknown-state-ref", `Unknown disabled-state owner "${dependency.targetRef.ownerId}". Inspect structural owners before using it.`);
212
+ break;
213
+ default: unreachable(dependency);
214
+ }
215
+ for (const issue of expressionIssues(parsed.data)) report("expression-function-signature-mismatch", `${issue.message} at ${JSON.stringify(issue.path)}.`);
216
+ transformExpression(parsed.data, {
217
+ const: (node) => node,
218
+ responseVariable: (node) => node,
219
+ ownerState: (node) => node,
220
+ contextVariable: (node) => node,
221
+ function: (node, path) => {
222
+ functionTypeIssues(node, responseSlotTypes).forEach((message) => report("expression-function-signature-mismatch", `${message} at ${JSON.stringify(path)}.`));
223
+ return node;
224
+ }
225
+ });
226
+ const inferred = inferExpressionReturnType(parsed.data, responseSlotTypes);
227
+ if (expectedReturnType && inferred && inferred !== expectedReturnType) report("expression-return-type-mismatch", `Must return ${expectedReturnType}, but this expression returns ${inferred}.`);
228
+ };
229
+ //#endregion
230
+ //#region src/engine/definition-diagnostics.ts
231
+ const getDiagnosticLocation = (survey, path, ownerId) => {
232
+ const owner = ownerId ? survey.owners.get(ownerId) : path ? ownerAtPath(survey, path) : void 0;
233
+ return owner && path && owner.path.every((part, index) => part === path[index]) ? {
234
+ ownerId: owner.id,
235
+ path: path.slice(owner.path.length)
236
+ } : { path };
237
+ };
238
+ const getIntroducedDefinitionIssues = (before, after, locale) => {
239
+ const key = (survey, issue) => stableStringify([
240
+ issue.code,
241
+ getDiagnosticLocation(survey, issue.path, issue.ownerId),
242
+ issue.message
243
+ ]);
244
+ const prior = new Set(getDefinitionDiagnostics(before, locale).map((issue) => key(before, issue)));
245
+ return getDefinitionDiagnostics(after, locale).filter((issue) => !prior.has(key(after, issue))).map((issue) => ({
246
+ severity: issue.severity,
247
+ code: issue.code,
248
+ message: issue.kind === "reference" ? `${JSON.stringify(issue.path)}: ${issue.message}. Repair or remove this reference in the same change.` : issue.message,
249
+ itemId: issue.ownerItemId
250
+ }));
251
+ };
252
+ //#endregion
253
+ //#region src/engine/expression-reference-integrity.ts
254
+ /** Includes the same answer/state/dependency/quality checks the editor presents. */
255
+ const analyzeSurveyAssistantExpressionReferences = (survey, trustedCapabilities, locale) => {
256
+ try {
257
+ return getDefinitionDiagnostics(createAssistantSurvey(survey, trustedCapabilities), locale);
258
+ } catch (error) {
259
+ if (error instanceof DefinitionError) return [...error.diagnostics];
260
+ return [{
261
+ severity: "error",
262
+ code: "invalid-survey-schema",
263
+ path: [],
264
+ message: error instanceof Error ? error.message : String(error)
265
+ }];
266
+ }
267
+ };
268
+ const getSurveyAssistantExpressionDiagnosticGuidance = (diagnostic) => {
269
+ if (diagnostic.kind === "reference") return "This reference points at content that no longer exists, usually after the author deleted or changed it. Inspect the owning item, current response slots and options, then retarget it to the intended replacement or remove the dependent condition, rule, prefill or setting if it no longer applies. Do not guess a replacement from spelling.";
270
+ if (diagnostic.code.includes("cycle")) return "Break the answer/state dependency cycle using an independent controlling answer or recorded context. A child inherits its ancestors' disabled state; prefills do not break cycles.";
271
+ if (diagnostic.code === "external-checkbox-lock") return "Use visibility for answers that stop applying. Disabled checkboxes keep selected values and cannot be unticked. Use selected-and-enabled only when inherited locking should also exclude the option from this expression.";
272
+ return diagnostic.severity === "error" ? "Repair this located definition error using current owner and slot IDs before claiming success." : "Review this author warning in the selected language; preserve intentional survey logic.";
273
+ };
274
+ //#endregion
275
+ //#region src/engine/expression-mutations.ts
276
+ const id = z.string().min(1);
277
+ const expressionMutationSchema = z.discriminatedUnion("kind", [
278
+ z.strictObject({
279
+ kind: z.literal("set-condition"),
280
+ ownerId: id,
281
+ property: z.enum(["visibility", "disabled"]),
282
+ expression: expressionSchema
283
+ }),
284
+ z.strictObject({
285
+ kind: z.literal("clear-condition"),
286
+ ownerId: id,
287
+ property: z.enum(["visibility", "disabled"])
288
+ }),
289
+ z.strictObject({
290
+ kind: z.literal("set-config-expression"),
291
+ elementId: id,
292
+ path: z.string().startsWith("/"),
293
+ expression: expressionSchema
294
+ }),
295
+ z.strictObject({
296
+ kind: z.literal("clear-config-expression"),
297
+ elementId: id,
298
+ path: z.string().startsWith("/")
299
+ }),
300
+ z.strictObject({
301
+ kind: z.literal("set-validation-rule"),
302
+ itemId: id,
303
+ rule: validationRuleSchema
304
+ }),
305
+ z.strictObject({
306
+ kind: z.literal("remove-validation-rule"),
307
+ itemId: id,
308
+ ruleId: id
309
+ }),
310
+ z.strictObject({
311
+ kind: z.literal("set-prefill"),
312
+ elementId: id,
313
+ slotId: id,
314
+ prefill: slotPrefillSchema
315
+ }),
316
+ z.strictObject({
317
+ kind: z.literal("clear-prefill"),
318
+ elementId: id,
319
+ slotId: id,
320
+ prefillId: id
321
+ }),
322
+ z.strictObject({
323
+ kind: z.literal("set-template-value"),
324
+ templateKey: id,
325
+ templateValue: templateValueSchema
326
+ }),
327
+ z.strictObject({
328
+ kind: z.literal("clear-template-value"),
329
+ templateKey: id
330
+ })
331
+ ]);
332
+ const pointer$1 = (path) => path.length ? `/${path.map((part) => encodeJsonPointerSegment(String(part))).join("/")}` : "";
333
+ const readAt = (root, path) => path.reduce((value, part) => value && typeof value === "object" ? value[part] : void 0, root);
334
+ const isLiteral = (value) => typeof value === "number" || typeof value === "string";
335
+ /** Compile against the exact trusted registry and one detached accepted revision.
336
+ * Conditions are located through annotated owners, including custom components.
337
+ * Only the final batch graph is compiled, so a multi-step cycle repair is atomic.
338
+ * The turn draft and normal editor apply path remain responsible for publishing.
339
+ */
340
+ function buildExpressionMutationBatch(mutations, definition) {
341
+ const issues = [];
342
+ const patches = [];
343
+ let working = definition;
344
+ for (const [operationIndex, input] of mutations.entries()) {
345
+ const pending = [];
346
+ const report = (code, message, severity = "error") => issues.push({
347
+ severity,
348
+ code,
349
+ message,
350
+ operationIndex
351
+ });
352
+ const put = (path, value) => pending.push({
353
+ op: "add",
354
+ path: pointer$1(path),
355
+ value: jsonValueSchema.parse(value)
356
+ });
357
+ const remove = (path, exists) => exists ? pending.push({
358
+ op: "remove",
359
+ path: pointer$1(path)
360
+ }) : report("expression-location-already-empty", `${pointer$1(path)} is already empty.`, "warning");
361
+ const check = (expression, scope, expectedReturnType) => validateExpression({
362
+ expression,
363
+ scope,
364
+ expectedReturnType,
365
+ issues,
366
+ operationIndex,
367
+ responseSlotTypes: Object.fromEntries(working.slots.definitions.map((slot) => [slot.slotId, slot.valueType])),
368
+ stateTargets: new Set(working.owners.keys())
369
+ });
370
+ try {
371
+ const mutation = expressionMutationSchema.parse(input);
372
+ switch (mutation.kind) {
373
+ case "set-config-expression":
374
+ case "clear-config-expression": {
375
+ const element = working.elements.get(mutation.elementId);
376
+ if (!element) throw new Error(`Unknown element "${mutation.elementId}".`);
377
+ const path = [
378
+ ...element.path,
379
+ "config",
380
+ ...parseJsonPointer(mutation.path)
381
+ ];
382
+ const location = pointer$1(path);
383
+ const boundPath = path.at(-1) === "expression" ? path.slice(0, -1) : void 0;
384
+ const bound = boundPath && readAt(working.serialize(), boundPath);
385
+ const declaredBound = !!boundPath && inspectAnnotated(working.registry.elementSchema, element.node).some((visit) => visit.annotation.kind === "bound" && pointer$1([...element.path, ...visit.path]) === pointer$1(boundPath));
386
+ if (mutation.kind === "set-config-expression") {
387
+ check(mutation.expression, `element ${mutation.elementId} config ${mutation.path}`);
388
+ if (boundPath && (bound === void 0 || declaredBound && isLiteral(bound))) put(boundPath, {
389
+ expression: mutation.expression,
390
+ ...bound === void 0 ? {} : { fallback: bound }
391
+ });
392
+ else put(path, mutation.expression);
393
+ if (!Survey.parse(applyJsonPatch(working.serialize(), pending), working.registry, { admission: "draft" }).expressions.some((entry) => pointer$1(entry.path) === location)) throw new Error(`Config path "${mutation.path}" is not a registered expression field.`);
394
+ } else {
395
+ if (!working.expressions.some((entry) => pointer$1(entry.path) === location)) throw new Error(`Config path "${mutation.path}" does not contain a registered expression field.`);
396
+ const fallback = declaredBound ? bound.fallback : void 0;
397
+ if (boundPath && declaredBound) if (isLiteral(fallback)) put(boundPath, fallback);
398
+ else remove(boundPath, true);
399
+ else remove(path, true);
400
+ }
401
+ break;
402
+ }
403
+ case "set-condition":
404
+ case "clear-condition": {
405
+ const owner = working.owners.get(mutation.ownerId);
406
+ if (!owner) throw new Error(`Unknown structural owner "${mutation.ownerId}". Inspect owners before editing a condition.`);
407
+ const path = [...owner.path, mutation.property];
408
+ if (mutation.kind === "set-condition") {
409
+ check(mutation.expression, `${mutation.property} of ${mutation.ownerId}`, "boolean");
410
+ put(path, mutation.expression);
411
+ } else remove(path, owner[mutation.property] !== void 0);
412
+ break;
413
+ }
414
+ case "set-validation-rule":
415
+ case "remove-validation-rule": {
416
+ const item = working.flowItems.get(mutation.itemId);
417
+ const owner = working.owners.get(mutation.itemId);
418
+ if (!item || item.itemType !== "content" || !owner) throw new Error(`Validation rules require a content item: "${mutation.itemId}".`);
419
+ const path = [
420
+ ...owner.path,
421
+ "config",
422
+ "validationRules"
423
+ ];
424
+ const rules = [...item.config.validationRules ?? []];
425
+ const ruleId = mutation.kind === "set-validation-rule" ? mutation.rule.id : mutation.ruleId;
426
+ const index = rules.findIndex((rule) => rule.id === ruleId);
427
+ if (mutation.kind === "set-validation-rule") {
428
+ check(mutation.rule.expression, `validation ${ruleId}`, "boolean");
429
+ if (mutation.rule.when) check(mutation.rule.when, `validation ${ruleId} condition`, "boolean");
430
+ if (index < 0) rules.push(mutation.rule);
431
+ else rules[index] = mutation.rule;
432
+ put(path, rules);
433
+ } else if (index >= 0) {
434
+ remove([...path, index], true);
435
+ for (const [locale, translations] of Object.entries(working.data.translations ?? {})) if (Object.hasOwn(translations.content, ruleId)) remove([
436
+ "translations",
437
+ locale,
438
+ "content",
439
+ ruleId
440
+ ], true);
441
+ } else report("expression-location-already-empty", `Validation "${ruleId}" is already absent.`, "warning");
442
+ break;
443
+ }
444
+ case "set-prefill":
445
+ case "clear-prefill": {
446
+ const element = working.elements.get(mutation.elementId);
447
+ const slot = working.slots.get(mutation.slotId);
448
+ if (!element || !slot || slot.elementId !== mutation.elementId || slot.computed) throw new Error(`Prefill target "${mutation.slotId}" must be a stored slot owned by element "${mutation.elementId}".`);
449
+ const prefills = structuredClone(element.node.prefills ?? {});
450
+ const entries = ownValue(prefills, mutation.slotId) ?? [];
451
+ const prefillId = mutation.kind === "set-prefill" ? mutation.prefill.id : mutation.prefillId;
452
+ const index = entries.findIndex((prefill) => prefill.id === prefillId);
453
+ if (mutation.kind === "set-prefill") {
454
+ if (mutation.prefill.when) check(mutation.prefill.when, `prefill ${prefillId} condition`, "boolean");
455
+ if (mutation.prefill.source.type === "expression") check(mutation.prefill.source.expression, `prefill ${prefillId} value`, slot.valueType);
456
+ if (index < 0) entries.push(mutation.prefill);
457
+ else entries[index] = mutation.prefill;
458
+ } else if (index >= 0) entries.splice(index, 1);
459
+ else report("expression-location-already-empty", `Prefill "${prefillId}" is already absent.`, "warning");
460
+ if (entries.length) setOwnValue(prefills, mutation.slotId, entries);
461
+ else delete prefills[mutation.slotId];
462
+ put([...element.path, "prefills"], prefills);
463
+ break;
464
+ }
465
+ case "set-template-value":
466
+ case "clear-template-value":
467
+ if (mutation.kind === "set-template-value") {
468
+ if (mutation.templateValue.expression) check(mutation.templateValue.expression, `template ${mutation.templateKey}`, mutation.templateValue.type === "formatDate" ? "date" : mutation.templateValue.returnType);
469
+ put(["templateValues"], {
470
+ ...working.data.templateValues,
471
+ [mutation.templateKey]: mutation.templateValue
472
+ });
473
+ } else remove(["templateValues", mutation.templateKey], Object.hasOwn(working.data.templateValues ?? {}, mutation.templateKey));
474
+ break;
475
+ default: unreachable(mutation);
476
+ }
477
+ if (issues.some((issue) => issue.severity === "error")) return {
478
+ issues,
479
+ patches: []
480
+ };
481
+ working = Survey.parse(applyJsonPatch(working.serialize(), pending), definition.registry, { admission: "draft" });
482
+ patches.push(...pending);
483
+ } catch (error) {
484
+ report("expression-mutation-transition-failed", error instanceof Error ? error.message : String(error));
485
+ return {
486
+ issues,
487
+ patches: []
488
+ };
489
+ }
490
+ }
491
+ try {
492
+ new SurveyStateGraph(working, { admission: "draft" });
493
+ } catch (error) {
494
+ issues.push({
495
+ severity: "error",
496
+ code: "invalid-expression-graph",
497
+ message: error instanceof Error ? error.message : String(error)
498
+ });
499
+ return {
500
+ issues,
501
+ patches: []
502
+ };
503
+ }
504
+ return {
505
+ issues,
506
+ patches
507
+ };
508
+ }
509
+ //#endregion
510
+ //#region src/engine/inspection.ts
511
+ const DEFAULT_LIMIT = 12;
512
+ const MAX_LIMIT = 50;
513
+ const itemSummarySchema = z.object({
514
+ itemId: z.string(),
515
+ itemType: z.string(),
516
+ editorName: z.string(),
517
+ breadcrumb: z.string(),
518
+ itemLabel: z.string().optional(),
519
+ parentId: z.string().optional(),
520
+ depth: z.number().int().nonnegative().optional(),
521
+ childCount: z.number().int().nonnegative().optional(),
522
+ path: z.array(z.string()).optional(),
523
+ siblingNames: z.array(z.string()).optional()
524
+ });
525
+ const translationEntrySchema = z.object({
526
+ locale: z.string(),
527
+ ownerId: z.string(),
528
+ role: z.string(),
529
+ text: z.string()
530
+ });
531
+ const schemaPath = z.array(z.union([z.string(), z.number()]));
532
+ const contentOwnerDetailsSchema = z.object({
533
+ ownerId: z.string(),
534
+ kind: z.string(),
535
+ position: z.number().int().positive().optional(),
536
+ parentId: z.string().optional(),
537
+ elementId: z.string().optional(),
538
+ path: schemaPath,
539
+ roles: z.array(z.object({
540
+ role: z.string(),
541
+ policy: z.enum(["inline", "block"]),
542
+ recommended: z.boolean().optional()
543
+ })),
544
+ visibility: z.boolean(),
545
+ disabled: z.boolean(),
546
+ followUpTriggerId: z.string().optional()
547
+ });
548
+ const elementDetailsSchema = z.object({
549
+ elementId: z.string(),
550
+ elementType: z.string(),
551
+ position: z.number().int().positive().optional(),
552
+ path: schemaPath,
553
+ configPath: z.string(),
554
+ config: jsonValueSchema,
555
+ responseSlotIds: z.array(z.string()),
556
+ owners: z.array(contentOwnerDetailsSchema)
557
+ });
558
+ const itemConfigDetailsSchema = z.object({
559
+ kind: z.enum([
560
+ "content",
561
+ "group",
562
+ "page-break"
563
+ ]),
564
+ summary: z.array(z.string()),
565
+ listOrdering: stringRecord(z.string(), z.string()),
566
+ elements: z.array(elementDetailsSchema),
567
+ owners: z.array(contentOwnerDetailsSchema),
568
+ bodyLocations: z.array(z.object({
569
+ ownerId: z.string(),
570
+ path: schemaPath,
571
+ followUp: z.boolean()
572
+ }))
573
+ });
574
+ const normalizeText = (value) => value?.trim().toLowerCase() ?? "";
575
+ const clampLimit = (limit) => Math.min(50, Math.max(1, limit ?? 12));
576
+ const parseCursor = (cursor) => {
577
+ if (!cursor) return 0;
578
+ const parsed = Number.parseInt(cursor, 10);
579
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
580
+ };
581
+ const unique = (values) => Array.from(new Set(values));
582
+ const sliceWithCursor = (values, input) => {
583
+ const offset = parseCursor(input.cursor);
584
+ const nextOffset = offset + clampLimit(input.limit);
585
+ return {
586
+ items: values.slice(offset, nextOffset),
587
+ nextCursor: nextOffset < values.length ? String(nextOffset) : void 0,
588
+ truncated: nextOffset < values.length
589
+ };
590
+ };
591
+ const mapOutlineById = (snapshot) => new Map(snapshot.inspection.outline.map((item) => [item.itemId, item]));
592
+ const mapItemIndexById = (snapshot) => new Map(snapshot.inspection.itemIndex.map((item) => [item.itemId, item]));
593
+ /** The full-survey item index, regardless of the (possibly focus-scoped) outline. */
594
+ const getItemIndexEntries = (snapshot) => snapshot.inspection.itemIndex;
595
+ const getRawSurvey = (snapshot) => snapshot.survey;
596
+ const getRawSurveyItems = (snapshot) => snapshot.survey.surveyItems;
597
+ const mapRawItemsById = (snapshot) => new Map(getRawSurveyItems(snapshot).map((item) => [item.id, item]));
598
+ const summarizeRawItem = (item) => ({
599
+ itemId: item.id,
600
+ itemType: item.itemType,
601
+ editorName: item.metadata?.itemLabel?.trim() || item.id,
602
+ breadcrumb: item.id,
603
+ itemLabel: item.metadata?.itemLabel
604
+ });
605
+ const buildItemTranslationEntries = (snapshot, itemId, input) => {
606
+ const localeFilter = input.locales?.length ? new Set(input.locales) : void 0;
607
+ const textLimit = input.textLimit ?? 400;
608
+ const entries = [];
609
+ const owners = [...snapshot.model.owners.values()].filter((owner) => owner.itemId === itemId);
610
+ for (const [locale, translations] of Object.entries(snapshot.model.data.translations ?? {})) {
611
+ if (localeFilter && !localeFilter.has(locale)) continue;
612
+ for (const owner of owners) for (const [role, content] of Object.entries(ownValue(translations.content, owner.id) ?? {})) {
613
+ const text = getContentPlainText(content).trim();
614
+ if (text) entries.push({
615
+ locale,
616
+ ownerId: owner.id,
617
+ role,
618
+ text: text.length > textLimit ? `${text.slice(0, textLimit)}...` : text
619
+ });
620
+ }
621
+ }
622
+ return entries;
623
+ };
624
+ const summarizeOutlineNode = (node) => ({
625
+ itemId: node.itemId,
626
+ itemType: node.itemType,
627
+ editorName: node.editorName,
628
+ breadcrumb: node.breadcrumb,
629
+ itemLabel: node.itemLabel,
630
+ parentId: node.parentId,
631
+ depth: node.depth,
632
+ childCount: node.childCount
633
+ });
634
+ const summarizeItemIndexEntry = (entry, outlineById) => {
635
+ const outlineNode = outlineById.get(entry.itemId);
636
+ return {
637
+ itemId: entry.itemId,
638
+ itemType: entry.itemType,
639
+ editorName: entry.editorName,
640
+ breadcrumb: entry.breadcrumb,
641
+ itemLabel: entry.itemLabel,
642
+ parentId: outlineNode?.parentId,
643
+ depth: outlineNode?.depth,
644
+ childCount: outlineNode?.childCount,
645
+ path: entry.path
646
+ };
647
+ };
648
+ const summarizeItemIndexEntries = (entries, outlineById) => entries.map((entry) => summarizeItemIndexEntry(entry, outlineById));
649
+ /** Components and follow-ups come from the registry's declared ownership, not type-name scanners. */
650
+ const buildItemConfigDetails = (item, model) => {
651
+ const owners = [...model.owners.values()].filter((owner) => owner.itemId === item.id).map((owner) => ({
652
+ ownerId: owner.id,
653
+ kind: owner.kind,
654
+ position: owner.kind !== "item" && typeof owner.path.at(-1) === "number" ? Number(owner.path.at(-1)) + 1 : void 0,
655
+ parentId: owner.parentId,
656
+ elementId: owner.elementId,
657
+ path: [...owner.path],
658
+ roles: [...owner.roles],
659
+ visibility: owner.visibility !== void 0,
660
+ disabled: owner.disabled !== void 0,
661
+ followUpTriggerId: owner.followUpTriggerId
662
+ }));
663
+ const elements = [...model.elements.values()].filter((element) => element.itemId === item.id).map((element) => ({
664
+ elementId: element.node.id,
665
+ elementType: element.node.elementType,
666
+ position: typeof element.path.at(-1) === "number" ? Number(element.path.at(-1)) + 1 : void 0,
667
+ path: [...element.path],
668
+ configPath: `/${[...element.path, "config"].map((part) => encodeJsonPointerSegment(String(part))).join("/")}`,
669
+ config: jsonValueSchema.parse(element.node.config),
670
+ responseSlotIds: element.facts.slots.map((slot) => slot.slotId),
671
+ owners: owners.filter((owner) => owner.elementId === element.node.id)
672
+ }));
673
+ return {
674
+ kind: item.itemType,
675
+ listOrdering: {
676
+ elements: "Authored depth-first traversal, including follow-ups; position is one-based within each element's own layout, not this flattened list.",
677
+ owners: "Declared content-owner traversal. A position is one-based within the array identified by path; consult the element's shuffle configuration before interpreting it as respondent-visible order.",
678
+ bodyLocations: "Authored body traversal; follow-ups stay directly under their triggering owner, not after the entire element."
679
+ },
680
+ summary: item.itemType === "content" ? [`${elements.length} elements in vertically ordered layouts and sections`] : item.itemType === "group" ? [`${item.config.items.length} flow children`] : ["Page break"],
681
+ elements,
682
+ owners,
683
+ bodyLocations: [...model.bodies.values()].filter((body) => model.owners.get(body.ownerId)?.itemId === item.id).map((body) => ({
684
+ ...body,
685
+ path: [...body.path]
686
+ }))
687
+ };
688
+ };
689
+ const buildConfigSearchText = (item, model) => {
690
+ const details = buildItemConfigDetails(item, model);
691
+ const values = [...details.summary, ...details.elements.flatMap((element) => [
692
+ element.elementId,
693
+ element.elementType,
694
+ JSON.stringify(element.config)
695
+ ])];
696
+ for (const slot of model.slots.definitions.filter((slot) => slot.itemId === item.id)) values.push(slot.slotId, slot.variableName, ...Object.values(slot.referenceValueKeys ?? {}));
697
+ const itemPath = model.owners.get(item.id)?.path;
698
+ if (itemPath) {
699
+ for (const entry of model.expressions) if (itemPath.every((part, index) => entry.path[index] === part)) for (const reference of collectExpressionDependencies(entry.expression)) values.push(reference.kind === "answer" ? reference.slotId : reference.targetRef.ownerId);
700
+ }
701
+ return values;
702
+ };
703
+ const summarizeFocusItemNode = (node, outlineById) => {
704
+ const outlineNode = outlineById.get(node.itemId);
705
+ return {
706
+ itemId: node.itemId,
707
+ itemType: node.itemType,
708
+ editorName: node.editorName,
709
+ breadcrumb: node.breadcrumb,
710
+ itemLabel: node.itemLabel,
711
+ parentId: outlineNode?.parentId,
712
+ depth: outlineNode?.depth,
713
+ childCount: outlineNode?.childCount ?? node.descendants.length,
714
+ siblingNames: node.siblingNames
715
+ };
716
+ };
717
+ const summarizeItemById = (itemId, indexById, outlineById) => {
718
+ const outlineNode = outlineById.get(itemId);
719
+ if (outlineNode) return summarizeOutlineNode(outlineNode);
720
+ const indexEntry = indexById.get(itemId);
721
+ if (indexEntry) return summarizeItemIndexEntry(indexEntry, outlineById);
722
+ };
723
+ /** Resolve only persistent item IDs. Search can discover names and breadcrumbs. */
724
+ const resolveItemSummary = (identifier, indexById, outlineById) => {
725
+ return summarizeItemById(identifier, indexById, outlineById);
726
+ };
727
+ const itemMatchesQuery = (item, query) => {
728
+ return [
729
+ item.itemId,
730
+ item.itemType,
731
+ item.editorName,
732
+ item.breadcrumb,
733
+ item.itemLabel,
734
+ ...item.path ?? [],
735
+ ...item.siblingNames ?? []
736
+ ].filter(Boolean).join(" ").toLowerCase().includes(query);
737
+ };
738
+ /**
739
+ * Direct children of `parentId` derived from the full-survey item index.
740
+ * This works for any item, selected or not.
741
+ */
742
+ const findDirectChildrenByParentId = (entries, outlineById, parentId) => {
743
+ return entries.filter((entry) => outlineById.get(entry.itemId)?.parentId === parentId).map((entry) => summarizeItemIndexEntry(entry, outlineById));
744
+ };
745
+ //#endregion
746
+ //#region src/engine/normalization.ts
747
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
748
+ const getString = (value) => typeof value === "string" ? value.trim() : void 0;
749
+ const textInline = (text, style) => ({
750
+ type: "text",
751
+ text,
752
+ ...style ? { style } : {}
753
+ });
754
+ const paragraphBlock = (text, style) => ({
755
+ type: "paragraph",
756
+ children: [textInline(text, style)]
757
+ });
758
+ const parseCanonicalRichTextBlocks = (blocks) => {
759
+ const parsed = surveyContentSchema.safeParse({
760
+ type: ContentType.richText,
761
+ version: 1,
762
+ doc: {
763
+ type: "doc",
764
+ blocks
765
+ }
766
+ });
767
+ return parsed.success && parsed.data.type === ContentType.richText && stableStringify(parsed.data.doc.blocks) === stableStringify(blocks) ? parsed.data.doc.blocks : void 0;
768
+ };
769
+ const getBlockText = (block) => [
770
+ block.text,
771
+ block.content,
772
+ block.label
773
+ ].find((value) => typeof value === "string");
774
+ const getShorthandHeadingLevel = (value) => {
775
+ if (value.level !== void 0) return value.level;
776
+ const typeLevel = getString(value.type)?.toLowerCase().match(/^h([1-6])$/)?.[1];
777
+ return typeLevel ? Number(typeLevel) : 2;
778
+ };
779
+ const normalizeRichTextStyle = (value) => {
780
+ const rawStyle = isRecord$2(value.style) ? value.style : {};
781
+ const style = {};
782
+ const weight = rawStyle.weight ?? value.weight;
783
+ const italic = rawStyle.italic ?? value.italic;
784
+ const underline = rawStyle.underline ?? value.underline;
785
+ const color = rawStyle.color ?? value.color;
786
+ if (value.strong === true || value.bold === true || weight === "strong" || weight === "bold") style.weight = "strong";
787
+ else if (weight === "normal") style.weight = "normal";
788
+ if (value.italic === true || italic === "italic") style.italic = "italic";
789
+ else if (italic === "normal") style.italic = "normal";
790
+ if (value.underline === true || underline === "underline") style.underline = "underline";
791
+ else if (underline === "none") style.underline = "none";
792
+ if (color === "primary" || color === "accent" || color === "default") style.color = color;
793
+ return Object.keys(style).length > 0 ? style : void 0;
794
+ };
795
+ const richTextShorthandNodeTypes = {
796
+ "bullet-list": "bulletList",
797
+ bulletlist: "bulletList",
798
+ list: "bulletList",
799
+ bullets: "bulletList",
800
+ infobox: "infoBox",
801
+ "info-box": "infoBox",
802
+ note: "infoBox",
803
+ footnote: "infoBox",
804
+ divider: "separator",
805
+ rule: "separator",
806
+ linebreak: "lineBreak",
807
+ "line-break": "lineBreak",
808
+ break: "lineBreak",
809
+ listitem: "listItem",
810
+ inlineextension: "inlineExtension",
811
+ blockextension: "blockExtension"
812
+ };
813
+ const resolveRichTextShorthandNodeType = (value) => {
814
+ const type = getString(value)?.toLowerCase();
815
+ return type ? ownValue(richTextShorthandNodeTypes, type) ?? (/^h[1-6]$/.test(type) ? "heading" : type) : void 0;
816
+ };
817
+ const normalizeRichTextInline = (value, allowLink = true) => {
818
+ if (typeof value === "string") return textInline(value);
819
+ if (!isRecord$2(value)) return value;
820
+ const type = resolveRichTextShorthandNodeType(value.type);
821
+ const canonicalParagraph = parseCanonicalRichTextBlocks([{
822
+ type: "paragraph",
823
+ children: [value]
824
+ }])?.[0];
825
+ const canonicalInline = canonicalParagraph?.type === "paragraph" ? canonicalParagraph.children[0] : void 0;
826
+ if (canonicalInline && (allowLink || canonicalInline.type !== "link")) return canonicalInline;
827
+ if (type === "lineBreak") return { type: "lineBreak" };
828
+ if (type === "link" && allowLink) {
829
+ const href = getString(value.href) ?? getString(value.url);
830
+ if (!href) return;
831
+ const childValues = value.children === void 0 ? [getBlockText(value) ?? href] : value.children;
832
+ const children = Array.isArray(childValues) ? childValues.map((child) => normalizeRichTextInline(child, false)) : childValues;
833
+ return {
834
+ ...value,
835
+ type: "link",
836
+ href,
837
+ children
838
+ };
839
+ }
840
+ if (type && type !== "text") return value;
841
+ const text = getBlockText(value);
842
+ return text === void 0 ? value : textInline(text, normalizeRichTextStyle(value));
843
+ };
844
+ const normalizeRichTextChildren = (value) => {
845
+ if (!Array.isArray(value.children)) return;
846
+ return value.children.map((child) => normalizeRichTextInline(child));
847
+ };
848
+ const normalizeRichTextParagraph = (value) => typeof value === "string" || isRecord$2(value) && (!value.type || getString(value.type)?.toLowerCase() === "paragraph") ? normalizeRichTextBlock(value) : value;
849
+ const normalizeRichTextBlock = (value) => {
850
+ if (typeof value === "string") return paragraphBlock(value);
851
+ if (!isRecord$2(value)) return value;
852
+ const canonicalBlock = parseCanonicalRichTextBlocks([value])?.[0];
853
+ if (canonicalBlock) return canonicalBlock;
854
+ const type = resolveRichTextShorthandNodeType(value.type);
855
+ const text = getBlockText(value);
856
+ const style = normalizeRichTextStyle(value);
857
+ const children = normalizeRichTextChildren(value);
858
+ if (type === "heading") return {
859
+ ...value,
860
+ type: "heading",
861
+ level: getShorthandHeadingLevel(value),
862
+ children: value.children !== void 0 ? children ?? value.children : text !== void 0 ? [textInline(text, style)] : void 0
863
+ };
864
+ if (type === "bulletList") {
865
+ if (!Array.isArray(value.items)) return value;
866
+ return {
867
+ ...value,
868
+ type: "bulletList",
869
+ items: value.items.map((item) => {
870
+ const itemText = typeof item === "string" ? item : isRecord$2(item) ? getBlockText(item) : void 0;
871
+ if (isRecord$2(item) && Array.isArray(item.children)) return {
872
+ type: "listItem",
873
+ ...item,
874
+ children: item.children.map(normalizeRichTextParagraph)
875
+ };
876
+ return itemText !== void 0 ? {
877
+ type: "listItem",
878
+ ...isRecord$2(item) ? item : {},
879
+ children: [paragraphBlock(itemText)]
880
+ } : item;
881
+ })
882
+ };
883
+ }
884
+ if (type === "infoBox") return {
885
+ ...value,
886
+ type: "infoBox",
887
+ title: typeof value.title === "string" ? [textInline(value.title)] : Array.isArray(value.title) ? value.title.map((inline) => normalizeRichTextInline(inline)) : value.title,
888
+ children: Array.isArray(value.children) ? value.children.map(normalizeRichTextParagraph) : value.children !== void 0 ? value.children : text !== void 0 ? [paragraphBlock(text)] : void 0
889
+ };
890
+ if (type === "image") return {
891
+ ...value,
892
+ type: "image",
893
+ caption: Array.isArray(value.caption) ? value.caption.map(normalizeRichTextParagraph) : value.caption
894
+ };
895
+ if (type === "separator") return {
896
+ ...value,
897
+ type: "separator"
898
+ };
899
+ if (type === "paragraph" || !type) {
900
+ if (!children && text === void 0) return value;
901
+ return {
902
+ ...value,
903
+ type: "paragraph",
904
+ children: value.children !== void 0 ? children ?? value.children : [textInline(text ?? "", style)]
905
+ };
906
+ }
907
+ return value;
908
+ };
909
+ const RICH_TEXT_BODY_KEYS = [
910
+ "text",
911
+ "content",
912
+ "label",
913
+ "children",
914
+ "items",
915
+ "blocks"
916
+ ];
917
+ const MAX_RICH_TEXT_NORMALIZATION_ISSUES = 32;
918
+ const INSPECTABLE_RICH_TEXT_NODE_TYPES = /* @__PURE__ */ new Set([
919
+ "paragraph",
920
+ "heading",
921
+ "bulletList",
922
+ "listItem",
923
+ "image",
924
+ "infoBox",
925
+ "separator",
926
+ "blockExtension",
927
+ "text",
928
+ "template",
929
+ "link",
930
+ "lineBreak",
931
+ "inlineExtension"
932
+ ]);
933
+ const inspectRichTextNode = (value, path, issues, shorthand, implicitType = "paragraph", depth = 0) => {
934
+ if (!isRecord$2(value) || issues.length >= MAX_RICH_TEXT_NORMALIZATION_ISSUES) return;
935
+ const report = (field, message) => {
936
+ if (issues.length < MAX_RICH_TEXT_NORMALIZATION_ISSUES) issues.push({
937
+ path: field ? `${path}.${field}` : path,
938
+ message
939
+ });
940
+ };
941
+ if (depth > 8) {
942
+ report("", "Rich-text nesting exceeds the supported node structure. Use the documented block and inline grammar.");
943
+ return;
944
+ }
945
+ const type = shorthand ? resolveRichTextShorthandNodeType(value.type) ?? implicitType : getString(value.type);
946
+ if (!type || !INSPECTABLE_RICH_TEXT_NODE_TYPES.has(type)) return;
947
+ const textBody = type === "text" || shorthand && [
948
+ "paragraph",
949
+ "heading",
950
+ "infoBox",
951
+ "listItem",
952
+ "link"
953
+ ].includes(type ?? "");
954
+ const acceptsChildren = [
955
+ "paragraph",
956
+ "heading",
957
+ "infoBox",
958
+ "listItem",
959
+ "link"
960
+ ].includes(type ?? "");
961
+ const bodyKeys = RICH_TEXT_BODY_KEYS.filter((key) => value[key] !== void 0);
962
+ if (bodyKeys.length > 1) report("", `Conflicting rich-text body representations: ${bodyKeys.join(", ")}. Provide one body representation; preserve all intended content in it.`);
963
+ for (const key of bodyKeys) {
964
+ if (!(key === "children" ? acceptsChildren : key === "items" ? type === "bulletList" : key === "blocks" ? false : textBody && (shorthand || key === "text"))) report(key, key === "blocks" ? "Nested blocks is not a supported node body. Use children for canonical nested content, items for a bulletList, or text for a simple shorthand body." : `The ${type ?? "untyped"} node does not support the ${key} body field. Use the documented node structure; image captions use caption paragraph blocks and separators have no body.`);
965
+ if ([
966
+ "text",
967
+ "content",
968
+ "label"
969
+ ].includes(key) && typeof value[key] !== "string") report(key, "Rich-text shorthand text must be a string. Use the documented children structure for nested content.");
970
+ }
971
+ for (const key of ["href", "url"]) if (value[key] !== void 0 && (type !== "link" || !shorthand && key === "url")) report(key, "Links must use a link inline node with href and children (or text shorthand). A URL on another node is not rendered as a link.");
972
+ for (const key of ["title", "caption"]) if (value[key] !== void 0 && !(key === "title" ? type === "infoBox" || type === "link" : type === "image")) report(key, `The ${type ?? "untyped"} node does not support ${key}. Use the documented node structure to preserve this content.`);
973
+ for (const key of [
974
+ "children",
975
+ "items",
976
+ "title",
977
+ "caption"
978
+ ]) {
979
+ const children = value[key];
980
+ if (Array.isArray(children)) {
981
+ const childType = key === "items" ? "listItem" : key === "title" || key === "children" && [
982
+ "paragraph",
983
+ "heading",
984
+ "link"
985
+ ].includes(type ?? "") ? "text" : "paragraph";
986
+ for (let index = 0; index < children.length && issues.length < MAX_RICH_TEXT_NORMALIZATION_ISSUES; index++) inspectRichTextNode(children[index], `${path}.${key}.${index}`, issues, shorthand, childType, depth + 1);
987
+ }
988
+ }
989
+ };
990
+ const normalizeRichTextContent = (value, path, issues) => {
991
+ if (!isRecord$2(value)) return value;
992
+ if (value.type === ContentType.richText && isRecord$2(value.doc) && value.version === 1) {
993
+ if (Array.isArray(value.doc.blocks)) for (let index = 0; index < value.doc.blocks.length && issues.length < MAX_RICH_TEXT_NORMALIZATION_ISSUES; index++) inspectRichTextNode(value.doc.blocks[index], `${path}.doc.blocks.${index}`, issues, false);
994
+ return value;
995
+ }
996
+ const candidateBlocks = Array.isArray(value.richTextBlocks) ? value.richTextBlocks : Array.isArray(value.blocks) ? value.blocks : void 0;
997
+ if (!candidateBlocks) return value;
998
+ const shorthandKey = Array.isArray(value.richTextBlocks) ? "richTextBlocks" : "blocks";
999
+ const issueCount = issues.length;
1000
+ for (let index = 0; index < candidateBlocks.length && issues.length < MAX_RICH_TEXT_NORMALIZATION_ISSUES; index++) inspectRichTextNode(candidateBlocks[index], `${path}.${shorthandKey}.${index}`, issues, true);
1001
+ if (issues.length > issueCount || issues.length >= MAX_RICH_TEXT_NORMALIZATION_ISSUES) return value;
1002
+ const blocks = candidateBlocks.map(normalizeRichTextBlock);
1003
+ return {
1004
+ type: ContentType.richText,
1005
+ version: 1,
1006
+ doc: {
1007
+ type: "doc",
1008
+ blocks
1009
+ }
1010
+ };
1011
+ };
1012
+ const inspectTranslationRepresentations = (value, path, issues) => {
1013
+ if (!isRecord$2(value)) return;
1014
+ const shorthandKeys = ["richTextBlocks", "blocks"].filter((key) => Object.prototype.hasOwnProperty.call(value, key));
1015
+ if (value.plainText !== void 0 && value.content !== void 0) issues.push({
1016
+ path,
1017
+ message: "Do not combine plainText with content; provide one representation."
1018
+ });
1019
+ if (shorthandKeys.length === 0) return;
1020
+ if (shorthandKeys.length > 1) issues.push({
1021
+ path,
1022
+ message: "Provide only richTextBlocks; do not combine it with the blocks alias."
1023
+ });
1024
+ const shorthandKey = shorthandKeys[0];
1025
+ const shorthand = value[shorthandKey];
1026
+ if (!Array.isArray(shorthand)) issues.push({
1027
+ path: `${path}.${shorthandKey}`,
1028
+ message: `${shorthandKey} must be an array of rich-text blocks.`
1029
+ });
1030
+ else if (shorthand.length === 0) issues.push({
1031
+ path: `${path}.${shorthandKey}`,
1032
+ message: `${shorthandKey} must contain at least one block. Use an explicit canonical content value when intentionally clearing a surface.`
1033
+ });
1034
+ if (value.plainText !== void 0 || value.content !== void 0) issues.push({
1035
+ path,
1036
+ message: "Do not combine richTextBlocks with plainText or content; provide one representation."
1037
+ });
1038
+ };
1039
+ /** Accept unambiguous model-facing shorthand without changing the canonical document grammar. */
1040
+ const normalizeSurveyChangeOperations = (operations, defaultLocale) => {
1041
+ const issues = [];
1042
+ const text = (value, path) => {
1043
+ if (!isRecord$2(value)) return value;
1044
+ const candidate = { ...value };
1045
+ const aliases = ["text", "label"].filter((key) => Object.hasOwn(candidate, key));
1046
+ const explicit = [
1047
+ "plainText",
1048
+ "content",
1049
+ "richTextBlocks",
1050
+ "blocks"
1051
+ ].some((key) => Object.hasOwn(candidate, key));
1052
+ if (aliases.length > 1 || aliases.length && explicit) issues.push({
1053
+ path,
1054
+ message: "Provide one content representation; do not combine text/label with plainText, content or richTextBlocks."
1055
+ });
1056
+ else if (aliases.length === 1 && typeof candidate[aliases[0]] === "string") {
1057
+ candidate.plainText = candidate[aliases[0]];
1058
+ delete candidate[aliases[0]];
1059
+ }
1060
+ if (typeof candidate.content === "string" && candidate.plainText === void 0) {
1061
+ candidate.plainText = candidate.content;
1062
+ delete candidate.content;
1063
+ }
1064
+ inspectTranslationRepresentations(candidate, path, issues);
1065
+ const content = normalizeRichTextContent(candidate.content ?? (Array.isArray(candidate.richTextBlocks) || Array.isArray(candidate.blocks) ? candidate : void 0), `${path}.content`, issues);
1066
+ const { richTextBlocks: _rich, blocks: _blocks, ...rest } = candidate;
1067
+ return {
1068
+ ...rest,
1069
+ locale: candidate.locale ?? defaultLocale,
1070
+ ...content !== void 0 ? { content } : {}
1071
+ };
1072
+ };
1073
+ const translations = (value, path) => {
1074
+ if (Array.isArray(value)) return value.map((entry, index) => text(entry, `${path}.${index}`));
1075
+ if (!isRecord$2(value)) return value;
1076
+ const result = [];
1077
+ for (const [locale, branch] of Object.entries(value)) {
1078
+ if (!isRecord$2(branch)) {
1079
+ issues.push({
1080
+ path: `${path}.${locale}`,
1081
+ message: "Expected a map of exact owner IDs and their content roles."
1082
+ });
1083
+ continue;
1084
+ }
1085
+ const owners = Object.hasOwn(branch, "content") ? branch.content : branch;
1086
+ if (!isRecord$2(owners) || Object.hasOwn(branch, "content") && Object.keys(branch).length !== 1) {
1087
+ issues.push({
1088
+ path: `${path}.${locale}`,
1089
+ message: "Use an owner-role content map only; survey-level translations need their own typed operation."
1090
+ });
1091
+ continue;
1092
+ }
1093
+ for (const [ownerId, roles] of Object.entries(owners)) {
1094
+ if (!isRecord$2(roles)) {
1095
+ issues.push({
1096
+ path: `${path}.${locale}.${ownerId}`,
1097
+ message: "Expected a map from content roles to translated values."
1098
+ });
1099
+ continue;
1100
+ }
1101
+ for (const [role, content] of Object.entries(roles)) {
1102
+ const leafPath = `${path}.${locale}.${ownerId}.${role}`;
1103
+ const leaf = typeof content === "string" ? { plainText: content } : content === null ? { content: null } : isRecord$2(content) && typeof content.type === "string" ? { content } : content;
1104
+ if (!isRecord$2(leaf)) {
1105
+ issues.push({
1106
+ path: leafPath,
1107
+ message: "Expected text, canonical content, richTextBlocks, or null to clear."
1108
+ });
1109
+ continue;
1110
+ }
1111
+ if ([
1112
+ "ownerId",
1113
+ "role",
1114
+ "locale"
1115
+ ].some((key) => Object.hasOwn(leaf, key))) {
1116
+ issues.push({
1117
+ path: leafPath,
1118
+ message: "Put ownerId, role and locale either in array entries or in map keys, not both."
1119
+ });
1120
+ continue;
1121
+ }
1122
+ result.push(text({
1123
+ ...leaf,
1124
+ ownerId,
1125
+ role,
1126
+ locale
1127
+ }, leafPath));
1128
+ }
1129
+ }
1130
+ }
1131
+ return result;
1132
+ };
1133
+ return {
1134
+ operations: operations.map((value, index) => {
1135
+ if (!isRecord$2(value)) return value;
1136
+ const candidate = { ...value };
1137
+ if (typeof candidate.op === "string") {
1138
+ if (candidate.kind !== void 0 && candidate.kind !== candidate.op) issues.push({
1139
+ path: String(index),
1140
+ message: "Conflicting operation kind and op alias; provide one intended operation."
1141
+ });
1142
+ else {
1143
+ candidate.kind = candidate.op;
1144
+ delete candidate.op;
1145
+ }
1146
+ }
1147
+ const normalized = candidate.kind === "update-content" || candidate.kind === "update-survey-translation" ? text(candidate, String(index)) : candidate;
1148
+ if (isRecord$2(normalized) && normalized.translations !== void 0) normalized.translations = translations(normalized.translations, `${index}.translations`);
1149
+ if (isRecord$2(normalized) && normalized.kind === "create-item" && isRecord$2(normalized.item)) {
1150
+ const item = normalized.item;
1151
+ if (Object.hasOwn(normalized, "itemId")) {
1152
+ if (typeof item.id === "string" && normalized.itemId === item.id) delete normalized.itemId;
1153
+ else issues.push({
1154
+ path: `${index}.itemId`,
1155
+ message: "Conflicting create-item itemId and item.id; use item.id for the new item identity."
1156
+ });
1157
+ }
1158
+ if (Object.hasOwn(item, "translations")) {
1159
+ const nested = translations(item.translations, `${index}.item.translations`);
1160
+ const outer = normalized.translations ?? [];
1161
+ if (Array.isArray(nested) && Array.isArray(outer)) {
1162
+ const combined = [];
1163
+ const byTarget = /* @__PURE__ */ new Map();
1164
+ for (const entry of [...outer, ...nested]) {
1165
+ if (!isRecord$2(entry) || ![
1166
+ entry.locale,
1167
+ entry.ownerId,
1168
+ entry.role
1169
+ ].every((value) => typeof value === "string")) {
1170
+ combined.push(entry);
1171
+ continue;
1172
+ }
1173
+ const key = JSON.stringify([
1174
+ entry.locale,
1175
+ entry.ownerId,
1176
+ entry.role
1177
+ ]);
1178
+ if (byTarget.has(key)) {
1179
+ if (stableStringify(byTarget.get(key)) !== stableStringify(entry)) issues.push({
1180
+ path: `${index}.item.translations`,
1181
+ message: `Conflicting translations for owner "${entry.ownerId}", role "${entry.role}", locale "${entry.locale}"; provide one intended value beside item.`
1182
+ });
1183
+ continue;
1184
+ }
1185
+ byTarget.set(key, entry);
1186
+ combined.push(entry);
1187
+ }
1188
+ const { translations: _nested, ...canonicalItem } = item;
1189
+ normalized.item = canonicalItem;
1190
+ normalized.translations = combined;
1191
+ } else issues.push({
1192
+ path: `${index}.item.translations`,
1193
+ message: "Creation translations must be an owner-role entry array or locale/owner/role map; put them beside item."
1194
+ });
1195
+ }
1196
+ }
1197
+ return normalized;
1198
+ }),
1199
+ issues
1200
+ };
1201
+ };
1202
+ //#endregion
1203
+ //#region src/engine/reference-diagnostics.ts
1204
+ const MAX_REFERENCE_LENGTH = 256;
1205
+ const MAX_DISTANCE = 3;
1206
+ const spellingDistance = (left, right) => {
1207
+ if (Math.abs(left.length - right.length) > MAX_DISTANCE) return Infinity;
1208
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
1209
+ for (let row = 1; row <= left.length; row++) {
1210
+ const current = [row];
1211
+ for (let column = 1; column <= right.length; column++) current[column] = Math.min(current[column - 1] + 1, previous[column] + 1, previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1));
1212
+ if (Math.min(...current) > MAX_DISTANCE) return Infinity;
1213
+ previous = current;
1214
+ }
1215
+ return previous[right.length];
1216
+ };
1217
+ const suggestExactReferences = (supplied, known) => {
1218
+ if (supplied.length > MAX_REFERENCE_LENGTH) return [];
1219
+ const candidates = [];
1220
+ for (const value of new Set(known)) {
1221
+ if (value.length > MAX_REFERENCE_LENGTH || value === supplied) continue;
1222
+ const distance = supplied.endsWith(`.${value}`) ? 0 : spellingDistance(supplied, value);
1223
+ if (distance <= MAX_DISTANCE) candidates.push({
1224
+ value,
1225
+ distance
1226
+ });
1227
+ }
1228
+ candidates.sort((left, right) => left.distance - right.distance || left.value.localeCompare(right.value));
1229
+ return candidates.filter((entry) => entry.distance === candidates[0]?.distance).slice(0, 3).map(({ value }) => value);
1230
+ };
1231
+ const formatExactReferenceSuggestions = (supplied, known, label) => {
1232
+ const suggestions = suggestExactReferences(supplied, known);
1233
+ return suggestions.length ? ` Similar valid ${label}: ${suggestions.map((value) => JSON.stringify(value)).join(", ")}. Confirm the intended target and copy its exact identifier.` : "";
1234
+ };
1235
+ //#endregion
1236
+ //#region src/engine/operation-execution.ts
1237
+ const pointer = (parts) => `/${parts.map((part) => encodeJsonPointerSegment(String(part))).join("/")}`;
1238
+ const buildContent = (value) => value.content === null ? void 0 : value.content ?? (value.plainText !== void 0 ? {
1239
+ type: "plain",
1240
+ content: value.plainText
1241
+ } : void 0);
1242
+ const metadata = {
1243
+ label: "Assistant change",
1244
+ source: "assistant"
1245
+ };
1246
+ /** Private validation simulation. Accepted results still enter the host's normal editor transaction. */
1247
+ var SurveyOperationExecutionSession = class {
1248
+ editor;
1249
+ baseline;
1250
+ /** Identities removed while other content still referenced them. */
1251
+ removedDependencies = /* @__PURE__ */ new Set();
1252
+ constructor(survey, capabilities, answerTypes) {
1253
+ this.editor = new SurveyEditor(createAssistantSurvey(survey, capabilities), { answerTypes });
1254
+ this.baseline = this.model;
1255
+ }
1256
+ get model() {
1257
+ return this.editor.survey;
1258
+ }
1259
+ getAnswerTypes() {
1260
+ return this.editor.getAnswerTypes();
1261
+ }
1262
+ hasLocale(locale) {
1263
+ return Object.hasOwn(this.model.data.translations ?? {}, locale);
1264
+ }
1265
+ writeContent(ref, value) {
1266
+ if (ref.scope === "content") {
1267
+ const owner = this.model.owners.get(ref.ownerId);
1268
+ if (!owner) throw new Error(`Unknown content owner "${ref.ownerId}".${formatExactReferenceSuggestions(ref.ownerId, this.model.owners.keys(), "owner IDs")}`);
1269
+ if (!owner.roles.some((role) => role.role === ref.role)) throw new Error(`Unknown content role "${ref.role}" on owner "${ref.ownerId}".${formatExactReferenceSuggestions(ref.role, owner.roles.map((role) => role.role), "content roles")}`);
1270
+ }
1271
+ this.editor.setLocalizedContent(ref, value.locale, buildContent(value), metadata);
1272
+ }
1273
+ translations(values) {
1274
+ for (const entry of values ?? []) this.writeContent({
1275
+ scope: "content",
1276
+ ownerId: entry.ownerId,
1277
+ role: entry.role
1278
+ }, entry);
1279
+ }
1280
+ /**
1281
+ * The editor lets people leave broken references to repair later; the assistant never does.
1282
+ * Remember what a removal leaves dangling, so repairs may follow later in the same change.
1283
+ */
1284
+ recordRemoval(id) {
1285
+ for (const reference of this.editor.deletionImpact(id)) this.removedDependencies.add(reference.id);
1286
+ }
1287
+ /** Reject a change that still leaves references to content it removed. */
1288
+ assertNoDanglingRemovals() {
1289
+ if (!this.removedDependencies.size) return;
1290
+ const survey = this.model;
1291
+ const references = annotatedReferences(inspectAnnotated(survey.registry.surveySchema, survey.data)).filter((reference) => reference.path[0] !== "translations" && this.removedDependencies.has(reference.id) && !survey.identities.has(reference.id));
1292
+ if (!references.length) return;
1293
+ throw new DefinitionError(references.map((reference) => ({
1294
+ severity: "error",
1295
+ code: "deletion-has-dependencies",
1296
+ path: [...reference.path],
1297
+ message: `This reference to ${reference.target} "${reference.id}" depends on the content being removed. Update or remove this exact reference in the same change; inspect expression-diagnostics and the owning item for context.`
1298
+ })));
1299
+ }
1300
+ patch(patches, cleanOwnedContent = false) {
1301
+ this.editor.transact(metadata, (draft) => {
1302
+ const patched = this.model.registry.surveySchema.parse(applyJsonPatch(draft, patches));
1303
+ if (cleanOwnedContent) {
1304
+ const remaining = contentOwnerIds(this.model.registry.surveySchema, patched);
1305
+ for (const translation of Object.values(patched.translations ?? {})) for (const id of this.model.owners.keys()) if (!remaining.has(id)) delete translation.content[id];
1306
+ }
1307
+ for (const key of Object.keys(draft)) Reflect.deleteProperty(draft, key);
1308
+ Object.assign(draft, patched);
1309
+ });
1310
+ }
1311
+ apply(operation) {
1312
+ this.editor.runAction(() => {
1313
+ switch (operation.kind) {
1314
+ case "insert-preset": throw new Error("Materialize trusted presets before executing a change set");
1315
+ case "create-item": {
1316
+ const parent = this.model.getItem(operation.target.parentItemId);
1317
+ if (parent?.itemType !== "group") throw new DefinitionError([{
1318
+ severity: "error",
1319
+ code: "target-parent-not-found",
1320
+ path: [],
1321
+ message: `The target must be an existing group. Current group IDs: ${this.model.data.surveyItems.filter((item) => item.itemType === "group").map((item) => JSON.stringify(item.id)).join(", ")}.`
1322
+ }]);
1323
+ if (operation.target.index !== void 0 && operation.target.index > parent.config.items.length) throw new Error("Insertion index exceeds the current sibling count");
1324
+ this.editor.addItem({
1325
+ parentId: parent.id,
1326
+ index: operation.target.index
1327
+ }, operation.item);
1328
+ this.translations(operation.translations);
1329
+ break;
1330
+ }
1331
+ case "insert-element": {
1332
+ const path = this.model.identities.get(operation.target.layoutId)?.path.slice(0, -1);
1333
+ if (!path || this.model.identities.get(operation.target.layoutId)?.kind !== "layout") throw new Error("Unknown layout target");
1334
+ const length = [...this.model.elements.values()].filter((element) => element.path.length === path.length + 2 && path.every((part, index) => element.path[index] === part)).length;
1335
+ if (operation.target.index !== void 0 && operation.target.index > length) throw new Error("Insertion index exceeds the layout child count");
1336
+ this.editor.insertElement(operation.element, operation.target, metadata);
1337
+ this.translations(operation.translations);
1338
+ break;
1339
+ }
1340
+ case "insert-blocks": {
1341
+ const body = this.model.bodies.get(operation.bodyOwnerId);
1342
+ if (!body) throw new Error("Unknown body owner");
1343
+ const position = operation.index;
1344
+ this.patch(operation.body.blocks.map((block, index) => ({
1345
+ op: "add",
1346
+ path: `${pointer(body.path)}/blocks/${position === void 0 ? "-" : position + index}`,
1347
+ value: block
1348
+ })), false);
1349
+ this.translations(operation.translations);
1350
+ break;
1351
+ }
1352
+ case "update-content":
1353
+ this.writeContent(operation.ref, operation);
1354
+ break;
1355
+ case "update-survey-translation":
1356
+ this.writeContent({
1357
+ scope: "survey",
1358
+ surface: operation.surface,
1359
+ contentKey: operation.contentKey
1360
+ }, operation);
1361
+ break;
1362
+ case "patch-element-config":
1363
+ if (!this.model.elements.get(operation.elementId)) throw new Error(`Unknown element "${operation.elementId}".${formatExactReferenceSuggestions(operation.elementId, this.model.elements.keys(), "element IDs")}`);
1364
+ this.editor.updateElement(operation.elementId, (node) => {
1365
+ const config = applyJsonPatch(node.config, operation.patches);
1366
+ node.config = this.model.registry.get(node.elementType).schema.parse({
1367
+ ...node,
1368
+ config
1369
+ }).config;
1370
+ }, metadata);
1371
+ this.translations(operation.translations);
1372
+ break;
1373
+ case "patch-item-config": {
1374
+ const owner = this.model.owners.get(operation.itemId);
1375
+ if (!this.model.getItem(operation.itemId) || !owner) throw new Error(`Unknown item "${operation.itemId}". Inspect current item IDs before editing.`);
1376
+ const root = `${pointer(owner.path)}/config`;
1377
+ this.patch(operation.patches.map((patch) => ({
1378
+ ...patch,
1379
+ path: root + patch.path,
1380
+ ..."from" in patch ? { from: root + patch.from } : {}
1381
+ })), true);
1382
+ this.translations(operation.translations);
1383
+ break;
1384
+ }
1385
+ case "update-item-label":
1386
+ case "update-item-color":
1387
+ if (!this.model.getItem(operation.itemId)) throw new Error(`Unknown item "${operation.itemId}". Inspect current item IDs before editing.`);
1388
+ this.editor.transact(metadata, (draft) => {
1389
+ const item = draft.surveyItems.find((item) => item.id === operation.itemId);
1390
+ item.metadata = {
1391
+ ...item.metadata,
1392
+ ...operation.kind === "update-item-label" ? { itemLabel: operation.itemLabel } : { editorItemColor: operation.color }
1393
+ };
1394
+ });
1395
+ break;
1396
+ case "remove-item": {
1397
+ const item = this.model.getItem(operation.itemId);
1398
+ if (!item) throw new Error(`Unknown item "${operation.itemId}". Inspect current item IDs before editing.`);
1399
+ if (item.itemType === "group" && item.config.items.length && !operation.includeChildren) throw new Error("Set includeChildren to remove a group and its descendants");
1400
+ this.recordRemoval(item.id);
1401
+ this.editor.removeItem(item.id);
1402
+ break;
1403
+ }
1404
+ case "remove-content":
1405
+ this.recordRemoval(operation.ownerId);
1406
+ this.editor.remove(operation.ownerId);
1407
+ break;
1408
+ case "move-item":
1409
+ this.editor.moveItem(operation.itemId, {
1410
+ parentId: operation.target.parentItemId,
1411
+ index: operation.target.index
1412
+ }, metadata);
1413
+ break;
1414
+ case "reorder-items": {
1415
+ const parent = this.model.getItem(operation.parentItemId);
1416
+ if (parent?.itemType !== "group") throw new Error("Unknown parent group");
1417
+ if (operation.itemIds.length !== parent.config.items.length || new Set(operation.itemIds).size !== parent.config.items.length || operation.itemIds.some((id) => !parent.config.items.includes(id))) throw new Error("Supply every existing direct child exactly once");
1418
+ this.editor.transact(metadata, (draft) => {
1419
+ const item = draft.surveyItems.find((item) => item.id === parent.id);
1420
+ if (item.itemType === "group") item.config.items = [...operation.itemIds];
1421
+ });
1422
+ break;
1423
+ }
1424
+ case "move-element":
1425
+ this.editor.moveElement(operation.elementId, operation.target.layoutId, operation.target.index, operation.ruleTransfer);
1426
+ break;
1427
+ case "move-block":
1428
+ this.editor.moveBlock(operation.blockId, operation.bodyOwnerId, operation.index, operation.ruleTransfer);
1429
+ break;
1430
+ case "update-layout":
1431
+ this.editor.updateLayout(operation.layoutId, operation.layout);
1432
+ break;
1433
+ case "update-response-settings":
1434
+ this.editor.updateResponseSettings(operation.naming, operation.settings);
1435
+ break;
1436
+ case "add-survey-locale":
1437
+ if (!this.editor.addLocale(operation.locale)) throw new DefinitionError([{
1438
+ severity: "error",
1439
+ code: "survey-locale-already-exists",
1440
+ path: ["translations", operation.locale],
1441
+ message: "The survey locale already exists"
1442
+ }]);
1443
+ break;
1444
+ case "remove-survey-locale":
1445
+ if (!this.editor.removeLocale(operation.locale)) throw new DefinitionError([{
1446
+ severity: "error",
1447
+ code: "survey-locale-not-found",
1448
+ path: ["translations", operation.locale],
1449
+ message: "Unknown survey locale"
1450
+ }]);
1451
+ break;
1452
+ case "update-survey-metadata":
1453
+ this.editor.transact(metadata, (draft) => {
1454
+ draft.metadata ??= {};
1455
+ for (const [key, value] of Object.entries(operation.values)) if (value === null) delete draft.metadata[key];
1456
+ else setOwnValue(draft.metadata, key, value);
1457
+ });
1458
+ break;
1459
+ case "update-survey-pagination":
1460
+ this.editor.updateMaxItemsPerPage(operation.value ?? void 0);
1461
+ break;
1462
+ case "set-image-asset":
1463
+ this.editor.setAsset(operation.assetId, operation.asset);
1464
+ break;
1465
+ case "update-image-asset-metadata": {
1466
+ const asset = this.model.getAsset(operation.assetId);
1467
+ if (!asset) throw new Error("Unknown image asset");
1468
+ const next = structuredClone(asset);
1469
+ next.metadata ??= {};
1470
+ for (const [key, value] of Object.entries(operation.metadata)) if (value === null) Reflect.deleteProperty(next.metadata, key);
1471
+ else if (value !== void 0) setOwnValue(next.metadata, key, value);
1472
+ this.editor.setAsset(operation.assetId, next);
1473
+ break;
1474
+ }
1475
+ case "remove-image-asset":
1476
+ if (!this.model.hasAsset(operation.assetId)) throw new Error("Unknown image asset");
1477
+ if (this.model.getAssetUsages(operation.assetId).length) throw new DefinitionError([{
1478
+ severity: "error",
1479
+ code: "image-asset-still-referenced",
1480
+ path: ["assets", operation.assetId],
1481
+ message: "Remove image placements before deleting this asset"
1482
+ }]);
1483
+ this.editor.removeAsset(operation.assetId);
1484
+ break;
1485
+ case "update-placed-image": {
1486
+ const content = this.editor.getLocalizedContent(operation.ref, operation.locale);
1487
+ if (content?.type !== "richText") throw new Error("Image placement is not rich text");
1488
+ const next = structuredClone(content);
1489
+ const image = next.doc.blocks.filter((block) => block.type === "image")[operation.imageIndex];
1490
+ if (!image) throw new Error("Unknown image placement");
1491
+ if (operation.expectedAssetId && (image.source.type !== "asset" || image.source.assetId !== operation.expectedAssetId)) throw new DefinitionError([{
1492
+ severity: "error",
1493
+ code: "placed-image-asset-mismatch",
1494
+ path: ["imageIndex"],
1495
+ message: "Image placement changed; inspect it again"
1496
+ }]);
1497
+ const changes = operation.changes;
1498
+ if (changes.source) image.source = changes.source;
1499
+ if (changes.alt === null) delete image.alt;
1500
+ else if (changes.alt !== void 0) image.alt = changes.alt;
1501
+ if (changes.caption !== void 0) {
1502
+ if (!changes.caption) delete image.caption;
1503
+ else image.caption = [{
1504
+ type: "paragraph",
1505
+ children: [{
1506
+ type: "text",
1507
+ text: changes.caption
1508
+ }]
1509
+ }];
1510
+ }
1511
+ const size = { ...image.size };
1512
+ for (const key of [
1513
+ "width",
1514
+ "height",
1515
+ "maxWidth",
1516
+ "maxHeight"
1517
+ ]) if (changes[key] === null) delete size[key];
1518
+ else if (changes[key] !== void 0) size[key] = changes[key];
1519
+ if (Object.keys(size).length) image.size = size;
1520
+ else delete image.size;
1521
+ if (changes.alignment === null) delete image.alignment;
1522
+ else if (changes.alignment) image.alignment = changes.alignment;
1523
+ this.editor.setLocalizedContent(operation.ref, operation.locale, next, metadata);
1524
+ break;
1525
+ }
1526
+ case "survey-json-patch":
1527
+ this.patch(operation.patches);
1528
+ break;
1529
+ default: throw new Error(`Unknown operation ${String(operation)}`);
1530
+ }
1531
+ const diagnostics = getAnswerTypeChangeDiagnostics(this.baseline, this.model);
1532
+ if (diagnostics.length) throw new DefinitionError(diagnostics);
1533
+ });
1534
+ }
1535
+ serialize() {
1536
+ return this.model.serialize();
1537
+ }
1538
+ };
1539
+ const executeSurveyOperationsOnRawSurvey = ({ operations, survey, capabilities, answerTypes }) => {
1540
+ const session = new SurveyOperationExecutionSession(survey, capabilities, answerTypes);
1541
+ for (const operation of operations) session.apply(operation);
1542
+ session.assertNoDanglingRemovals();
1543
+ return session.serialize();
1544
+ };
1545
+ //#endregion
1546
+ //#region src/engine/authoring-content.ts
1547
+ function requirements(survey) {
1548
+ const result = /* @__PURE__ */ new Map();
1549
+ for (const { node, itemId, facts } of survey.elements.values()) {
1550
+ const add = (ownerId, role, renderable = false) => {
1551
+ const ref = {
1552
+ scope: "content",
1553
+ ownerId,
1554
+ role
1555
+ };
1556
+ result.set(getLocalizationEntryKey(ref), {
1557
+ elementId: node.id,
1558
+ itemId,
1559
+ ref,
1560
+ renderable
1561
+ });
1562
+ };
1563
+ if (node.elementType === "information" || node.elementType === "consent") add(node.id, "body", true);
1564
+ for (const binding of facts.bindings) {
1565
+ if (binding.labelRefs) for (const ref of binding.labelRefs) add(ref.ownerId, ref.role);
1566
+ else result.set(JSON.stringify(["element-name", node.id]), {
1567
+ elementId: node.id,
1568
+ itemId
1569
+ });
1570
+ for (const id of binding.selection?.options ?? []) if (survey.owners.get(id)?.roles.some((role) => role.role === "label")) add(id, "label");
1571
+ }
1572
+ }
1573
+ return result;
1574
+ }
1575
+ /** Changes to existing inputs must not introduce unnamed components or erase naming content.
1576
+ * New elements may be blank presets awaiting later authoring; they retain normal warnings.
1577
+ * Presence in any actual language counts, separately from translation completeness.
1578
+ */
1579
+ function getIntroducedMissingAuthoringContent(before, after) {
1580
+ const previous = requirements(before);
1581
+ const locales = after.locales;
1582
+ const present = (survey, requirement) => requirement.ref ? locales.some((locale) => {
1583
+ const content = readLocalizationContent(survey, requirement.ref, locale);
1584
+ return requirement.renderable ? !isContentEmpty(content) : hasLocalizationText(content);
1585
+ }) : Boolean(resolveElementName(survey, requirement.elementId, locales));
1586
+ const issues = [];
1587
+ for (const [key, requirement] of requirements(after)) {
1588
+ if (present(after, requirement)) continue;
1589
+ const prior = previous.get(key);
1590
+ if (prior ? !present(before, prior) : !before.elements.has(requirement.elementId)) continue;
1591
+ if (!requirement.ref && prior) {
1592
+ const name = resolveElementName(before, requirement.elementId, locales);
1593
+ if (name && locales.some((locale) => hasLocalizationText(readLocalizationContent(after, {
1594
+ scope: "content",
1595
+ ownerId: name.ownerId,
1596
+ role: name.role
1597
+ }, locale)))) continue;
1598
+ }
1599
+ issues.push({
1600
+ severity: "error",
1601
+ code: "missing-authoring-content",
1602
+ itemId: requirement.itemId,
1603
+ message: requirement.ref ? `Provide non-empty localized content for owner "${requirement.ref.ownerId}" role "${requirement.ref.role}" in the same proposal. Preserve previously authored content unless its control is also removed.` : `Element "${requirement.elementId}" needs a localized label or an unambiguous enclosing title in the same proposal. Preserve its previously authored naming content.`
1604
+ });
1605
+ }
1606
+ return issues;
1607
+ }
1608
+ //#endregion
1609
+ //#region src/engine/operations.ts
1610
+ const MAX_SURVEY_ASSISTANT_OPERATIONS_PER_PROPOSAL = 200;
1611
+ const MAX_SURVEY_ASSISTANT_OPERATIONS_PER_TURN_DRAFT = 1e3;
1612
+ const buildSurveyDocumentVersion = (snapshot) => ({ ...snapshot.version });
1613
+ function materialize(operation, model, caps) {
1614
+ if (operation.kind !== "insert-preset") return operation;
1615
+ const capability = caps.byElementType.get(operation.elementType);
1616
+ if (capability?.manifest.create !== "deterministic" || !capability.manifest.creationPresets?.some((preset) => preset.id === operation.presetId) || !capability.materializePreset) throw new Error("This preset is not available from the trusted registry");
1617
+ if (capability.supportsOperation && !capability.supportsOperation(operation)) throw new Error("The host does not permit this preset operation");
1618
+ const created = capability.materializePreset(operation.presetId, operation.locale, new Set(model.slots.definitions.map((slot) => slot.variableName)));
1619
+ if (created.element.elementType !== operation.elementType) throw new Error("Preset returned the wrong element type");
1620
+ for (const ownerId of Object.keys(created.content ?? {})) if (model.owners.has(ownerId)) throw new Error(`Initial localized owner '${ownerId}' already exists`);
1621
+ const translations = Object.entries(created.content ?? {}).flatMap(([ownerId, roles]) => Object.entries(roles).map(([role, content]) => ({
1622
+ ownerId,
1623
+ role,
1624
+ locale: operation.locale,
1625
+ content
1626
+ })));
1627
+ if ("layoutId" in operation.target) return {
1628
+ kind: "insert-element",
1629
+ id: operation.id,
1630
+ target: operation.target,
1631
+ element: elementTransportSchema.parse(created.element),
1632
+ translations
1633
+ };
1634
+ const editor = new SurveyEditor(model);
1635
+ const { itemId } = editor.insertElement(elementTransportSchema.parse(created.element), {
1636
+ parentId: operation.target.parentItemId,
1637
+ index: operation.target.index
1638
+ }, {
1639
+ label: "Assistant preset",
1640
+ source: "assistant"
1641
+ }, created.content ? {
1642
+ locale: operation.locale,
1643
+ content: created.content
1644
+ } : void 0);
1645
+ return surveyOperationSchema.parse({
1646
+ kind: "create-item",
1647
+ id: operation.id,
1648
+ target: operation.target,
1649
+ item: editor.survey.getItem(itemId),
1650
+ translations
1651
+ });
1652
+ }
1653
+ function elementContext(model, id) {
1654
+ const entry = model.elements.get(id);
1655
+ if (!entry) return void 0;
1656
+ const ancestors = [];
1657
+ let owner = model.owners.get(id);
1658
+ while (owner) {
1659
+ ancestors.push({
1660
+ id: owner.id,
1661
+ disabled: owner.disabled,
1662
+ visibility: owner.visibility
1663
+ });
1664
+ owner = owner.parentId ? model.owners.get(owner.parentId) : void 0;
1665
+ }
1666
+ const owners = new Set([...model.owners.values()].filter((owner) => owner.elementId === id).map((owner) => owner.id));
1667
+ for (const ancestor of ancestors) owners.add(ancestor.id);
1668
+ return {
1669
+ node: entry.node,
1670
+ itemId: entry.itemId,
1671
+ ancestors,
1672
+ content: Object.fromEntries(Object.entries(model.data.translations ?? {}).map(([locale, translation]) => [locale, Object.fromEntries(Object.entries(translation.content).filter(([ownerId]) => owners.has(ownerId)))]))
1673
+ };
1674
+ }
1675
+ function requireCapabilities(operation, before, after, caps) {
1676
+ if ([
1677
+ "add-survey-locale",
1678
+ "remove-survey-locale",
1679
+ "update-survey-metadata",
1680
+ "update-survey-pagination",
1681
+ "update-survey-translation",
1682
+ "set-image-asset",
1683
+ "update-image-asset-metadata",
1684
+ "remove-image-asset",
1685
+ "update-item-label",
1686
+ "update-item-color"
1687
+ ].includes(operation.kind)) return;
1688
+ const direct = /* @__PURE__ */ new Set();
1689
+ if ("elementId" in operation) direct.add(operation.elementId);
1690
+ if (operation.kind === "update-layout") {
1691
+ const path = before.identities.get(operation.layoutId)?.path.slice(0, -1);
1692
+ if (path) {
1693
+ for (const entry of before.elements.values()) if (path.every((part, index) => entry.path[index] === part)) direct.add(entry.node.id);
1694
+ }
1695
+ }
1696
+ if (operation.kind === "reorder-items" || operation.kind === "move-item") {
1697
+ const affected = operation.kind === "move-item" ? [operation.itemId] : operation.itemIds;
1698
+ for (const entry of before.elements.values()) if (affected.includes(entry.itemId)) direct.add(entry.node.id);
1699
+ }
1700
+ if (operation.kind === "survey-json-patch" || operation.kind === "patch-item-config") {
1701
+ for (const [id, entry] of before.elements) if (stableStringify(entry.path) !== stableStringify(after.elements.get(id)?.path)) direct.add(id);
1702
+ }
1703
+ if ("blockId" in operation || "ownerId" in operation) {
1704
+ const id = "blockId" in operation ? operation.blockId : operation.ownerId;
1705
+ const path = before.identities.get(id)?.path.slice(0, -1);
1706
+ if (path) {
1707
+ for (const element of before.elements.values()) if (path.every((part, index) => element.path[index] === part)) direct.add(element.node.id);
1708
+ }
1709
+ }
1710
+ for (const id of /* @__PURE__ */ new Set([...before.elements.keys(), ...after.elements.keys()])) {
1711
+ if (!direct.has(id) && stableStringify(elementContext(before, id)) === stableStringify(elementContext(after, id))) continue;
1712
+ const previous = before.elements.get(id);
1713
+ const next = after.elements.get(id);
1714
+ const entry = next ?? previous;
1715
+ const changesType = previous !== void 0 && next !== void 0 && previous.node.elementType !== next.node.elementType;
1716
+ if (changesType) {
1717
+ const sourceCapability = caps.byElementType.get(previous.node.elementType);
1718
+ if (!sourceCapability?.manifest.supportedOperations.includes(operation.kind) || sourceCapability.supportsOperation && !sourceCapability.supportsOperation(operation)) throw new Error(`Element '${previous.node.elementType}' does not permit '${operation.kind}' through the assistant`);
1719
+ }
1720
+ const capability = caps.byElementType.get(entry.node.elementType);
1721
+ if (!capability?.manifest.supportedOperations.includes(operation.kind) || capability.supportsOperation && !capability.supportsOperation(operation)) throw new Error(`Element '${entry.node.elementType}' does not permit '${operation.kind}' through the assistant`);
1722
+ if ((!previous || changesType) && capability.manifest.create !== "deterministic") throw new Error(`Element '${entry.node.elementType}' is not deterministically creatable`);
1723
+ if (!next || !capability.validateElement) continue;
1724
+ const existingErrors = /* @__PURE__ */ new Map();
1725
+ if (previous?.node.elementType === next.node.elementType) for (const issue of capability.validateElement(previous.node)) {
1726
+ if (issue.severity !== "error") continue;
1727
+ const key = stableStringify(issue);
1728
+ existingErrors.set(key, (existingErrors.get(key) ?? 0) + 1);
1729
+ }
1730
+ const errors = capability.validateElement(next.node).filter((issue) => {
1731
+ if (issue.severity !== "error") return false;
1732
+ const key = stableStringify(issue);
1733
+ const count = existingErrors.get(key) ?? 0;
1734
+ if (!count) return true;
1735
+ existingErrors.set(key, count - 1);
1736
+ return false;
1737
+ });
1738
+ if (errors.length) throw new Error(errors.map((issue) => issue.message).join("\n"));
1739
+ }
1740
+ }
1741
+ /** Preserve sibling wording when a model uses the low-level escape hatch. */
1742
+ function validateTranslationPatchSafety(operation, model) {
1743
+ for (const patch of operation.patches) {
1744
+ if (patch.op === "test") continue;
1745
+ for (const path of patch.op === "move" ? [patch.path, patch.from] : [patch.path]) {
1746
+ const segments = parseJsonPointer(path);
1747
+ if (segments[0] !== "translations") continue;
1748
+ let current = model.data;
1749
+ for (const segment of segments) current = current && typeof current === "object" ? ownValue(current, segment) : void 0;
1750
+ if ((segments.length <= 2 || segments.length === 3 || segments[2] === "content" && segments.length === 4) && (segments.length === 1 || current !== void 0)) throw new Error("Raw patches cannot replace or remove a translation container. Use update-content for an owner/role leaf or the typed locale operations.");
1751
+ }
1752
+ }
1753
+ }
1754
+ const expressionsKey = (model) => stableStringify(model.expressions.map((entry) => ({
1755
+ path: entry.path,
1756
+ expression: entry.expression
1757
+ })));
1758
+ function validateSurveyOperations({ allowExpressionJsonPatches = false, scope = "proposal", maxOperations = 200, operations, snapshot: input }) {
1759
+ const issues = [], warnings = [], normalizedOperations = [];
1760
+ const fail = (code, message, operationIndex) => issues.push({
1761
+ severity: "error",
1762
+ code,
1763
+ message,
1764
+ operationIndex
1765
+ });
1766
+ if (!input) {
1767
+ fail("missing-snapshot", "No live editor snapshot was provided");
1768
+ return {
1769
+ valid: false,
1770
+ issues,
1771
+ warnings,
1772
+ normalizedOperations
1773
+ };
1774
+ }
1775
+ if (!operations.length || operations.length > maxOperations) {
1776
+ fail(operations.length ? "too-many-operations" : "empty-operations", `Provide 1-${maxOperations} operations`);
1777
+ return {
1778
+ valid: false,
1779
+ issues,
1780
+ warnings,
1781
+ normalizedOperations
1782
+ };
1783
+ }
1784
+ try {
1785
+ const caps = ("model" in input ? input.trustedCapabilities : void 0) ?? defaultSurveyAssistantCapabilitySet;
1786
+ const snapshot = "model" in input ? input : createSurveyAssistantEngineContext(input, caps);
1787
+ assertSurveyAssistantCapabilities({
1788
+ manifest: snapshot.capabilities,
1789
+ trustedCapabilities: caps,
1790
+ versionDigest: snapshot.version.capabilityDigest
1791
+ });
1792
+ const session = new SurveyOperationExecutionSession(snapshot.survey, caps, snapshot.document.answerTypes);
1793
+ for (const [operationIndex, unparsed] of operations.entries()) try {
1794
+ const operation = materialize(surveyOperationSchema.parse(unparsed), session.model, caps);
1795
+ if (operation.kind === "survey-json-patch") {
1796
+ validateTranslationPatchSafety(operation, session.model);
1797
+ if (scope === "proposal" && operations.length !== 1) {
1798
+ fail("raw-patch-must-be-single-operation", "Put all raw patches in one operation", operationIndex);
1799
+ break;
1800
+ }
1801
+ if (operation.patches.some((patch) => [patch.path, ..."from" in patch ? [patch.from] : []].some((path) => path === "" || /^\/(\$schema|schemaVersion)(\/|$)/.test(path)))) {
1802
+ fail("survey-schema-is-immutable", "Raw patches cannot replace the document root or its schema identity", operationIndex);
1803
+ break;
1804
+ }
1805
+ }
1806
+ const before = session.model;
1807
+ session.apply(operation);
1808
+ const after = session.model;
1809
+ if (operation.kind === "update-item-label" || operation.kind === "update-item-color") {
1810
+ const key = operation.kind === "update-item-label" ? "itemLabel" : "editorItemColor";
1811
+ if (before.data.surveyItems.find((item) => item.id === operation.itemId)?.metadata?.[key] === after.data.surveyItems.find((item) => item.id === operation.itemId)?.metadata?.[key]) warnings.push({
1812
+ severity: "warning",
1813
+ code: key === "itemLabel" ? "no-label-change" : "no-color-change",
1814
+ operationIndex,
1815
+ itemId: operation.itemId,
1816
+ message: "The editor metadata already has this value."
1817
+ });
1818
+ }
1819
+ if (operation.kind === "add-survey-locale") warnings.push({
1820
+ severity: "warning",
1821
+ code: "empty-locale-registration",
1822
+ operationIndex,
1823
+ message: "Registering an empty locale does not complete a request to localize or translate the survey. Add the requested localized content as well."
1824
+ });
1825
+ if (operation.kind === "update-content" || operation.kind === "update-survey-translation") {
1826
+ const ref = operation.kind === "update-content" ? operation.ref : {
1827
+ scope: "survey",
1828
+ surface: operation.surface,
1829
+ contentKey: operation.contentKey
1830
+ };
1831
+ if (stableStringify(readLocalizationContent(before, ref, operation.locale)) === stableStringify(readLocalizationContent(after, ref, operation.locale))) warnings.push({
1832
+ severity: "warning",
1833
+ code: "no-content-change",
1834
+ operationIndex,
1835
+ itemId: ref.scope === "content" ? after.owners.get(ref.ownerId)?.itemId : void 0,
1836
+ message: "The localized content already has this value."
1837
+ });
1838
+ }
1839
+ if (!allowExpressionJsonPatches && (operation.kind === "survey-json-patch" || operation.kind === "patch-element-config" || operation.kind === "patch-item-config") && expressionsKey(before) !== expressionsKey(after)) {
1840
+ fail("expression-patch-requires-specialized-tool", "Use survey_expression for condition, validation, prefill and template expression changes", operationIndex);
1841
+ break;
1842
+ }
1843
+ requireCapabilities(operation, before, after, caps);
1844
+ if (operation.kind !== "add-survey-locale") for (const locale of Object.keys(after.data.translations ?? {})) {
1845
+ if (Object.hasOwn(before.data.translations ?? {}, locale)) continue;
1846
+ warnings.push({
1847
+ severity: "warning",
1848
+ code: "introduced-locale",
1849
+ operationIndex,
1850
+ operationId: operation.id,
1851
+ message: `Locale "${locale}" is not currently present and will be introduced by this translation change. Confirm that this is the intended locale code.`
1852
+ });
1853
+ }
1854
+ const oldExpressions = new Set(before.expressions.map((entry) => stableStringify([getDiagnosticLocation(before, entry.path), entry.expression])));
1855
+ for (const entry of after.expressions) if (!oldExpressions.has(stableStringify([getDiagnosticLocation(after, entry.path), entry.expression]))) validateExpression({
1856
+ expression: entry.expression,
1857
+ expectedReturnType: [
1858
+ "visibility",
1859
+ "disabled",
1860
+ "when"
1861
+ ].includes(String(entry.path.at(-1))) || entry.path.at(-1) === "expression" && entry.path.includes("validationRules") ? "boolean" : void 0,
1862
+ issues,
1863
+ operationIndex,
1864
+ responseSlotTypes: Object.fromEntries(after.slots.definitions.map((slot) => [slot.slotId, slot.valueType])),
1865
+ stateTargets: new Set(after.owners.keys()),
1866
+ scope: JSON.stringify(entry.path)
1867
+ });
1868
+ if (issues.length) break;
1869
+ normalizedOperations.push(operation);
1870
+ } catch (error) {
1871
+ if (error instanceof DefinitionError) issues.push(...error.diagnostics.filter((issue) => issue.severity === "error").map((issue) => ({
1872
+ severity: "error",
1873
+ code: issue.code,
1874
+ message: `${JSON.stringify(issue.path)}: ${issue.message}`,
1875
+ operationIndex
1876
+ })));
1877
+ else if (error instanceof JsonPatchError) {
1878
+ const patch = "patches" in unparsed ? unparsed.patches[error.operationIndex ?? 0] : void 0;
1879
+ const location = patch ? `${unparsed.kind === "survey-json-patch" ? "Survey" : "Config"} patch ${(error.operationIndex ?? 0) + 1} (${patch.op} ${patch.path}): ` : "";
1880
+ const hint = error.code === "path-not-found" && patch?.op === "replace" ? " Use add for a new property; replace requires an existing property." : "";
1881
+ fail(error.code, `${location}${error.message}${hint}`, operationIndex);
1882
+ } else if (error instanceof z.ZodError) fail("invalid-operation", error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("\n"), operationIndex);
1883
+ else fail("invalid-operation", error instanceof Error ? error.message : String(error), operationIndex);
1884
+ break;
1885
+ }
1886
+ if (!issues.length) try {
1887
+ session.assertNoDanglingRemovals();
1888
+ } catch (error) {
1889
+ if (!(error instanceof DefinitionError)) throw error;
1890
+ issues.push(...error.diagnostics.map((issue) => ({
1891
+ severity: "error",
1892
+ code: issue.code,
1893
+ message: `${JSON.stringify(issue.path)}: ${issue.message}`
1894
+ })));
1895
+ }
1896
+ if (!issues.length) {
1897
+ issues.push(...getIntroducedMissingAuthoringContent(snapshot.model, session.model));
1898
+ new SurveyStateGraph(session.model, { admission: "draft" });
1899
+ const existingExports = new Set(getDefaultProjectionIssues(snapshot.model));
1900
+ for (const message of getDefaultProjectionIssues(session.model)) if (!existingExports.has(message)) fail("invalid-response-export", message);
1901
+ for (const issue of getIntroducedDefinitionIssues(snapshot.model, session.model, snapshot.ui.selectedLocale)) if (issue.severity === "error") issues.push(issue);
1902
+ else warnings.push(issue);
1903
+ }
1904
+ } catch (error) {
1905
+ fail("invalid-survey-definition", error instanceof Error ? error.message : String(error));
1906
+ }
1907
+ return {
1908
+ valid: issues.length === 0,
1909
+ issues,
1910
+ warnings,
1911
+ normalizedOperations
1912
+ };
1913
+ }
1914
+ const createSurveyChangeSet = ({ allowExpressionJsonPatches, maxOperations, operations, snapshot, summary, turnId }) => {
1915
+ const validation = validateSurveyOperations({
1916
+ allowExpressionJsonPatches,
1917
+ maxOperations,
1918
+ operations,
1919
+ snapshot
1920
+ });
1921
+ return {
1922
+ protocolVersion: 1,
1923
+ id: generateId(),
1924
+ turnId,
1925
+ status: validation.valid ? "validated" : "rejected",
1926
+ summary: summary.trim(),
1927
+ operations: validation.normalizedOperations,
1928
+ precondition: snapshot ? buildSurveyDocumentVersion(snapshot) : void 0,
1929
+ validation,
1930
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1931
+ };
1932
+ };
1933
+ //#endregion
1934
+ //#region src/engine/operation-state.ts
1935
+ const getGroupChildren = (item) => item?.itemType === "group" ? item.config.items : [];
1936
+ //#endregion
1937
+ //#region src/engine/audit.ts
1938
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1939
+ const QUALITY_PRINCIPLES = [
1940
+ {
1941
+ id: "purpose-alignment",
1942
+ title: "Purpose and construct alignment",
1943
+ prompt: "Does every question serve a stated objective, and does it measure the intended construct?"
1944
+ },
1945
+ {
1946
+ id: "cognitive-burden",
1947
+ title: "Respondent burden",
1948
+ prompt: "Is the survey as short and cognitively simple as its purpose allows?"
1949
+ },
1950
+ {
1951
+ id: "question-wording",
1952
+ title: "Clear, neutral wording",
1953
+ prompt: "Are questions specific, neutral, answerable, and limited to one idea without assumptions or double negatives?"
1954
+ },
1955
+ {
1956
+ id: "recall",
1957
+ title: "Recall and reference periods",
1958
+ prompt: "Are recall periods explicit, realistic, and consistent with what respondents can remember?"
1959
+ },
1960
+ {
1961
+ id: "response-options",
1962
+ title: "Response option quality",
1963
+ prompt: "Are options mutually exclusive, collectively sufficient, balanced, ordered, and consistently scaled?"
1964
+ },
1965
+ {
1966
+ id: "key-semantic-consistency",
1967
+ title: "Variable-name and export-code consistency",
1968
+ prompt: "Do response variable names and option export codes follow the survey's established convention and avoid contradicting the content they currently identify? Generic, numeric, or opaque keys are acceptable when that is the survey's consistent pattern; flag descriptive keys when they have become stale or misleading, and mixed conventions when the inconsistency appears accidental."
1969
+ },
1970
+ {
1971
+ id: "flow",
1972
+ title: "Flow and completeness",
1973
+ prompt: "Does the order move logically from easy/general to specific, with needed instructions and no important objective omitted?"
1974
+ },
1975
+ {
1976
+ id: "routing",
1977
+ title: "Requiredness and routing",
1978
+ prompt: "Do required fields, branching, validation, and not-applicable paths avoid forcing inaccurate answers?"
1979
+ },
1980
+ {
1981
+ id: "sensitivity",
1982
+ title: "Sensitivity and ethics",
1983
+ prompt: "Are sensitive questions justified, respectfully phrased, appropriately optional, and paired with privacy context where needed?"
1984
+ },
1985
+ {
1986
+ id: "accessibility",
1987
+ title: "Accessibility and inclusion",
1988
+ prompt: "Is language plain and inclusive, and are content, controls, and images understandable without avoidable barriers?"
1989
+ },
1990
+ {
1991
+ id: "localization",
1992
+ title: "Cross-language equivalence",
1993
+ prompt: "Do translations preserve meaning and cultural relevance rather than merely matching words?"
1994
+ },
1995
+ {
1996
+ id: "testing",
1997
+ title: "Testing and validation",
1998
+ prompt: "Has the instrument been cognitively tested, piloted, and checked for reliability or validity where the intended use requires it?"
1999
+ }
2000
+ ];
2001
+ const collectKeyReviewEntries = (context, sourceLocale) => {
2002
+ const model = context.model;
2003
+ const entries = [];
2004
+ const text = (refs) => refs.map((ref) => getContentPlainText(readLocalizationContent(model, ref, sourceLocale)).trim()).filter(Boolean).join(" / ");
2005
+ for (const slot of model.slots.definitions) {
2006
+ const binding = model.elements.get(slot.elementId).facts.bindings.find((entry) => entry.slotId === slot.slotId);
2007
+ const resolved = resolveElementName(model, slot.elementId, [sourceLocale]);
2008
+ const refs = binding?.labelRefs?.map((ref) => ({
2009
+ scope: "content",
2010
+ ...ref
2011
+ })) ?? (resolved ? [{
2012
+ scope: "content",
2013
+ ownerId: resolved.ownerId,
2014
+ role: resolved.role
2015
+ }] : []);
2016
+ entries.push({
2017
+ kind: "response-variable",
2018
+ slotId: slot.slotId,
2019
+ valueType: slot.valueType,
2020
+ computed: !!slot.computed,
2021
+ itemId: slot.itemId,
2022
+ itemKey: model.resolveItemEditorName(slot.itemId, sourceLocale),
2023
+ key: slot.variableName,
2024
+ keySource: "explicit",
2025
+ localizationRefs: refs,
2026
+ content: text(refs)
2027
+ });
2028
+ const codes = new Map(Object.entries(slot.referenceValueKeys ?? {}));
2029
+ const declaredLabels = new Map(Object.entries(new Map(Object.entries(slot.referenceValueLabels ?? {})).get(sourceLocale) ?? {}));
2030
+ const optionIds = /* @__PURE__ */ new Set([...(slot.allowedValues ?? []).flatMap((value) => value.type === "reference" ? [value.value] : value.type === "reference[]" ? value.value : []), ...codes.keys()]);
2031
+ for (const id of optionIds) {
2032
+ const code = codes.get(id);
2033
+ const explicit = !!code?.trim();
2034
+ const optionRefs = model.owners.get(id)?.roles.some((role) => role.role === "label") ? [{
2035
+ scope: "content",
2036
+ ownerId: id,
2037
+ role: "label"
2038
+ }] : [];
2039
+ entries.push({
2040
+ kind: "choice-option",
2041
+ slotId: slot.slotId,
2042
+ itemId: slot.itemId,
2043
+ itemKey: model.resolveItemEditorName(slot.itemId, sourceLocale),
2044
+ key: explicit ? code : id,
2045
+ keySource: explicit ? "explicit" : "fallback-id",
2046
+ localizationRefs: optionRefs,
2047
+ content: optionRefs.length ? text(optionRefs) : declaredLabels.get(id)
2048
+ });
2049
+ }
2050
+ }
2051
+ return entries;
2052
+ };
2053
+ const auditSurvey = (context, options = {}) => {
2054
+ const findings = [];
2055
+ const add = (finding) => findings.push(finding);
2056
+ const items = context.survey.surveyItems;
2057
+ const itemTypes = {}, elementTypes = {};
2058
+ for (const item of items) setOwnValue(itemTypes, item.itemType, (ownValue(itemTypes, item.itemType) ?? 0) + 1);
2059
+ for (const element of context.model.elements.values()) setOwnValue(elementTypes, element.node.elementType, (ownValue(elementTypes, element.node.elementType) ?? 0) + 1);
2060
+ const locales = context.inspection.locales;
2061
+ const sourceLocale = options.sourceLocale ?? context.ui.selectedLocale ?? locales[0];
2062
+ const targetLocales = options.targetLocales ?? (options.sourceLocale ? locales.filter((locale) => locale !== sourceLocale) : locales);
2063
+ const allKeyReviewEntries = collectKeyReviewEntries(context, sourceLocale);
2064
+ const keyReviewOffset = Math.min(options.keyReviewOffset ?? 0, allKeyReviewEntries.length);
2065
+ const keyReviewLimit = options.keyReviewLimit ?? 30;
2066
+ const keyReviewEntries = allKeyReviewEntries.slice(keyReviewOffset, keyReviewOffset + keyReviewLimit);
2067
+ for (const entry of listLocalizationEntries(context.model, { sourceLocale: options.sourceLocale })) for (const locale of targetLocales) if (!hasLocalizationText(ownValue(entry.contents, locale)) && Object.entries(entry.contents).some(([other, content]) => other !== locale && hasLocalizationText(content))) add({
2068
+ code: "missing-translation",
2069
+ severity: "warning",
2070
+ category: "localization",
2071
+ ...entry.ref.scope === "content" ? { itemId: context.model.owners.get(entry.ref.ownerId)?.itemId } : {},
2072
+ locale,
2073
+ localizationRefs: [entry.ref],
2074
+ message: `Content "${entry.ref.scope === "content" ? entry.ref.role : entry.ref.contentKey}" is available in another language but has no ${locale} translation.`
2075
+ });
2076
+ for (const element of context.model.elements.values()) {
2077
+ const emptyControls = /* @__PURE__ */ new Set();
2078
+ for (const binding of element.facts.bindings) {
2079
+ if (!binding.selection || binding.selection.options.length || emptyControls.has(binding.controlId)) continue;
2080
+ emptyControls.add(binding.controlId);
2081
+ add({
2082
+ code: "choice-without-options",
2083
+ severity: "error",
2084
+ category: "content",
2085
+ itemId: element.itemId,
2086
+ path: context.model.owners.get(binding.controlId)?.path ?? element.path,
2087
+ message: `Choice input "${binding.controlId}" has no options.`,
2088
+ suggestion: "Add at least one option with localized wording, or remove the input."
2089
+ });
2090
+ }
2091
+ }
2092
+ for (const diagnostic of getDefinitionDiagnostics(context.model, sourceLocale)) add({
2093
+ code: diagnostic.code,
2094
+ severity: diagnostic.severity,
2095
+ category: diagnostic.code.startsWith("missing-") ? "content" : "logic",
2096
+ message: diagnostic.message,
2097
+ itemId: diagnostic.ownerItemId,
2098
+ path: diagnostic.path,
2099
+ suggestion: getSurveyAssistantExpressionDiagnosticGuidance(diagnostic)
2100
+ });
2101
+ for (const issue of getAssistantResponseSchemaIssues(context.survey, context.trustedCapabilities)) if (!findings.some((finding) => finding.code === issue.code && finding.message === issue.message)) add({
2102
+ ...issue,
2103
+ category: "responses"
2104
+ });
2105
+ const assets = context.survey.assets ?? {};
2106
+ for (const [assetId, asset] of Object.entries(assets)) {
2107
+ const metadata = isRecord$1(asset.metadata) ? asset.metadata : {};
2108
+ if (asset.kind === "image" && typeof metadata.alt !== "string") add({
2109
+ code: "missing-image-alt",
2110
+ severity: "warning",
2111
+ category: "assets",
2112
+ message: `Image asset "${assetId}" has no alternative text.`,
2113
+ suggestion: "Add concise alt text, or an empty alt value when the image is purely decorative."
2114
+ });
2115
+ }
2116
+ const limited = findings.slice(0, options.maxFindings ?? 200);
2117
+ return {
2118
+ summary: findings.reduce((acc, entry) => {
2119
+ acc[entry.severity] += 1;
2120
+ acc.total += 1;
2121
+ return acc;
2122
+ }, {
2123
+ error: 0,
2124
+ warning: 0,
2125
+ info: 0,
2126
+ total: 0
2127
+ }),
2128
+ findingCount: findings.length,
2129
+ truncated: limited.length < findings.length,
2130
+ metrics: {
2131
+ itemCount: context.inspection.itemCount,
2132
+ localeCount: locales.length,
2133
+ responseSlotCount: context.inspection.responseSlots.length,
2134
+ assetCount: Object.keys(assets).length,
2135
+ itemTypes,
2136
+ elementTypes
2137
+ },
2138
+ findings: limited,
2139
+ qualityReviewPrinciples: QUALITY_PRINCIPLES,
2140
+ keyReview: {
2141
+ sourceLocale,
2142
+ entryCount: allKeyReviewEntries.length,
2143
+ offset: keyReviewOffset,
2144
+ returnedEntryCount: keyReviewEntries.length,
2145
+ truncated: keyReviewOffset + keyReviewEntries.length < allKeyReviewEntries.length,
2146
+ entries: keyReviewEntries,
2147
+ note: "These key/content pairs are evidence for qualitative review, not deterministic defects. Judge them against the survey's own naming pattern; generic or opaque keys are not inherently problematic."
2148
+ },
2149
+ note: "Technical findings are deterministic. Quality principles are a structured review rubric; apply them with human/model judgment and domain context, and do not present them as psychometric validation."
2150
+ };
2151
+ };
2152
+ //#endregion
2153
+ //#region src/engine/mutation-recovery.ts
2154
+ const UNIDENTIFIED_MUTATION_TARGET = "unidentified-mutation";
2155
+ const SATISFIED_MUTATION_TARGET = "satisfied-mutation";
2156
+ const SATISFIED_ISSUES = /* @__PURE__ */ new Set([
2157
+ "survey-locale-already-exists",
2158
+ "item-already-in-target-parent",
2159
+ "no-expression-patches"
2160
+ ]);
2161
+ const VALUE_ONLY_ISSUES = /* @__PURE__ */ new Set([
2162
+ "invalid-mutation-value",
2163
+ "target-index-after-end",
2164
+ "option-index-after-end",
2165
+ "missing-create-content",
2166
+ "missing-choice-options",
2167
+ "missing-choice-option-label",
2168
+ "missing-choice-option-labels",
2169
+ "missing-form-field-content",
2170
+ "missing-form-field-labels",
2171
+ "missing-form-select-option-labels",
2172
+ "missing-embedded-form-field-labels",
2173
+ "missing-field-groups-wrapper",
2174
+ "invalid-form-layout-columns",
2175
+ "missing-expression",
2176
+ "invalid-expression",
2177
+ "unknown-response-ref",
2178
+ "expression-function-signature-mismatch",
2179
+ "expression-return-type-mismatch",
2180
+ "missing-date-format",
2181
+ "image-asset-still-referenced"
2182
+ ]);
2183
+ const exceptionalRecoveryTarget = (operationIndex, issues) => {
2184
+ const errors = issues.filter((issue) => issue.severity === "error" && (issue.operationIndex === operationIndex || issue.operationIndex === void 0));
2185
+ if (errors.length === 0) return void 0;
2186
+ if (errors.every((issue) => SATISFIED_ISSUES.has(issue.code))) return SATISFIED_MUTATION_TARGET;
2187
+ if (!errors.every((issue) => VALUE_ONLY_ISSUES.has(issue.code))) return UNIDENTIFIED_MUTATION_TARGET;
2188
+ };
2189
+ const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
2190
+ const at = (value, path) => path.split(".").reduce((entry, key) => record(entry)[key], value);
2191
+ const identityFields = {
2192
+ "create-item": ["target.parentItemId", "item.id"],
2193
+ "insert-element": ["target.layoutId", "element.id"],
2194
+ "insert-preset": [
2195
+ "elementType",
2196
+ "presetId",
2197
+ "locale",
2198
+ "target.layoutId?",
2199
+ "target.parentItemId?",
2200
+ "target.index?"
2201
+ ],
2202
+ "insert-blocks": ["bodyOwnerId"],
2203
+ "update-content": [
2204
+ "ref.scope",
2205
+ "ref.ownerId?",
2206
+ "ref.role?",
2207
+ "ref.surface?",
2208
+ "ref.contentKey?",
2209
+ "locale"
2210
+ ],
2211
+ "update-placed-image": [
2212
+ "ref.scope",
2213
+ "ref.ownerId?",
2214
+ "ref.role?",
2215
+ "ref.surface?",
2216
+ "ref.contentKey?",
2217
+ "locale",
2218
+ "imageIndex"
2219
+ ],
2220
+ "patch-element-config": ["elementId"],
2221
+ "patch-item-config": ["itemId"],
2222
+ "update-item-label": ["itemId"],
2223
+ "update-item-color": ["itemId"],
2224
+ "remove-item": ["itemId"],
2225
+ "move-item": ["itemId"],
2226
+ "reorder-items": ["parentItemId"],
2227
+ "move-element": ["elementId"],
2228
+ "move-block": ["blockId"],
2229
+ "remove-content": ["ownerId"],
2230
+ "update-layout": ["layoutId"],
2231
+ "update-response-settings": ["naming", "settings"],
2232
+ "set-image-asset": ["assetId"],
2233
+ "update-image-asset-metadata": ["assetId"],
2234
+ "remove-image-asset": ["assetId"],
2235
+ "update-survey-metadata": [],
2236
+ "update-survey-pagination": [],
2237
+ "update-survey-translation": [
2238
+ "locale",
2239
+ "surface",
2240
+ "contentKey"
2241
+ ],
2242
+ "add-survey-locale": ["locale"],
2243
+ "remove-survey-locale": ["locale"],
2244
+ "survey-json-patch": []
2245
+ };
2246
+ const identities = (kind, value, fields, context) => {
2247
+ const parts = [kind];
2248
+ for (const field of fields) {
2249
+ const optional = field.endsWith("?");
2250
+ const path = optional ? field.slice(0, -1) : field;
2251
+ let part = at(value, path);
2252
+ if (typeof part === "string" && context && /(?:^|\.)(?:itemId|parentItemId)$/.test(path)) {
2253
+ const direct = context.survey.surveyItems.find((item) => item.id === part);
2254
+ if (!direct) return void 0;
2255
+ part = direct.id;
2256
+ }
2257
+ if (part === void 0 && optional) parts.push(null);
2258
+ else if (typeof part === "string" && part.length > 0 || (path === "imageIndex" || path.endsWith("index")) && typeof part === "number" && Number.isInteger(part) && part >= 0) parts.push(part);
2259
+ else return void 0;
2260
+ }
2261
+ return parts;
2262
+ };
2263
+ const collectSurveyOperationRecoveryTargets = (operations, context, issues = []) => {
2264
+ const targets = operations.flatMap((operation, operationIndex) => {
2265
+ const value = record(operation);
2266
+ const kind = value.kind;
2267
+ if (typeof kind !== "string" || !Object.hasOwn(identityFields, kind)) return [UNIDENTIFIED_MUTATION_TARGET];
2268
+ if (kind === "survey-json-patch" || kind === "patch-item-config") return [UNIDENTIFIED_MUTATION_TARGET];
2269
+ const exceptional = exceptionalRecoveryTarget(operationIndex, issues);
2270
+ if (exceptional) return [exceptional];
2271
+ if (kind === "update-response-settings") {
2272
+ const targets = ["naming", "settings"].flatMap((surface) => Array.isArray(value[surface]) ? value[surface].map((edit) => {
2273
+ const entry = record(edit);
2274
+ const identity = entry.slotId ?? entry.elementId;
2275
+ return typeof identity === "string" && identity.length ? JSON.stringify([
2276
+ kind,
2277
+ surface,
2278
+ identity
2279
+ ]) : UNIDENTIFIED_MUTATION_TARGET;
2280
+ }) : []);
2281
+ return targets.length ? targets : [UNIDENTIFIED_MUTATION_TARGET];
2282
+ }
2283
+ const parts = identities(kind, value, identityFields[kind], context);
2284
+ if (!parts) return [UNIDENTIFIED_MUTATION_TARGET];
2285
+ const fields = kind === "update-survey-metadata" ? value.values : kind === "update-image-asset-metadata" ? value.metadata : kind === "update-placed-image" ? value.changes : void 0;
2286
+ if ([
2287
+ "update-survey-metadata",
2288
+ "update-image-asset-metadata",
2289
+ "update-placed-image"
2290
+ ].includes(kind)) {
2291
+ const keys = Object.keys(record(fields));
2292
+ return keys.length > 0 ? keys.map((key) => JSON.stringify([...parts, key])) : [UNIDENTIFIED_MUTATION_TARGET];
2293
+ }
2294
+ return [JSON.stringify(parts)];
2295
+ });
2296
+ return new Set(targets.length > 0 ? targets : [UNIDENTIFIED_MUTATION_TARGET]);
2297
+ };
2298
+ const expressionIdentityFields = {
2299
+ "set-condition": ["ownerId", "property"],
2300
+ "clear-condition": ["ownerId", "property"],
2301
+ "set-config-expression": ["elementId", "path"],
2302
+ "clear-config-expression": ["elementId", "path"],
2303
+ "set-validation-rule": ["itemId", "rule.id"],
2304
+ "remove-validation-rule": ["itemId", "ruleId"],
2305
+ "set-prefill": [
2306
+ "elementId",
2307
+ "slotId",
2308
+ "prefill.id"
2309
+ ],
2310
+ "clear-prefill": [
2311
+ "elementId",
2312
+ "slotId",
2313
+ "prefillId"
2314
+ ],
2315
+ "set-template-value": ["templateKey"],
2316
+ "clear-template-value": ["templateKey"]
2317
+ };
2318
+ const collectExpressionMutationRecoveryTargets = (mutations, context, issues = []) => [...new Set(mutations.map((mutation, operationIndex) => {
2319
+ const kind = record(mutation).kind;
2320
+ if (typeof kind !== "string" || !Object.hasOwn(expressionIdentityFields, kind)) return UNIDENTIFIED_MUTATION_TARGET;
2321
+ const parsed = expressionMutationSchema.safeParse(mutation);
2322
+ if (!parsed.success && !parsed.error.issues.every((issue) => issue.path[0] === "expression" || issue.path[0] === "templateValue" || issue.path[0] === "rule" && issue.path.length > 1 && issue.path[1] !== "id" || issue.path[0] === "prefill" && issue.path.length > 1 && issue.path[1] !== "id")) return UNIDENTIFIED_MUTATION_TARGET;
2323
+ const exceptional = exceptionalRecoveryTarget(operationIndex, issues);
2324
+ if (exceptional) return exceptional;
2325
+ const parts = identities(kind, mutation, expressionIdentityFields[kind], context);
2326
+ return parts ? JSON.stringify(parts) : UNIDENTIFIED_MUTATION_TARGET;
2327
+ }))];
2328
+ /** Classify a malformed batch without staging any of its parseable siblings.
2329
+ * The canonical validator sees those siblings in order, retaining dependencies;
2330
+ * all diagnostic indexes are mapped back to the original, unfiltered input.
2331
+ */
2332
+ const collectMalformedSurveyOperationRecoveryTargets = (operations, context, normalizationIssues = []) => {
2333
+ const parsedOperations = [];
2334
+ const originalIndexes = [];
2335
+ const issues = [];
2336
+ operations.forEach((operation, operationIndex) => {
2337
+ const parsed = surveyOperationSchema.safeParse(operation);
2338
+ const normalizationFailed = normalizationIssues.some((issue) => Number(issue.path.split(".")[0]) === operationIndex);
2339
+ if (parsed.success && !normalizationFailed) {
2340
+ if (parsed.data.kind === "survey-json-patch") return;
2341
+ parsedOperations.push(parsed.data);
2342
+ originalIndexes.push(operationIndex);
2343
+ return;
2344
+ }
2345
+ const value = record(operation);
2346
+ const kind = value.kind;
2347
+ const valueField = kind === "update-item-label" ? "itemLabel" : kind === "update-item-color" ? "color" : void 0;
2348
+ const parts = typeof kind === "string" && Object.hasOwn(identityFields, kind) ? identities(kind, value, identityFields[kind], context) : void 0;
2349
+ const supported = !!context?.survey.surveyItems.find((entry) => entry.id === parts?.[1]);
2350
+ const valueOnly = !normalizationFailed && !parsed.success && valueField && supported && parsed.error.issues.every((issue) => issue.path[0] === valueField);
2351
+ issues.push({
2352
+ severity: "error",
2353
+ operationIndex,
2354
+ code: valueOnly ? "invalid-mutation-value" : "invalid-operation-shape",
2355
+ message: "Malformed operation recovery classification."
2356
+ });
2357
+ });
2358
+ if (parsedOperations.length > 0) {
2359
+ const validation = validateSurveyOperations({
2360
+ operations: parsedOperations,
2361
+ snapshot: context
2362
+ });
2363
+ issues.push(...validation.issues.map((issue) => ({
2364
+ ...issue,
2365
+ ...issue.operationIndex !== void 0 ? { operationIndex: originalIndexes[issue.operationIndex] } : {}
2366
+ })));
2367
+ }
2368
+ return collectSurveyOperationRecoveryTargets(operations, context, issues);
2369
+ };
2370
+ /** Later validation can disprove an earlier target assumption. Refine only the
2371
+ * same attempted operation's exact identities; this does not discharge work.
2372
+ */
2373
+ const collectRecoveryTargetRefinements = (values, context, issues, collect) => values.flatMap((value, operationIndex) => {
2374
+ const operationIssues = issues.filter((issue) => issue.operationIndex === operationIndex || issue.operationIndex === void 0).map((issue) => ({
2375
+ ...issue,
2376
+ operationIndex: 0
2377
+ }));
2378
+ const before = [...collect([value], context)];
2379
+ const replacement = [...collect([value], context, operationIssues)].find((target) => target === "unidentified-mutation" || target === "satisfied-mutation");
2380
+ const targets = before.filter((target) => target !== "unidentified-mutation" && target !== "satisfied-mutation");
2381
+ return replacement && targets.length ? [{
2382
+ targets,
2383
+ replacement
2384
+ }] : [];
2385
+ });
2386
+ const collectSurveyOperationRecoveryRefinements = (values, context, issues) => collectRecoveryTargetRefinements(values, context, issues, collectSurveyOperationRecoveryTargets);
2387
+ const collectExpressionMutationRecoveryRefinements = (values, context, issues) => collectRecoveryTargetRefinements(values, context, issues, collectExpressionMutationRecoveryTargets);
2388
+ /** Refine only resource identities present before a successful delta and absent
2389
+ * after it. Values, other items' components, and opaque recovery markers do not
2390
+ * count as references. Comparing canonical states also covers raw deletions.
2391
+ */
2392
+ const collectRemovedRecoveryTargetRefinements = (targets, before, after) => {
2393
+ const resourceExists = (field, id, context) => {
2394
+ if (typeof id !== "string") return false;
2395
+ if (/(?:^|\.)(?:itemId|parentItemId)$/.test(field)) return context.survey.surveyItems.some((item) => item.id === id);
2396
+ if (field === "locale") return Object.hasOwn(context.survey.translations ?? {}, id);
2397
+ if (field === "assetId") return Object.hasOwn(context.survey.assets ?? {}, id);
2398
+ if (/(?:^|\.)(?:ownerId|bodyOwnerId|elementId|layoutId|blockId|slotId)$/.test(field)) return context.model.identities.has(id);
2399
+ return false;
2400
+ };
2401
+ return [...new Set(targets)].flatMap((target) => {
2402
+ if (!target.startsWith("[")) return [];
2403
+ let parts;
2404
+ try {
2405
+ parts = JSON.parse(target);
2406
+ } catch {
2407
+ return [];
2408
+ }
2409
+ if (!Array.isArray(parts) || typeof parts[0] !== "string") return [];
2410
+ const kind = parts[0];
2411
+ const fields = Object.hasOwn(identityFields, kind) ? identityFields[kind] : Object.hasOwn(expressionIdentityFields, kind) ? expressionIdentityFields[kind] : void 0;
2412
+ if (!fields) return [];
2413
+ const bindings = Object.fromEntries(fields.map((field, index) => [field.replace(/\?$/, ""), parts[index + 1]]));
2414
+ return Object.entries(bindings).some(([field, id]) => resourceExists(field, id, before) && !resourceExists(field, id, after)) ? [{
2415
+ targets: [target],
2416
+ replacement: UNIDENTIFIED_MUTATION_TARGET
2417
+ }] : [];
2418
+ });
2419
+ };
2420
+ //#endregion
2421
+ //#region src/engine/turn-survey-draft.ts
2422
+ const createId = () => globalThis.crypto.randomUUID();
2423
+ const operationTargets = (operation, context) => {
2424
+ const ownerTarget = (id) => {
2425
+ const identity = context?.model.identities.get(id), owner = context?.model.owners.get(id) ?? (identity?.ownerId ? context?.model.owners.get(identity.ownerId) : void 0);
2426
+ return owner ? [`item:${owner.itemId}`] : [`owner:${id}`];
2427
+ };
2428
+ switch (operation.kind) {
2429
+ case "create-item": return [`item:${operation.item.id}`];
2430
+ case "insert-element": return ownerTarget(operation.target.layoutId);
2431
+ case "insert-preset": return "layoutId" in operation.target ? ownerTarget(operation.target.layoutId) : [`item:${operation.target.parentItemId}`];
2432
+ case "insert-blocks":
2433
+ case "move-block": return [...ownerTarget(operation.bodyOwnerId), ...operation.kind === "move-block" ? ownerTarget(operation.blockId) : []];
2434
+ case "update-layout": return ownerTarget(operation.layoutId);
2435
+ case "remove-content": return ownerTarget(operation.ownerId);
2436
+ case "move-element": return [...ownerTarget(operation.elementId), ...ownerTarget(operation.target.layoutId)];
2437
+ case "patch-element-config": return ownerTarget(operation.elementId);
2438
+ case "update-content":
2439
+ case "update-placed-image": return operation.ref.scope === "content" ? ownerTarget(operation.ref.ownerId) : ["survey"];
2440
+ case "update-item-label":
2441
+ case "update-item-color":
2442
+ case "patch-item-config":
2443
+ case "remove-item":
2444
+ case "move-item": return [`item:${operation.itemId}`];
2445
+ case "reorder-items": return [`item:${operation.parentItemId}`];
2446
+ case "set-image-asset":
2447
+ case "update-image-asset-metadata":
2448
+ case "remove-image-asset": return [`asset:${operation.assetId}`];
2449
+ case "update-response-settings": return [...new Set(context?.responseVariables.filter((slot) => operation.naming.some((edit) => "slotId" in edit ? edit.slotId === slot.slotId : edit.elementId === slot.elementId) || operation.settings.some((edit) => edit.slotId === slot.slotId)).map((slot) => `item:${slot.itemId}`) ?? ["survey"])];
2450
+ case "update-survey-metadata":
2451
+ case "update-survey-pagination":
2452
+ case "update-survey-translation":
2453
+ case "add-survey-locale":
2454
+ case "remove-survey-locale": return ["survey"];
2455
+ case "survey-json-patch": return operation.patches.map((patch) => `path:${patch.path}`);
2456
+ default: throw new Error(String(operation));
2457
+ }
2458
+ };
2459
+ const collectOperationTargets = (operations, context) => new Set(operations.flatMap((operation) => operationTargets(operation, context)));
2460
+ const mutationRecoveryRouteByIssueCode = { "expression-patch-requires-specialized-tool": "expression" };
2461
+ const mutationRouteTarget = (route) => `mutation-route:${route}`;
2462
+ const collectExclusiveMutationRecoveryTargets = (changeSet) => {
2463
+ const errorIssues = changeSet.validation.issues.filter((issue) => issue.severity === "error");
2464
+ if (errorIssues.length === 0) return void 0;
2465
+ const routes = errorIssues.map((issue) => mutationRecoveryRouteByIssueCode[issue.code]);
2466
+ const knownRoutes = routes.filter((route) => route !== void 0);
2467
+ if (knownRoutes.length !== routes.length) return void 0;
2468
+ return new Set(knownRoutes.map(mutationRouteTarget));
2469
+ };
2470
+ const MAX_POST_CHANGE_TARGETS = 16;
2471
+ const MAX_POST_CHANGE_ITEMS = 6;
2472
+ const MAX_POST_CHANGE_CHILDREN = 16;
2473
+ const MAX_POST_CHANGE_TRANSLATIONS = 12;
2474
+ const MAX_POST_CHANGE_RESPONSE_SLOTS = 12;
2475
+ const MAX_POST_CHANGE_TEXT_CHARS = 100;
2476
+ const MAX_VERIFICATION_AUDIT_FINDINGS = 8;
2477
+ const MAX_VERIFICATION_MESSAGE_CHARS = 240;
2478
+ const SUBSTANTIAL_CHANGE_ITEM_COUNT = 6;
2479
+ const SUBSTANTIAL_CHANGE_OPERATION_COUNT = 12;
2480
+ const createPostChangeProjection = (context, targets, before, reorderedParentIds) => {
2481
+ const beforeItems = new Map(before?.survey.surveyItems.map((item) => [item.id, item]));
2482
+ const previewedChildOrders = new Set(reorderedParentIds);
2483
+ for (const item of context.survey.surveyItems) {
2484
+ if (item.itemType !== "group") continue;
2485
+ const previous = beforeItems.get(item.id);
2486
+ const children = getGroupChildren(item);
2487
+ const previousChildren = getGroupChildren(previous);
2488
+ if (!previous || children.length !== previousChildren.length || children.some((id, index) => id !== previousChildren[index])) previewedChildOrders.add(item.id);
2489
+ }
2490
+ const allTargets = [.../* @__PURE__ */ new Set([...targets, ...[...previewedChildOrders].map((id) => `item:${id}`)])];
2491
+ const requestedItemIds = allTargets.filter((target) => target.startsWith("item:")).map((target) => target.slice(5));
2492
+ const itemEntries = requestedItemIds.map((itemId) => context.inspection.itemIndex.find((entry) => entry.itemId === itemId)).filter((entry) => entry !== void 0);
2493
+ const itemsById = new Map(context.survey.surveyItems.map((item) => [item.id, item]));
2494
+ const outlineById = new Map(context.inspection.outline.map((item) => [item.itemId, item]));
2495
+ let truncated = allTargets.length > MAX_POST_CHANGE_TARGETS || itemEntries.length > MAX_POST_CHANGE_ITEMS;
2496
+ const touchedItems = itemEntries.slice(0, MAX_POST_CHANGE_ITEMS).map((entry) => {
2497
+ const ownerIds = new Set([...context.model.owners.values()].filter((owner) => owner.itemId === entry.itemId).map((owner) => owner.id));
2498
+ const allTranslations = Object.entries(context.survey.translations ?? {}).flatMap(([locale, translation]) => Object.entries(translation.content).filter(([ownerId]) => ownerIds.has(ownerId)).flatMap(([ownerId, roles]) => Object.entries(roles).map(([role, content]) => {
2499
+ const characters = [...getContentPlainText(content)];
2500
+ return {
2501
+ locale,
2502
+ ownerId,
2503
+ role,
2504
+ textPreview: characters.slice(0, MAX_POST_CHANGE_TEXT_CHARS).join(""),
2505
+ textLength: characters.length,
2506
+ truncated: characters.length > MAX_POST_CHANGE_TEXT_CHARS
2507
+ };
2508
+ })));
2509
+ const translations = allTranslations.slice(0, MAX_POST_CHANGE_TRANSLATIONS);
2510
+ const allResponseSlots = context.responseVariables.filter((slot) => slot.itemId === entry.itemId).map((slot) => slot.slotId);
2511
+ const responseSlots = allResponseSlots.slice(0, MAX_POST_CHANGE_RESPONSE_SLOTS);
2512
+ if (allTranslations.length > translations.length || allResponseSlots.length > responseSlots.length || translations.some((translation) => translation.truncated)) truncated = true;
2513
+ const parentId = outlineById.get(entry.itemId)?.parentId;
2514
+ const parent = parentId ? itemsById.get(parentId) : void 0;
2515
+ const siblings = parent?.itemType === "group" ? getGroupChildren(parent) : void 0;
2516
+ const index = siblings?.indexOf(entry.itemId) ?? -1;
2517
+ const placement = parent && siblings && index >= 0 ? {
2518
+ parentItemId: parent.id,
2519
+ index,
2520
+ siblingCount: siblings.length,
2521
+ ...index > 0 ? { previousItemId: siblings[index - 1] } : {},
2522
+ ...index + 1 < siblings.length ? { nextItemId: siblings[index + 1] } : {},
2523
+ shuffled: parent.itemType === "group" && parent.config.shuffleItems === true
2524
+ } : void 0;
2525
+ const item = itemsById.get(entry.itemId);
2526
+ const children = previewedChildOrders.has(entry.itemId) ? getGroupChildren(item) : void 0;
2527
+ const childOrder = children ? {
2528
+ itemIds: children.slice(0, MAX_POST_CHANGE_CHILDREN),
2529
+ total: children.length,
2530
+ truncated: children.length > MAX_POST_CHANGE_CHILDREN,
2531
+ shuffled: item?.itemType === "group" && item.config.shuffleItems === true
2532
+ } : void 0;
2533
+ if (childOrder?.truncated) truncated = true;
2534
+ return {
2535
+ itemId: entry.itemId,
2536
+ ...placement ? { placement } : {},
2537
+ ...childOrder ? { childOrder } : {},
2538
+ itemType: entry.itemType,
2539
+ editorName: entry.editorName,
2540
+ breadcrumb: entry.breadcrumb,
2541
+ ...entry.itemLabel ? { itemLabel: entry.itemLabel } : {},
2542
+ translationCount: allTranslations.length,
2543
+ translations,
2544
+ responseSlotCount: allResponseSlots.length,
2545
+ responseSlots,
2546
+ responseVariables: context.responseVariables.filter((slot) => responseSlots.includes(slot.slotId))
2547
+ };
2548
+ });
2549
+ return {
2550
+ version: context.version,
2551
+ itemCount: context.inspection.itemCount,
2552
+ localeCount: context.inspection.locales.length,
2553
+ locales: context.inspection.locales.slice(0, 32),
2554
+ targetCount: allTargets.length,
2555
+ touchedTargets: allTargets.slice(0, MAX_POST_CHANGE_TARGETS),
2556
+ touchedItemCount: requestedItemIds.length,
2557
+ availableTouchedItemCount: itemEntries.length,
2558
+ touchedItems,
2559
+ truncated: truncated || context.inspection.locales.length > 32
2560
+ };
2561
+ };
2562
+ const auditFindingKey = (finding, context) => [
2563
+ finding.code,
2564
+ finding.category,
2565
+ finding.itemId ?? "",
2566
+ finding.locale ?? "",
2567
+ JSON.stringify(finding.localizationRefs ?? getDiagnosticLocation(context.model, finding.path))
2568
+ ].join("\0");
2569
+ const isComplexOperation = (operation) => [
2570
+ "survey-json-patch",
2571
+ "patch-item-config",
2572
+ "patch-element-config",
2573
+ "insert-blocks",
2574
+ "move-element",
2575
+ "move-block",
2576
+ "create-item"
2577
+ ].includes(operation.kind);
2578
+ const createChangeVerification = ({ after, before, operations, postChange }) => {
2579
+ const beforeFindingKeys = new Set(auditSurvey(before, { maxFindings: 1e4 }).findings.map((finding) => auditFindingKey(finding, before)));
2580
+ const newAuditFindings = auditSurvey(after, { maxFindings: 1e4 }).findings.filter((finding) => !beforeFindingKeys.has(auditFindingKey(finding, after)));
2581
+ const reasons = [];
2582
+ if (operations.length > SUBSTANTIAL_CHANGE_OPERATION_COUNT || postChange.touchedItemCount > SUBSTANTIAL_CHANGE_ITEM_COUNT) reasons.push("substantial-change");
2583
+ if (operations.some(isComplexOperation)) reasons.push("complex-change");
2584
+ if (postChange.truncated) reasons.push("bounded-preview");
2585
+ if (newAuditFindings.length > 0) reasons.push("new-audit-findings");
2586
+ if (reasons.length === 0) return void 0;
2587
+ let truncated = newAuditFindings.length > MAX_VERIFICATION_AUDIT_FINDINGS;
2588
+ const auditFindings = newAuditFindings.slice(0, MAX_VERIFICATION_AUDIT_FINDINGS).map((finding) => {
2589
+ const messageCharacters = [...finding.message];
2590
+ const messagePreview = messageCharacters.slice(0, MAX_VERIFICATION_MESSAGE_CHARS).join("");
2591
+ const messageTruncated = messageCharacters.length > MAX_VERIFICATION_MESSAGE_CHARS;
2592
+ if (messageTruncated) truncated = true;
2593
+ return {
2594
+ code: finding.code,
2595
+ severity: finding.severity,
2596
+ category: finding.category,
2597
+ ...finding.itemId ? { itemId: finding.itemId } : {},
2598
+ ...finding.locale ? { locale: finding.locale } : {},
2599
+ ...finding.localizationRefs ? { localizationRefs: finding.localizationRefs } : {},
2600
+ messagePreview,
2601
+ messageLength: messageCharacters.length,
2602
+ truncated: messageTruncated
2603
+ };
2604
+ });
2605
+ return {
2606
+ required: true,
2607
+ reasons,
2608
+ newAuditFindingCount: newAuditFindings.length,
2609
+ auditFindings,
2610
+ truncated
2611
+ };
2612
+ };
2613
+ /**
2614
+ * Request-scoped transactional draft for one assistant response.
2615
+ *
2616
+ * Mutation tools submit deltas. A successful delta replaces the previous
2617
+ * candidate with a newly validated, cumulative change set. A rejected delta
2618
+ * leaves the accepted draft intact and blocks authorization until all of its
2619
+ * operation/surface obligations have been repaired. Only the latest cumulative
2620
+ * candidate can be authorized.
2621
+ */
2622
+ var TurnSurveyDraft = class {
2623
+ turnId;
2624
+ #candidates = /* @__PURE__ */ new Map();
2625
+ #draftOperations = [];
2626
+ #context;
2627
+ #baseline;
2628
+ #operationWarnings = [];
2629
+ #reportingTargets = /* @__PURE__ */ new Set();
2630
+ #latestChangeSetId;
2631
+ #unresolvedCandidates = /* @__PURE__ */ new Map();
2632
+ #final = null;
2633
+ constructor(turnId = createId()) {
2634
+ this.turnId = turnId;
2635
+ }
2636
+ initialize(context) {
2637
+ if (this.#context) throw new Error("The survey turn draft was initialized more than once.");
2638
+ this.#context = context;
2639
+ this.#baseline = context;
2640
+ }
2641
+ stage({ addressedTargets = [], allowExpressionJsonPatches = false, handledMutationRoutes = [], retryOfChangeIds = [], operations, snapshot, summary }) {
2642
+ this.#assertOpen();
2643
+ if (!this.#context && snapshot && "document" in snapshot && "trustedCapabilities" in snapshot) {
2644
+ this.#context = snapshot;
2645
+ this.#baseline = snapshot;
2646
+ }
2647
+ const currentContext = this.#context ?? snapshot;
2648
+ const recoveryTargets = addressedTargets.length > 0 ? new Set(addressedTargets) : collectSurveyOperationRecoveryTargets(operations, currentContext);
2649
+ const delta = createSurveyChangeSet({
2650
+ allowExpressionJsonPatches,
2651
+ operations,
2652
+ snapshot: currentContext,
2653
+ summary,
2654
+ turnId: this.turnId
2655
+ });
2656
+ if (operations.length === 0 && retryOfChangeIds.some((id) => this.#unresolvedCandidates.get(id)?.has("satisfied-mutation")) && delta.validation.issues.length > 0 && delta.validation.issues.every((issue) => issue.code === "empty-operations")) {
2657
+ delta.status = "validated";
2658
+ delta.validation = {
2659
+ ...delta.validation,
2660
+ valid: true,
2661
+ issues: []
2662
+ };
2663
+ }
2664
+ if (delta.status !== "validated" || !delta.validation.valid) return this.reject(delta, collectExclusiveMutationRecoveryTargets(delta) ?? (addressedTargets.length > 0 ? recoveryTargets : collectSurveyOperationRecoveryTargets(operations, currentContext, delta.validation.issues)), addressedTargets.length === 0 ? collectSurveyOperationRecoveryRefinements(operations, currentContext, delta.validation.issues) : []);
2665
+ const rejectMissingLinks = (missingRetryLinks) => this.reject({
2666
+ ...delta,
2667
+ status: "rejected",
2668
+ validation: {
2669
+ ...delta.validation,
2670
+ valid: false,
2671
+ issues: [{
2672
+ severity: "error",
2673
+ code: "mutation-retry-link-required",
2674
+ message: `No operations from this call were staged. Retry the complete corrected call with retryOfChangeIds: ${JSON.stringify(missingRetryLinks)} before further mutations.`
2675
+ }]
2676
+ }
2677
+ }, recoveryTargets);
2678
+ const missingRetryLinks = this.#pendingRepairs().filter((repair) => repair.requiresExplicitRetry && !retryOfChangeIds.includes(repair.changeId)).map((repair) => repair.changeId);
2679
+ if (missingRetryLinks.length > 0 && operations.length > 0) return rejectMissingLinks(missingRetryLinks);
2680
+ const cumulativeOperations = [...this.#draftOperations, ...delta.operations];
2681
+ if (cumulativeOperations.length > 1e3) return this.reject(createSurveyChangeSet({
2682
+ maxOperations: MAX_SURVEY_ASSISTANT_OPERATIONS_PER_TURN_DRAFT,
2683
+ operations: cumulativeOperations,
2684
+ snapshot: currentContext,
2685
+ summary,
2686
+ turnId: this.turnId
2687
+ }), recoveryTargets);
2688
+ const beforeContext = this.#context;
2689
+ let nextContext = beforeContext;
2690
+ if (this.#context && delta.operations.length > 0) {
2691
+ const execution = new SurveyOperationExecutionSession(this.#context.survey, this.#context.trustedCapabilities, this.#context.document.answerTypes);
2692
+ for (const operation of delta.operations) execution.apply(operation);
2693
+ execution.assertNoDanglingRemovals();
2694
+ nextContext = createSurveyAssistantEngineContext({
2695
+ ...this.#context.document,
2696
+ survey: execution.serialize(),
2697
+ answerTypes: execution.getAnswerTypes()
2698
+ }, this.#context.trustedCapabilities);
2699
+ }
2700
+ if (this.#baseline && nextContext) {
2701
+ const diagnostics = getAnswerTypeChangeDiagnostics(this.#baseline.model, nextContext.model);
2702
+ if (diagnostics.length) return this.reject({
2703
+ ...delta,
2704
+ status: "rejected",
2705
+ validation: {
2706
+ ...delta.validation,
2707
+ valid: false,
2708
+ issues: diagnostics.map((issue) => ({
2709
+ severity: "error",
2710
+ code: issue.code,
2711
+ message: `${JSON.stringify(issue.path)}: ${issue.message}`
2712
+ }))
2713
+ }
2714
+ }, recoveryTargets);
2715
+ }
2716
+ const removedRefinements = beforeContext && nextContext && delta.operations.length > 0 ? collectRemovedRecoveryTargetRefinements([...this.#unresolvedCandidates.values()].flatMap((pending) => [...pending].filter((target) => !recoveryTargets.has(target))), beforeContext, nextContext) : [];
2717
+ const removedTargets = new Set(removedRefinements.flatMap((refinement) => refinement.targets));
2718
+ const removalLinks = [...this.#unresolvedCandidates].filter(([id, pending]) => !retryOfChangeIds.includes(id) && [...pending].some((target) => removedTargets.has(target))).map(([id]) => id);
2719
+ if (removalLinks.length > 0) return rejectMissingLinks(removalLinks);
2720
+ this.#context = nextContext;
2721
+ this.#refineRecoveryTargets(removedRefinements);
2722
+ const priorCandidate = this.#latestChangeSetId ? this.#candidates.get(this.#latestChangeSetId) : void 0;
2723
+ const acknowledgment = delta.operations.length === 0;
2724
+ const reportingTargets = /* @__PURE__ */ new Set([
2725
+ ...collectOperationTargets(delta.operations, nextContext),
2726
+ ...addressedTargets,
2727
+ ...handledMutationRoutes.map(mutationRouteTarget),
2728
+ ...this.#reportingTargets
2729
+ ]);
2730
+ const operationWarnings = [...this.#operationWarnings, ...delta.validation.warnings.flatMap((warning) => warning.operationIndex === void 0 ? [] : [{
2731
+ ...warning,
2732
+ operationIndex: warning.operationIndex + this.#draftOperations.length
2733
+ }])];
2734
+ const definitionWarnings = this.#baseline && this.#context ? getIntroducedDefinitionIssues(this.#baseline.model, this.#context.model, this.#context.ui.selectedLocale).filter((issue) => issue.severity === "warning") : delta.validation.warnings.filter((warning) => warning.operationIndex === void 0);
2735
+ const postChange = acknowledgment ? priorCandidate?.postChange : this.#context ? createPostChangeProjection(this.#context, reportingTargets, this.#baseline, cumulativeOperations.flatMap((operation) => operation.kind === "reorder-items" ? [operation.parentItemId] : [])) : void 0;
2736
+ const verification = acknowledgment ? priorCandidate?.verification : this.#baseline && this.#context && postChange ? createChangeVerification({
2737
+ after: this.#context,
2738
+ before: this.#baseline,
2739
+ operations: cumulativeOperations,
2740
+ postChange
2741
+ }) : void 0;
2742
+ const registered = surveyChangeSetSchema.parse({
2743
+ ...delta,
2744
+ operations: cumulativeOperations,
2745
+ ...acknowledgment && priorCandidate ? { summary: priorCandidate.summary } : {},
2746
+ ...postChange ? { postChange } : {},
2747
+ ...verification ? { verification } : {},
2748
+ validation: {
2749
+ valid: true,
2750
+ issues: [],
2751
+ warnings: [...operationWarnings, ...definitionWarnings],
2752
+ normalizedOperations: cumulativeOperations
2753
+ },
2754
+ ...this.#latestChangeSetId ? { supersedesChangeIds: [this.#latestChangeSetId] } : {}
2755
+ });
2756
+ this.#operationWarnings = operationWarnings;
2757
+ this.#reportingTargets = reportingTargets;
2758
+ this.#draftOperations = registered.operations;
2759
+ this.#latestChangeSetId = registered.operations.length > 0 ? registered.id : void 0;
2760
+ const resolvedTargets = /* @__PURE__ */ new Set([...recoveryTargets, ...handledMutationRoutes.map(mutationRouteTarget)]);
2761
+ for (const [changeSetId, unresolvedTargets] of this.#unresolvedCandidates) {
2762
+ for (const target of resolvedTargets) if (target !== "unidentified-mutation" && target !== "satisfied-mutation") unresolvedTargets.delete(target);
2763
+ if (retryOfChangeIds.includes(changeSetId)) {
2764
+ unresolvedTargets.delete(SATISFIED_MUTATION_TARGET);
2765
+ if (delta.operations.length > 0) unresolvedTargets.delete(UNIDENTIFIED_MUTATION_TARGET);
2766
+ }
2767
+ if (unresolvedTargets.size === 0) this.#unresolvedCandidates.delete(changeSetId);
2768
+ }
2769
+ registered.pendingRepairs = this.#pendingRepairs();
2770
+ this.#candidates.set(registered.id, registered);
2771
+ return registered;
2772
+ }
2773
+ reject(candidate, addressedTargets = [], refinements = []) {
2774
+ this.#assertOpen();
2775
+ if (this.#candidates.has(candidate.id)) throw new Error(`Survey change "${candidate.id}" was registered more than once.`);
2776
+ const registered = surveyChangeSetSchema.parse({
2777
+ ...candidate,
2778
+ turnId: this.turnId,
2779
+ status: "rejected",
2780
+ validation: {
2781
+ ...candidate.validation,
2782
+ valid: false
2783
+ }
2784
+ });
2785
+ this.#refineRecoveryTargets(refinements);
2786
+ const targets = new Set(addressedTargets);
2787
+ if (targets.size === 0) targets.add(UNIDENTIFIED_MUTATION_TARGET);
2788
+ this.#unresolvedCandidates.set(registered.id, targets);
2789
+ registered.pendingRepairs = this.#pendingRepairs();
2790
+ this.#candidates.set(registered.id, registered);
2791
+ return registered;
2792
+ }
2793
+ #refineRecoveryTargets(refinements) {
2794
+ for (const refinement of refinements) for (const unresolved of this.#unresolvedCandidates.values()) {
2795
+ let matched = false;
2796
+ for (const target of refinement.targets) matched = unresolved.delete(target) || matched;
2797
+ if (matched) unresolved.add(refinement.replacement);
2798
+ }
2799
+ }
2800
+ #pendingRepairs() {
2801
+ return [...this.#unresolvedCandidates].map(([changeId, targets]) => ({
2802
+ changeId,
2803
+ targets: [...targets].filter((target) => target !== "unidentified-mutation" && target !== "satisfied-mutation"),
2804
+ requiresExplicitRetry: targets.has("unidentified-mutation") || targets.has("satisfied-mutation"),
2805
+ ...targets.has("satisfied-mutation") ? { allowsAcknowledgment: true } : {}
2806
+ }));
2807
+ }
2808
+ finalize(status, createdAt = (/* @__PURE__ */ new Date()).toISOString(), details = {}) {
2809
+ if (this.#final) return this.#final;
2810
+ const blockedChangeSetIds = [...this.#unresolvedCandidates.keys()];
2811
+ const canAuthorize = status === "completed" && blockedChangeSetIds.length === 0 && this.#latestChangeSetId !== void 0;
2812
+ this.#final = surveyAssistantTurnFinalSchema.parse({
2813
+ protocolVersion: 1,
2814
+ turnId: this.turnId,
2815
+ status,
2816
+ executableChangeSetIds: canAuthorize ? [this.#latestChangeSetId] : [],
2817
+ ...blockedChangeSetIds.length > 0 ? { blockedChangeSetIds } : {},
2818
+ ...details,
2819
+ createdAt
2820
+ });
2821
+ return this.#final;
2822
+ }
2823
+ getCandidate(changeSetId) {
2824
+ return this.#candidates.get(changeSetId);
2825
+ }
2826
+ get draftOperations() {
2827
+ return this.#draftOperations;
2828
+ }
2829
+ get context() {
2830
+ return this.#context;
2831
+ }
2832
+ get final() {
2833
+ return this.#final;
2834
+ }
2835
+ #assertOpen() {
2836
+ if (this.#final) throw new Error("Cannot register a survey change after the turn was finalized.");
2837
+ }
2838
+ };
2839
+ //#endregion
2840
+ //#region src/engine/task-context.ts
2841
+ const MAX_RELATED_ITEMS = 48;
2842
+ const MAX_SIBLING_ITEMS = 80;
2843
+ const MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM = 24;
2844
+ const MAX_VISIBLE_TEXT_CHARS = 600;
2845
+ const MAX_NAMING_EXAMPLES = 40;
2846
+ const MAX_TRANSLATION_EXAMPLES = 60;
2847
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2848
+ const getItemLabel = (item) => {
2849
+ if (!isRecord(item.metadata)) return void 0;
2850
+ const label = item.metadata.itemLabel;
2851
+ return typeof label === "string" && label.trim().length > 0 ? label.trim() : void 0;
2852
+ };
2853
+ const getCenteredNeighbors = (values, focusIndex, limit) => {
2854
+ if (focusIndex < 0 || limit < 1) return [];
2855
+ const windowSize = Math.min(values.length, limit + 1);
2856
+ const idealStart = focusIndex - Math.floor(limit / 2);
2857
+ const start = Math.min(Math.max(0, idealStart), values.length - windowSize);
2858
+ return values.slice(start, start + windowSize).filter((_, index) => start + index !== focusIndex);
2859
+ };
2860
+ const buildSurveyAssistantTaskContext = (snapshot, options) => {
2861
+ const engineContext = createSurveyAssistantEngineContext(snapshot, options.capabilities, { focusItemId: options.focusItemId });
2862
+ const { inspection } = engineContext;
2863
+ const model = engineContext.model;
2864
+ const itemName = (id) => model.resolveItemEditorName(id, snapshot.ui.selectedLocale);
2865
+ const itemsById = new Map(snapshot.survey.surveyItems.map((item) => [item.id, item]));
2866
+ const parentById = new Map(inspection.outline.flatMap((item) => item.parentId ? [[item.itemId, item.parentId]] : []));
2867
+ const childIdsByParent = /* @__PURE__ */ new Map();
2868
+ for (const item of inspection.outline) {
2869
+ if (!item.parentId) continue;
2870
+ const childIds = childIdsByParent.get(item.parentId) ?? [];
2871
+ childIds.push(item.itemId);
2872
+ childIdsByParent.set(item.parentId, childIds);
2873
+ }
2874
+ const breadcrumbById = new Map(inspection.itemIndex.map((item) => [item.itemId, item.breadcrumb]));
2875
+ const summarizeItem = (item) => {
2876
+ const allVisibleText = [];
2877
+ let visibleTextValueTruncated = false;
2878
+ for (const entry of listLocalizationEntries(model, { itemIds: [item.id] })) {
2879
+ if (entry.ref.scope !== "content" || model.owners.get(entry.ref.ownerId)?.itemId !== item.id) continue;
2880
+ for (const [locale, content] of Object.entries(entry.contents)) {
2881
+ const text = content ? getContentPlainText(content).trim() : "";
2882
+ if (!text) continue;
2883
+ if (text.length > MAX_VISIBLE_TEXT_CHARS) visibleTextValueTruncated = true;
2884
+ allVisibleText.push({
2885
+ locale,
2886
+ ownerId: entry.ref.ownerId,
2887
+ role: entry.ref.role,
2888
+ text: text.length > MAX_VISIBLE_TEXT_CHARS ? `${text.slice(0, MAX_VISIBLE_TEXT_CHARS)}...` : text
2889
+ });
2890
+ }
2891
+ }
2892
+ return {
2893
+ id: item.id,
2894
+ editorName: itemName(item.id),
2895
+ breadcrumb: breadcrumbById.get(item.id) ?? itemName(item.id),
2896
+ itemType: item.itemType,
2897
+ itemLabel: getItemLabel(item),
2898
+ visibleText: allVisibleText.slice(0, MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM),
2899
+ visibleTextTruncated: allVisibleText.length > MAX_VISIBLE_TEXT_ENTRIES_PER_ITEM || visibleTextValueTruncated
2900
+ };
2901
+ };
2902
+ const slots = model.slots.definitions;
2903
+ const requested = options.slotIds ?? (options.focusSlotId ? [options.focusSlotId] : []);
2904
+ const selectedSlots = slots.filter((slot) => requested.includes(slot.slotId));
2905
+ const matrixIds = new Set(selectedSlots.filter((slot) => slot.naming?.kind === "matrix").map((slot) => slot.elementId));
2906
+ const result = {
2907
+ existingVariableNames: slots.map((slot) => slot.variableName),
2908
+ responseVariables: slots.filter((slot) => requested.includes(slot.slotId) || matrixIds.has(slot.elementId)).map((slot) => ({ ...slot })),
2909
+ responseOwners: [...new Set(slots.filter((slot) => requested.includes(slot.slotId) || matrixIds.has(slot.elementId)).map((slot) => slot.itemId))].map((id) => ({
2910
+ item: summarizeItem(itemsById.get(id)),
2911
+ configuration: itemsById.get(id).config,
2912
+ content: Object.fromEntries([...model.owners.values()].filter((owner) => owner.itemId === id).map((owner) => [owner.id, ownValue(ownValue(snapshot.survey.translations, snapshot.ui.selectedLocale)?.content, owner.id) ?? {}]))
2913
+ })),
2914
+ document: {
2915
+ revision: snapshot.version.revision,
2916
+ contentHash: snapshot.version.contentHash,
2917
+ capabilityDigest: snapshot.version.capabilityDigest
2918
+ },
2919
+ survey: {
2920
+ key: snapshot.survey.metadata?.surveyKey,
2921
+ locales: inspection.locales,
2922
+ itemCount: inspection.itemCount,
2923
+ selectedLocale: snapshot.ui.selectedLocale
2924
+ }
2925
+ };
2926
+ const focusItem = options.focusItemId ? itemsById.get(options.focusItemId) : void 0;
2927
+ if (focusItem) {
2928
+ const parentId = parentById.get(focusItem.id);
2929
+ const parentItem = parentId ? itemsById.get(parentId) : void 0;
2930
+ const ancestorItems = [];
2931
+ const seenAncestorIds = /* @__PURE__ */ new Set();
2932
+ let ancestorId = parentId;
2933
+ while (ancestorId && !seenAncestorIds.has(ancestorId)) {
2934
+ seenAncestorIds.add(ancestorId);
2935
+ const ancestor = itemsById.get(ancestorId);
2936
+ if (!ancestor) break;
2937
+ ancestorItems.unshift(ancestor);
2938
+ ancestorId = parentById.get(ancestorId);
2939
+ }
2940
+ const siblingIds = parentItem ? childIdsByParent.get(parentItem.id) ?? [] : [];
2941
+ const siblingItemCount = Math.max(0, siblingIds.length - 1);
2942
+ const siblingItems = getCenteredNeighbors(siblingIds, siblingIds.indexOf(focusItem.id), MAX_SIBLING_ITEMS).map((itemId) => itemsById.get(itemId)).filter((item) => Boolean(item));
2943
+ const allRelatedItems = [];
2944
+ const seenRelatedIds = /* @__PURE__ */ new Set();
2945
+ const pending = [...childIdsByParent.get(focusItem.id) ?? []];
2946
+ while (pending.length > 0) {
2947
+ const itemId = pending.shift();
2948
+ const item = itemId ? itemsById.get(itemId) : void 0;
2949
+ if (!item || seenRelatedIds.has(item.id)) continue;
2950
+ seenRelatedIds.add(item.id);
2951
+ allRelatedItems.push(item);
2952
+ pending.push(...childIdsByParent.get(item.id) ?? []);
2953
+ }
2954
+ const relatedItems = allRelatedItems.slice(0, MAX_RELATED_ITEMS);
2955
+ result.focus = {
2956
+ item: summarizeItem(focusItem),
2957
+ parent: parentItem ? summarizeItem(parentItem) : void 0,
2958
+ ancestorPath: ancestorItems.map(summarizeItem),
2959
+ siblingNames: siblingItems.map((item) => itemName(item.id)),
2960
+ siblingItems: siblingItems.map(summarizeItem),
2961
+ siblingItemCount,
2962
+ siblingItemsTruncated: siblingItemCount > siblingItems.length,
2963
+ relatedItems: relatedItems.map(summarizeItem),
2964
+ relatedItemCount: allRelatedItems.length,
2965
+ relatedItemsTruncated: allRelatedItems.length > relatedItems.length
2966
+ };
2967
+ }
2968
+ if ((options.purpose === "suggest-response-name" || options.purpose === "suggest-response-settings" || options.purpose === "suggest-item-label") && focusItem) {
2969
+ const parentId = parentById.get(focusItem.id);
2970
+ const siblingIds = parentId ? childIdsByParent.get(parentId) ?? [] : [];
2971
+ const siblingPositionById = new Map(siblingIds.map((itemId, index) => [itemId, index]));
2972
+ const focusSiblingPosition = siblingPositionById.get(focusItem.id);
2973
+ const rankedItems = snapshot.survey.surveyItems.filter((item) => item.id !== focusItem.id).map((item, index) => ({
2974
+ item,
2975
+ index,
2976
+ siblingDistance: focusSiblingPosition !== void 0 && siblingPositionById.has(item.id) ? Math.abs(siblingPositionById.get(item.id) - focusSiblingPosition) : Number.POSITIVE_INFINITY,
2977
+ relationship: parentById.get(item.id) === parentId ? "sibling" : item.itemType === focusItem.itemType ? "same-type" : "survey"
2978
+ })).sort((left, right) => {
2979
+ const rank = {
2980
+ sibling: 0,
2981
+ "same-type": 1,
2982
+ survey: 2
2983
+ };
2984
+ return rank[left.relationship] - rank[right.relationship] || left.siblingDistance - right.siblingDistance || left.index - right.index;
2985
+ });
2986
+ const allKeyExamples = model.slots.definitions.filter((slot) => !requested.includes(slot.slotId) && !matrixIds.has(slot.elementId)).map((slot) => ({
2987
+ itemId: slot.itemId,
2988
+ itemType: itemsById.get(slot.itemId).itemType,
2989
+ key: slot.variableName,
2990
+ slotId: slot.slotId,
2991
+ relationship: "survey"
2992
+ }));
2993
+ const allLabelExamples = rankedItems.flatMap(({ item, relationship }) => {
2994
+ const label = getItemLabel(item);
2995
+ return label ? [{
2996
+ itemId: item.id,
2997
+ itemType: item.itemType,
2998
+ key: itemName(item.id),
2999
+ label,
3000
+ relationship
3001
+ }] : [];
3002
+ });
3003
+ result.naming = {
3004
+ keyExamples: allKeyExamples.slice(0, MAX_NAMING_EXAMPLES),
3005
+ keyExampleCount: allKeyExamples.length,
3006
+ keyExamplesTruncated: allKeyExamples.length > MAX_NAMING_EXAMPLES,
3007
+ labelExamples: allLabelExamples.slice(0, MAX_NAMING_EXAMPLES),
3008
+ labelExampleCount: allLabelExamples.length,
3009
+ labelExamplesTruncated: allLabelExamples.length > MAX_NAMING_EXAMPLES
3010
+ };
3011
+ }
3012
+ const sourceLocale = options.sourceLocale;
3013
+ if (options.purpose === "translate-content" && sourceLocale) {
3014
+ const relevantLocales = [sourceLocale, ...options.targetLocales ?? []];
3015
+ const sourceTokens = new Set((options.sourceTexts ?? []).flatMap((text) => text.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u)).filter((token) => token.length >= 3));
3016
+ const allExamples = [];
3017
+ listLocalizationEntries(model, { sourceLocale }).forEach((entry, order) => {
3018
+ if (entry.ref.scope !== "content") return;
3019
+ const owner = model.owners.get(entry.ref.ownerId);
3020
+ const values = {};
3021
+ for (const locale of relevantLocales) {
3022
+ const content = ownValue(entry.contents, locale);
3023
+ const text = content ? getContentPlainText(content).trim() : "";
3024
+ if (text) setOwnValue(values, locale, text.slice(0, MAX_VISIBLE_TEXT_CHARS));
3025
+ }
3026
+ const sourceText = ownValue(values, sourceLocale);
3027
+ if (!sourceText || Object.keys(values).length < 2) return;
3028
+ const normalizedSource = sourceText.toLocaleLowerCase();
3029
+ const score = [...sourceTokens].reduce((total, token) => total + (normalizedSource.includes(token) ? 1 : 0), 0);
3030
+ allExamples.push({
3031
+ itemId: owner.itemId,
3032
+ ownerId: owner.id,
3033
+ role: entry.ref.role,
3034
+ values,
3035
+ score,
3036
+ order
3037
+ });
3038
+ });
3039
+ allExamples.sort((left, right) => right.score - left.score || left.order - right.order);
3040
+ result.translation = {
3041
+ examples: allExamples.slice(0, MAX_TRANSLATION_EXAMPLES).map(({ itemId, ownerId, role, values }) => ({
3042
+ itemId,
3043
+ ownerId,
3044
+ role,
3045
+ values
3046
+ })),
3047
+ exampleCount: allExamples.length,
3048
+ examplesTruncated: allExamples.length > MAX_TRANSLATION_EXAMPLES
3049
+ };
3050
+ }
3051
+ return result;
3052
+ };
3053
+ //#endregion
3054
+ //#region src/engine/task-result-cache.ts
3055
+ const DEFAULT_TTL_MS = 9e5;
3056
+ const DEFAULT_MAX_ENTRIES = 64;
3057
+ /** Small in-memory LRU cache for deterministic, snapshot-scoped one-shot results. */
3058
+ var SurveyAssistantTaskResultCache = class {
3059
+ entries = /* @__PURE__ */ new Map();
3060
+ ttlMs;
3061
+ maxEntries;
3062
+ now;
3063
+ constructor(options = {}) {
3064
+ this.ttlMs = Math.max(0, options.ttlMs ?? DEFAULT_TTL_MS);
3065
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES);
3066
+ this.now = options.now ?? Date.now;
3067
+ }
3068
+ get(key) {
3069
+ const entry = this.entries.get(key);
3070
+ if (!entry) return void 0;
3071
+ if (entry.expiresAt <= this.now()) {
3072
+ this.entries.delete(key);
3073
+ return;
3074
+ }
3075
+ this.entries.delete(key);
3076
+ this.entries.set(key, entry);
3077
+ return entry.value;
3078
+ }
3079
+ set(key, value) {
3080
+ this.entries.delete(key);
3081
+ this.entries.set(key, {
3082
+ expiresAt: this.now() + this.ttlMs,
3083
+ value
3084
+ });
3085
+ while (this.entries.size > this.maxEntries) {
3086
+ const oldestKey = this.entries.keys().next().value;
3087
+ if (oldestKey === void 0) break;
3088
+ this.entries.delete(oldestKey);
3089
+ }
3090
+ }
3091
+ clear() {
3092
+ this.entries.clear();
3093
+ }
3094
+ };
3095
+ //#endregion
3096
+ export { validateExpression as $, itemSummarySchema as A, summarizeItemIndexEntries as B, elementDetailsSchema as C, getRawSurveyItems as D, getRawSurvey as E, parseCursor as F, unique as G, summarizeOutlineNode as H, resolveItemSummary as I, expressionSchema$1 as J, buildExpressionMutationBatch as K, sliceWithCursor as L, mapOutlineById as M, mapRawItemsById as N, itemConfigDetailsSchema as O, normalizeText as P, inferExpressionReturnType$1 as Q, summarizeFocusItemNode as R, contentOwnerDetailsSchema as S, getItemIndexEntries as T, summarizeRawItem as U, summarizeItemIndexEntry as V, translationEntrySchema as W, getSurveyAssistantExpressionDiagnosticGuidance as X, analyzeSurveyAssistantExpressionReferences as Y, getIntroducedDefinitionIssues as Z, MAX_LIMIT as _, collectExpressionMutationRecoveryTargets as a, buildItemTranslationEntries as b, auditSurvey as c, buildSurveyDocumentVersion as d, createSurveyAssistantEngineContext as et, createSurveyChangeSet as f, DEFAULT_LIMIT as g, normalizeSurveyChangeOperations as h, collectExpressionMutationRecoveryRefinements as i, mapItemIndexById as j, itemMatchesQuery as k, MAX_SURVEY_ASSISTANT_OPERATIONS_PER_PROPOSAL as l, executeSurveyOperationsOnRawSurvey as m, buildSurveyAssistantTaskContext as n, createAssistantSurvey as nt, collectMalformedSurveyOperationRecoveryTargets as o, validateSurveyOperations as p, expressionMutationSchema as q, TurnSurveyDraft as r, getAssistantVisibilityIssues as rt, collectSurveyOperationRecoveryTargets as s, SurveyAssistantTaskResultCache as t, deriveResponseSlotTypes as tt, MAX_SURVEY_ASSISTANT_OPERATIONS_PER_TURN_DRAFT as u, buildConfigSearchText as v, findDirectChildrenByParentId as w, clampLimit as x, buildItemConfigDetails as y, summarizeItemById as z };
3097
+
3098
+ //# sourceMappingURL=engine-D950TfvC.mjs.map