@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.
- package/dist/{authoring-references-DbuOKerg.mjs → authoring-references-CmwLc_C8.mjs} +40 -32
- package/dist/authoring-references-CmwLc_C8.mjs.map +1 -0
- package/dist/capabilities-D55dq-fO.mjs +582 -0
- package/dist/capabilities-D55dq-fO.mjs.map +1 -0
- package/dist/capabilities-default.d.mts +4 -12
- package/dist/capabilities-default.d.mts.map +1 -1
- package/dist/capabilities-default.mjs +2 -2
- package/dist/capabilities-z95e1mti.d.mts +117 -0
- package/dist/capabilities-z95e1mti.d.mts.map +1 -0
- package/dist/{constants-B6HzpEsx.d.mts → constants-HL_klgJl.d.mts} +4 -4
- package/dist/{constants-B6HzpEsx.d.mts.map → constants-HL_klgJl.d.mts.map} +1 -1
- package/dist/{controller-proxy-DEeFl9IA.mjs → controller-proxy-BQ1YsVxJ.mjs} +4 -3
- package/dist/controller-proxy-BQ1YsVxJ.mjs.map +1 -0
- package/dist/{controller-proxy-D7uQfm5f.d.mts → controller-proxy-Cmzdr9tV.d.mts} +4 -4
- package/dist/{controller-proxy-D7uQfm5f.d.mts.map → controller-proxy-Cmzdr9tV.d.mts.map} +1 -1
- package/dist/default-D22AKTqE.mjs +233 -0
- package/dist/default-D22AKTqE.mjs.map +1 -0
- package/dist/{digest-C39WyAWG.d.mts → digest-FnTTDBq2.d.mts} +2 -2
- package/dist/digest-FnTTDBq2.d.mts.map +1 -0
- package/dist/digest.d.mts +1 -1
- package/dist/engine-D950TfvC.mjs +3098 -0
- package/dist/engine-D950TfvC.mjs.map +1 -0
- package/dist/engine.d.mts +4 -3
- package/dist/engine.mjs +3 -3
- package/dist/{index-WB5kfnz0.d.mts → index-B8TLBd1G.d.mts} +8 -6
- package/dist/index-B8TLBd1G.d.mts.map +1 -0
- package/dist/index-CaX3hZmr.d.mts +23813 -0
- package/dist/index-CaX3hZmr.d.mts.map +1 -0
- package/dist/{index-r5riv4md.d.mts → index-CeUlGkoU.d.mts} +27 -6
- package/dist/index-CeUlGkoU.d.mts.map +1 -0
- package/dist/{index-D7wT6P3t.d.mts → index-DYHdvSE8.d.mts} +83 -41
- package/dist/index-DYHdvSE8.d.mts.map +1 -0
- package/dist/lifecycle-DFzxVxpq.d.mts +54346 -0
- package/dist/lifecycle-DFzxVxpq.d.mts.map +1 -0
- package/dist/{memory-thread-repository-DAnOGIB3.mjs → memory-thread-repository-DmLadygY.mjs} +15 -9
- package/dist/{memory-thread-repository-DAnOGIB3.mjs.map → memory-thread-repository-DmLadygY.mjs.map} +1 -1
- package/dist/protocol-BQ7uIDYT.mjs +569 -0
- package/dist/protocol-BQ7uIDYT.mjs.map +1 -0
- package/dist/protocol.d.mts +4 -4
- package/dist/protocol.mjs +3 -2
- package/dist/{react-C_GY-Fsc.mjs → react-DybJ5pWw.mjs} +17 -11
- package/dist/react-DybJ5pWw.mjs.map +1 -0
- package/dist/react-integration.d.mts +1 -1
- package/dist/react-integration.mjs +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.mjs +2 -2
- package/dist/references/assistant-operations.md +190 -215
- package/dist/references/core-rules.md +2 -2
- package/dist/references/element-types.md +189 -0
- package/dist/references/expressions.md +215 -238
- package/dist/references/follow-ups.md +151 -0
- package/dist/references/localization.md +26 -12
- package/dist/references/response-variables.md +3 -3
- package/dist/references/rich-text-content.md +22 -43
- package/dist/references/source-material-surveys.md +20 -25
- package/dist/references/survey-data-model.md +36 -117
- package/dist/{request-context-Dg4jSwN1.mjs → request-context-KVh8VjgH.mjs} +4 -4
- package/dist/request-context-KVh8VjgH.mjs.map +1 -0
- package/dist/server-agent.d.mts +28 -7
- package/dist/server-agent.d.mts.map +1 -1
- package/dist/server-agent.mjs +33 -25
- package/dist/server-agent.mjs.map +1 -1
- package/dist/server-runtime.d.mts +6 -5
- package/dist/server-runtime.d.mts.map +1 -1
- package/dist/server-runtime.mjs +3 -3
- package/dist/server-tasks.d.mts +1 -1
- package/dist/server-tasks.mjs +1 -1
- package/dist/server-tools.d.mts +1 -1
- package/dist/server-tools.mjs +1 -1
- package/dist/server.d.mts +7 -6
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/dist/{tasks-BZEiwgxA.mjs → tasks-Coh5N027.mjs} +16 -15
- package/dist/tasks-Coh5N027.mjs.map +1 -0
- package/dist/{tasks-B9h3LPam.d.mts → tasks-wb9M-bak.d.mts} +11 -11
- package/dist/{tasks-B9h3LPam.d.mts.map → tasks-wb9M-bak.d.mts.map} +1 -1
- package/dist/{thread-handlers-Bef0td9N.d.mts → thread-handlers-C3Iya92v.d.mts} +6 -5
- package/dist/thread-handlers-C3Iya92v.d.mts.map +1 -0
- package/dist/{tools-CEsv-WoO.mjs → tools-D0KBDolR.mjs} +435 -408
- package/dist/tools-D0KBDolR.mjs.map +1 -0
- package/dist/turn-survey-draft-B34lggSG.d.mts +185 -0
- package/dist/turn-survey-draft-B34lggSG.d.mts.map +1 -0
- package/dist/ui.d.mts +3 -3
- package/dist/ui.d.mts.map +1 -1
- package/dist/ui.mjs +35 -23
- package/dist/ui.mjs.map +1 -1
- package/package.json +7 -7
- package/dist/authoring-references-DbuOKerg.mjs.map +0 -1
- package/dist/capabilities-CmeeAjEc.d.mts +0 -192
- package/dist/capabilities-CmeeAjEc.d.mts.map +0 -1
- package/dist/capabilities-DCMA71PP.mjs +0 -58
- package/dist/capabilities-DCMA71PP.mjs.map +0 -1
- package/dist/controller-proxy-DEeFl9IA.mjs.map +0 -1
- package/dist/default-fhSon2RM.mjs +0 -760
- package/dist/default-fhSon2RM.mjs.map +0 -1
- package/dist/digest-C39WyAWG.d.mts.map +0 -1
- package/dist/engine-nXoCWYpQ.mjs +0 -6888
- package/dist/engine-nXoCWYpQ.mjs.map +0 -1
- package/dist/index-Ciq7vHIs.d.mts +0 -716
- package/dist/index-Ciq7vHIs.d.mts.map +0 -1
- package/dist/index-D7wT6P3t.d.mts.map +0 -1
- package/dist/index-WB5kfnz0.d.mts.map +0 -1
- package/dist/index-r5riv4md.d.mts.map +0 -1
- package/dist/lifecycle-Dm8u7QFh.d.mts +0 -545
- package/dist/lifecycle-Dm8u7QFh.d.mts.map +0 -1
- package/dist/protocol-CF4Bh_Ch.mjs +0 -1255
- package/dist/protocol-CF4Bh_Ch.mjs.map +0 -1
- package/dist/react-C_GY-Fsc.mjs.map +0 -1
- package/dist/references/embedded-forms.md +0 -146
- package/dist/references/item-types.md +0 -641
- package/dist/request-context-Dg4jSwN1.mjs.map +0 -1
- package/dist/tasks-BZEiwgxA.mjs.map +0 -1
- package/dist/thread-handlers-Bef0td9N.d.mts.map +0 -1
- 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
|