@case-framework/survey-core 0.7.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/build/index.mjs CHANGED
@@ -1,891 +1,274 @@
1
- import { $ as generateId, A as DurationUnits, B as ConstExpression, C as ReservedSurveyItemTypes, D as deserializeSurveyItemPrefill, E as SurveyItemPrefillTargetType, F as TemplateDefTypes, G as FunctionExpression, H as ContextVariableType, I as deserializeTemplateValue, J as ReferenceUsageType, K as FunctionExpressionNames, L as deserializeTemplateValues, M as ValueType, N as assertResponseValue, O as prefillTargetsEqual, P as isResponseValue, Q as generateCodingKey, R as serializeTemplateValue, S as SurveyItemKey, T as SurveyItemPrefillApplyMode, U as Expression, V as ContextVariableExpression, W as ExpressionType, X as ValueReferenceMethod, Y as ValueReference, Z as createSeededRandom, _ as toItemTypeDefinitionRegistry, a as getContentPlainText, b as builtInItemCoreRegistry, c as hasRenderableRichTextContent, d as SurveyTranslations, et as shuffleArray, f as validateLocale, g as createItemTypeDefinitionRegistry, h as createItemCore, i as getAssetUsagesFromContent, j as NumberPrecision, k as serializeSurveyItemPrefill, l as isContentEmpty, m as createFullRegistry, n as ContentType, nt as structuredCloneMethod, o as getPlainTextFromRichTextContent, p as CURRENT_SURVEY_SCHEMA, q as ResponseVariableExpression, r as createRichTextContent, s as hasRenderableRichTextBlock, t as Survey, tt as shuffleIndices, u as SurveyItemTranslations, v as GroupItemCore, w as SurveyItemCore, x as isBuiltInItemType, y as PageBreakItemCore, z as serializeTemplateValues } from "./survey-BKblV5rN.mjs";
2
- import { SURVEY_EDITOR_ITEM_COLORS } from "./editor/colors.mjs";
3
- import { format } from "date-fns";
4
- import { enUS } from "date-fns/locale";
5
- //#region src/to_mirgrate/data_types/legacy-types.ts
6
- const isLegacyItemGroupComponent = (item) => {
7
- const items = item.items;
8
- return items !== void 0 && items.length > 0;
9
- };
10
- const isLegacySurveyGroupItem = (item) => {
11
- const items = item.items;
12
- return items !== void 0 && items.length > 0;
13
- };
14
- //#endregion
15
- //#region src/survey/responses/item-response.ts
16
- /**
17
- * SurveyItemResponse to store the response of a survey item.
18
- */
19
- var SurveyItemResponse = class SurveyItemResponse {
20
- itemId;
21
- meta;
22
- response;
23
- itemType;
24
- constructor(itemDef, response) {
25
- this.itemId = itemDef.itemId;
26
- this.response = response;
27
- this.itemType = itemDef.itemType;
28
- }
29
- serialize() {
30
- return {
31
- itemId: this.itemId,
32
- itemType: this.itemType,
33
- meta: this.meta,
34
- response: this.response?.serialize()
35
- };
36
- }
37
- static deserialize(json) {
38
- if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey item response payload");
39
- if (typeof json.itemId !== "string" || json.itemId.trim().length === 0) throw new Error("Survey item response is missing a valid itemId");
40
- if (typeof json.itemType !== "string" || json.itemType.trim().length === 0) throw new Error("Survey item response is missing a valid itemType");
41
- const newResponse = new SurveyItemResponse({
42
- itemId: json.itemId,
43
- itemType: json.itemType
44
- }, json.response ? ResponseItem.deserialize(json.response) : void 0);
45
- newResponse.meta = json.meta;
46
- return newResponse;
47
- }
48
- };
49
- var ResponseItem = class ResponseItem {
50
- _slots;
51
- constructor(initialValues) {
52
- this._slots = new Map(initialValues ?? []);
53
- }
54
- get(slotId) {
55
- return this._slots.get(slotId);
56
- }
57
- setSlotValue(slotId, slotResponse) {
58
- if (slotResponse) this._slots.set(slotId, slotResponse);
59
- else this._slots.delete(slotId);
60
- }
61
- serialize() {
62
- if (this._slots.size === 0) return;
63
- return { slots: Object.fromEntries(this._slots.entries()) };
64
- }
65
- isEmpty() {
66
- return this._slots.size === 0;
67
- }
68
- clone() {
69
- return new ResponseItem(Array.from(this._slots.entries()));
70
- }
71
- static deserialize(json) {
72
- if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid response item payload");
73
- const { slots } = json;
74
- if (!slots || typeof slots !== "object" || Array.isArray(slots)) throw new Error("Invalid response slots payload");
75
- return new ResponseItem(Object.entries(slots).map(([slotId, slotResponse]) => {
76
- if (slotId.trim().length === 0) throw new Error("Response slot id cannot be empty");
77
- assertResponseValue(slotResponse, `response.slots.${slotId}`);
78
- return [slotId, slotResponse];
79
- }));
1
+ import { $ as inspectAnnotated, A as slotPrefillSchema, B as ValueType, C as createCompositionSchema, Ct as calendarDaySchema, D as followUpBody, Dt as isCalendarDate, E as elementShape, Et as formatCalendarDate, F as richTextImageDimensionSchema, G as stringRecord, H as isResponseValue, I as richTextImageSourceSchema, J as boundField, K as annotated, L as surveyContentSchema, M as templateValueSchema, N as validationRuleSchema, O as itemEditorMetadataSchema, Ot as isCalendarDateUnit, P as jsonValueSchema, Q as extendObject, R as DurationUnits, S as conditionsShape, St as calendarDateToLocalDate, T as createSurveySchema, Tt as differenceCalendarDates, U as ownValue, V as assertResponseValue, W as setOwnValue, X as contentOwnerIds, Y as contentOwner, Z as expressionField, _ as hasRenderableRichTextBlock, _t as CALENDAR_DATE_UNITS, a as getLocalizationItems, at as expressionEditorConfigSchema, b as CURRENT_SURVEY_SCHEMA, bt as calendarDateFromLocalDate, c as listLocalizationEntries, ct as responseValueSchema, d as validateLocalizationRef, dt as remapExpression, et as ownedId, f as ContentType, ft as expressionArguments, g as getPlainTextFromRichTextContent, gt as visitExpression, h as getContentPlainText, ht as unreachable, i as getLocalizationEntryKey, it as transformAnnotated, j as slotTransformSchema, k as layoutPolicySchema, l as readLocalizationContent, lt as valueTypeSchema, m as getAssetUsagesFromContent, mt as transformExpression, n as getLocalizationCellKey, nt as remapAnnotated, o as hasLocalizationText, ot as expressionSchema, p as createRichTextContent, pt as operand, q as annotatedReferences, r as getLocalizationCoverage, rt as schemaAnnotation, s as isEmptySurveyLocalizationEntry, st as functionNameSchema, t as SURVEY_LOCALIZATION_SURFACES, tt as referenceId, u as resolveParticipantContent, ut as collectExpressionDependencies, v as hasRenderableRichTextContent, vt as addCalendarDate, w as createFlowItemSchema, wt as compareCalendarDates, x as CURRENT_SURVEY_SCHEMA_VERSION, xt as calendarDatePrecision, y as isContentEmpty, yt as calendarDateBounds, z as NumberPrecision } from "./localization-1lzNpyFX.mjs";
2
+ import { $ as shuffleArray, A as isAdmissibleInDraft, B as expressionIssues, C as surveyEventSchema, D as DefinitionError, E as hasPermittedChange, F as allocateResponseVariableName, G as responseValueElements, H as functionTypeIssues, I as isValidResponseVariableName, J as FUNCTION_SIGNATURES, K as responseValuesEqual, L as suggestResponseVariableName, M as getCompositionLabel, N as ResponseSlotRegistry, O as Survey, P as RESPONSE_VARIABLE_NAME_MAX_LENGTH, Q as resolveUniqueItemKey, R as ExpressionEvaluationError, S as itemResponseMetadataSchema, T as canWriteSelection, U as inferExpressionReturnType, V as expressionTypeIssues, W as invalidResponseDomainValues, X as generateCodingKey, Y as createSeededRandom, Z as generateId, _ as SlotTransformMode, a as optionIds, b as surveyResponseSchema, c as resolvedSlotEncoding, d as exportSingleResponseSlot, et as shuffleIndices, f as decodeExportColumnId, g as META_COLUMN_ORDER, h as ExportColumnSource, i as localizedExportSlots, j as surveyLocales, k as checkDocumentBudget, l as BooleanExportValues, m as EXPORTER_PROFILE_SCHEMA_VERSION, n as getDefaultProjectionDiagnostics, o as optionMapping, p as encodeExportColumnId, q as validateResponseSlotValue, r as getDefaultProjectionIssues, s as projectSlot, t as formatProjectedValue, tt as structuredCloneMethod, u as DEFAULT_TRANSFORM, v as SURVEY_RESPONSE_SCHEMA_VERSION, w as SurveyStateGraph, x as SurveyEventTypes, y as SurveyResponse, z as evaluateExpression } from "./projection-DrLPFroE.mjs";
3
+ import { t as strictKeyedObject } from "./keyed-object-D1yh_W5v.mjs";
4
+ import { format } from "date-fns/format";
5
+ import { z } from "zod";
6
+ //#region src/json-patch.ts
7
+ var JsonPatchError = class extends Error {
8
+ code;
9
+ path;
10
+ operationIndex;
11
+ constructor(message, code, path, operationIndex) {
12
+ super(message);
13
+ this.code = code;
14
+ this.path = path;
15
+ this.operationIndex = operationIndex;
16
+ this.name = "JsonPatchError";
80
17
  }
81
18
  };
82
- //#endregion
83
- //#region src/survey/responses/response-meta.ts
84
- const SurveyEventTypes = {
85
- openSurvey: "openSurvey",
86
- pageChanged: "pageChanged",
87
- responeChanged: "responeChanged",
88
- languageChanged: "languageChanged"
19
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
20
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21
+ const assertValidPointerSegment = (segment, pointer) => {
22
+ if (/~(?![01])/.test(segment)) throw new JsonPatchError(`Invalid JSON pointer segment "${segment}" in "${pointer}". Only "~0" and "~1" escapes are allowed.`, "invalid-json-pointer", pointer);
89
23
  };
90
- //#endregion
91
- //#region src/survey/responses/survey-response.ts
92
- const SURVEY_RESPONSE_SCHEMA_VERSION = 1;
93
- /**
94
- * SurveyResponse to store the responses of a survey.
95
- */
96
- var SurveyResponse = class SurveyResponse {
97
- key;
98
- participantId;
99
- submittedAt;
100
- versionId;
101
- responses;
102
- events;
103
- context;
104
- constructor(key, versionId) {
105
- this.key = key;
106
- this.participantId = "";
107
- this.submittedAt = 0;
108
- this.versionId = versionId;
109
- this.responses = /* @__PURE__ */ new Map();
110
- this.events = [];
111
- }
112
- serialize() {
113
- const items = Object.fromEntries(Array.from(this.responses.entries()).flatMap(([itemId, response]) => {
114
- const serialized = response.serialize();
115
- if (!serialized.response && !serialized.meta) return [];
116
- return [[itemId, serialized]];
117
- }));
118
- return {
119
- schemaVersion: 1,
120
- key: this.key,
121
- participantId: this.participantId,
122
- submittedAt: this.submittedAt,
123
- versionId: this.versionId,
124
- items,
125
- events: this.events,
126
- context: this.context
127
- };
128
- }
129
- static deserialize(json) {
130
- if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey response payload");
131
- if (json.schemaVersion !== 1) throw new Error(`Unsupported survey response schema version: ${String(json.schemaVersion)}`);
132
- if (typeof json.key !== "string" || json.key.trim().length === 0) throw new Error("Survey response is missing a valid key");
133
- if (typeof json.versionId !== "string" || json.versionId.trim().length === 0) throw new Error("Survey response is missing a valid versionId");
134
- if (!json.items || typeof json.items !== "object" || Array.isArray(json.items)) throw new Error("Survey response is missing a valid items object");
135
- const response = new SurveyResponse(json.key, json.versionId);
136
- response.participantId = json.participantId;
137
- response.submittedAt = json.submittedAt;
138
- response.responses = new Map(Object.entries(json.items).map(([itemId, itemResponse]) => {
139
- const deserialized = SurveyItemResponse.deserialize(itemResponse);
140
- if (deserialized.itemId !== itemId) throw new Error(`Survey response item key mismatch for '${itemId}'`);
141
- return [itemId, deserialized];
142
- }));
143
- response.events = json.events ?? [];
144
- response.versionId = json.versionId;
145
- response.key = json.key;
146
- if (json.context) {
147
- if (typeof json.context !== "object" || Array.isArray(json.context)) throw new Error("Survey response context must be an object");
148
- response.context = Object.fromEntries(Object.entries(json.context).map(([contextKey, value]) => {
149
- assertResponseValue(value, `context.${contextKey}`);
150
- return [contextKey, value];
151
- }));
152
- }
153
- return response;
154
- }
24
+ const decodeJsonPointerSegment = (segment) => {
25
+ assertValidPointerSegment(segment, segment);
26
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
155
27
  };
156
- //#endregion
157
- //#region src/survey/responses/utils.ts
158
- const initValueForType = (returnType) => {
159
- switch (returnType) {
160
- case ValueType.string: return {
161
- type: ValueType.string,
162
- value: ""
163
- };
164
- case ValueType.number: return {
165
- type: ValueType.number,
166
- value: 0
167
- };
168
- case ValueType.boolean: return {
169
- type: ValueType.boolean,
170
- value: false
171
- };
172
- case ValueType.date: return {
173
- type: ValueType.date,
174
- value: Math.floor(Date.now() / 1e3)
175
- };
176
- case ValueType.stringArray: return {
177
- type: ValueType.stringArray,
178
- value: []
179
- };
180
- case ValueType.numberArray: return {
181
- type: ValueType.numberArray,
182
- value: []
183
- };
184
- case ValueType.dateArray: return {
185
- type: ValueType.dateArray,
186
- value: []
187
- };
188
- case ValueType.duration: return {
189
- type: ValueType.duration,
190
- value: 0,
191
- unit: DurationUnits.seconds
192
- };
193
- case ValueType.reference: return {
194
- type: ValueType.reference,
195
- value: ""
196
- };
197
- case ValueType.durationArray: return {
198
- type: ValueType.durationArray,
199
- value: [],
200
- unit: DurationUnits.seconds
201
- };
202
- case ValueType.referenceArray: return {
203
- type: ValueType.referenceArray,
204
- value: []
205
- };
206
- default: throw new Error("Invalid return type: " + returnType);
207
- }
28
+ const encodeJsonPointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
29
+ const parseJsonPointer = (pointer) => {
30
+ if (pointer === "") return [];
31
+ if (!pointer.startsWith("/")) throw new JsonPatchError(`Invalid JSON pointer "${pointer}". It must be empty or start with "/".`, "invalid-json-pointer", pointer);
32
+ return pointer.slice(1).split("/").map((segment) => {
33
+ assertValidPointerSegment(segment, pointer);
34
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
35
+ });
208
36
  };
209
- //#endregion
210
- //#region src/expressions/expression-evaluator.ts
211
- var ExpressionEvaluator = class {
212
- context;
213
- constructor(context) {
214
- this.context = context;
215
- }
216
- eval(expression) {
217
- if (expression === void 0) return;
218
- switch (expression.type) {
219
- case ExpressionType.Const: return this.evaluateConst(expression);
220
- case ExpressionType.Function: return this.evaluateFunction(expression);
221
- case ExpressionType.ResponseVariable: return this.evaluateResponseVariable(expression);
222
- case ExpressionType.ContextVariable: return this.evaluateContextVariable(expression);
223
- default: throw new Error(`Unsupported expression type: ${expression.type}`);
224
- }
225
- }
226
- setContext(context) {
227
- this.context = context;
228
- }
229
- evaluateConst(expression) {
230
- return expression.value;
231
- }
232
- evaluateResponseVariable(expression) {
233
- const varRef = expression.responseVariableRef;
234
- switch (varRef.name) {
235
- case "get": return this.context?.responses[varRef.itemId]?.response?.get(varRef.slotId);
236
- case "isDefined": return {
237
- type: ValueType.boolean,
238
- value: this.context?.responses[varRef.itemId]?.response?.get(varRef.slotId) !== void 0
239
- };
240
- default: throw new Error(`Unsupported value reference method: ${varRef.name}`);
241
- }
242
- }
243
- evaluateFunction(expression) {
244
- switch (expression.functionName) {
245
- case "and": return this.evaluateAnd(expression);
246
- case "or": return this.evaluateOr(expression);
247
- case "not": return this.evaluateNot(expression);
248
- case "str_eq": return this.evaluateStrEq(expression);
249
- case "eq": return this.evaluateEq(expression);
250
- case "gt": return this.evaluateGt(expression);
251
- case "gte": return this.evaluateGte(expression);
252
- case "lt": return this.evaluateLt(expression);
253
- case "lte": return this.evaluateLte(expression);
254
- case "in_range": return this.evaluateInRange(expression);
255
- case "sum": return this.evaluateSum(expression);
256
- case "min": return this.evaluateMin(expression);
257
- case "max": return this.evaluateMax(expression);
258
- case "list_contains": return this.evaluateListContains(expression);
259
- default: throw new Error(`Unsupported function: ${expression.functionName}`);
260
- }
37
+ const parseArrayIndex = ({ segment, arrayLength, allowAppend, allowEnd, path }) => {
38
+ if (segment === "-") {
39
+ if (allowAppend) return arrayLength;
40
+ throw new JsonPatchError(`JSON pointer "${path}" uses "-" where an existing array index is required.`, "invalid-array-index", path);
261
41
  }
262
- evaluateContextVariable(expression) {
263
- if (!this.context?.surveyContext) return;
264
- const surveyContext = this.context.surveyContext;
265
- switch (expression.contextType) {
266
- case ContextVariableType.Locale: return {
267
- type: ValueType.string,
268
- value: surveyContext.locale
269
- };
270
- case ContextVariableType.ParticipantFlag: {
271
- if (!expression.key) return;
272
- const flagKey = this.eval(expression.key);
273
- if (flagKey?.type !== ValueType.string || flagKey.value === void 0) return;
274
- const flagValue = surveyContext.participantFlags?.[flagKey.value];
275
- switch (expression.asType) {
276
- case ValueType.boolean: return {
277
- type: ValueType.boolean,
278
- value: flagValue !== void 0
279
- };
280
- case ValueType.string:
281
- if (flagValue === void 0) return;
282
- return {
283
- type: ValueType.string,
284
- value: flagValue
285
- };
286
- case ValueType.number: {
287
- if (flagValue === void 0) return;
288
- const numValue = parseFloat(flagValue);
289
- return isNaN(numValue) ? void 0 : {
290
- type: ValueType.number,
291
- value: numValue
292
- };
293
- }
294
- case ValueType.date: {
295
- if (flagValue === void 0) return;
296
- const timestamp = parseFloat(flagValue);
297
- return isNaN(timestamp) ? void 0 : {
298
- type: ValueType.date,
299
- value: timestamp
300
- };
301
- }
302
- default:
303
- if (flagValue === void 0) return;
304
- return {
305
- type: ValueType.string,
306
- value: flagValue
307
- };
308
- }
309
- }
310
- case ContextVariableType.CustomValue: {
311
- if (!expression.key) return;
312
- const customKey = this.eval(expression.key);
313
- if (customKey?.type !== ValueType.string || customKey.value === void 0) return;
314
- const customValue = surveyContext.customValues?.[customKey.value];
315
- if (customValue === void 0) return;
316
- if (expression.asType !== void 0) {
317
- if (customValue?.type !== expression.asType) throw new Error(`Custom value type does not match expected type, got ${customValue?.type} and expected ${expression.asType}`);
318
- }
319
- return customValue;
320
- }
321
- case ContextVariableType.CustomExpression: {
322
- if (!expression.key) return;
323
- const expressionKey = this.eval(expression.key);
324
- if (expressionKey?.type !== ValueType.string || expressionKey.value === void 0) return;
325
- const customExpression = surveyContext.customExpressions?.[expressionKey.value];
326
- if (typeof customExpression !== ExpressionType.Function || customExpression === void 0) return;
327
- try {
328
- const args = expression.arguments;
329
- const result = customExpression(args);
330
- if (expression.asType !== void 0) {
331
- if (result?.type !== expression.asType) throw new Error(`Custom expression result type does not match expected type, got ${result?.type} and expected ${expression.asType}`);
332
- }
333
- return result;
334
- } catch (error) {
335
- throw new Error(`Error evaluating custom expression: ${error}`);
336
- }
337
- }
42
+ if (!/^(0|[1-9][0-9]*)$/.test(segment)) throw new JsonPatchError(`JSON pointer "${path}" uses invalid array index "${segment}".`, "invalid-array-index", path);
43
+ const index = Number(segment);
44
+ const maximum = allowEnd ? arrayLength : arrayLength - 1;
45
+ if (index < 0 || index > maximum) throw new JsonPatchError(`JSON pointer "${path}" array index ${index} is out of bounds.`, "array-index-out-of-bounds", path);
46
+ return index;
47
+ };
48
+ const getValueAtSegments = (document, segments, path) => {
49
+ let current = document;
50
+ for (const segment of segments) {
51
+ if (Array.isArray(current)) {
52
+ const index = parseArrayIndex({
53
+ segment,
54
+ arrayLength: current.length,
55
+ allowAppend: false,
56
+ allowEnd: false,
57
+ path
58
+ });
59
+ current = current[index];
60
+ continue;
338
61
  }
62
+ if (!isRecord(current) || !hasOwn(current, segment)) throw new JsonPatchError(`JSON pointer "${path}" does not exist.`, "path-not-found", path);
63
+ current = current[segment];
339
64
  }
340
- evaluateAnd(expression) {
341
- return {
342
- type: ValueType.boolean,
343
- value: expression.arguments.every((arg) => this.eval(arg)?.value === true)
344
- };
345
- }
346
- evaluateOr(expression) {
347
- return {
348
- type: ValueType.boolean,
349
- value: expression.arguments.some((arg) => this.eval(arg)?.value === true)
350
- };
351
- }
352
- evaluateNot(expression) {
353
- if (expression.arguments.length !== 1 || expression.arguments[0] === void 0) throw new Error(`Not function expects 1 argument, got ${expression.arguments.length}`);
354
- const resolvedValue = this.eval(expression.arguments[0]);
355
- if (resolvedValue?.type !== ValueType.boolean) throw new Error(`Not function expects a boolean argument, got ${resolvedValue?.type}`);
356
- if (resolvedValue === void 0) return {
357
- type: ValueType.boolean,
358
- value: false
359
- };
360
- return {
361
- type: ValueType.boolean,
362
- value: !resolvedValue.value
363
- };
364
- }
365
- evaluateListContains(expression) {
366
- if (expression.arguments.length !== 2) throw new Error(`List contains function expects 2 arguments, got ${expression.arguments.length}`);
367
- const resolvedList = this.eval(expression.arguments[0]);
368
- const resolvedItem = this.eval(expression.arguments[1]);
369
- if (resolvedList === void 0 || resolvedItem === void 0) return {
370
- type: ValueType.boolean,
371
- value: false
372
- };
373
- const allowedListTypes = [ValueType.stringArray, ValueType.referenceArray];
374
- const allowedItemTypes = [ValueType.string, ValueType.reference];
375
- const resolvedListType = resolvedList.type;
376
- const resolvedItemType = resolvedItem.type;
377
- if (!allowedListTypes.includes(resolvedListType) || !allowedItemTypes.includes(resolvedItemType)) throw new Error(`List contains function expects a string array list and a string item, got ${resolvedList?.type} and ${resolvedItem?.type}`);
378
- const list = resolvedList.value;
379
- const item = resolvedItem.value;
380
- return {
381
- type: ValueType.boolean,
382
- value: list.includes(item)
383
- };
384
- }
385
- evaluateStrEq(expression) {
386
- if (expression.arguments.length !== 2) throw new Error(`String equals function expects 2 arguments, got ${expression.arguments.length}`);
387
- const resolvedA = this.eval(expression.arguments[0]);
388
- const resolvedB = this.eval(expression.arguments[1]);
389
- if (resolvedA === void 0 || resolvedB === void 0) return {
390
- type: ValueType.boolean,
391
- value: false
392
- };
393
- if (resolvedA?.type !== ValueType.string || resolvedB?.type !== ValueType.string) throw new Error(`String equals function expects strings for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
394
- return {
395
- type: ValueType.boolean,
396
- value: resolvedA.value === resolvedB.value
397
- };
398
- }
399
- evaluateEq(expression) {
400
- if (expression.arguments.length !== 2) throw new Error(`Equals function expects 2 arguments, got ${expression.arguments.length}`);
401
- const resolvedA = this.eval(expression.arguments[0]);
402
- const resolvedB = this.eval(expression.arguments[1]);
403
- if (resolvedA === void 0 || resolvedB === void 0) return {
404
- type: ValueType.boolean,
405
- value: false
406
- };
407
- if (resolvedA?.type !== resolvedB?.type) throw new Error(`Equals function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
408
- return {
409
- type: ValueType.boolean,
410
- value: resolvedA.value === resolvedB.value
411
- };
412
- }
413
- evaluateGt(expression) {
414
- if (expression.arguments.length !== 2) throw new Error(`Greater than function expects 2 arguments, got ${expression.arguments.length}`);
415
- const resolvedA = this.eval(expression.arguments[0]);
416
- const resolvedB = this.eval(expression.arguments[1]);
417
- if (resolvedA === void 0 || resolvedB === void 0) return {
418
- type: ValueType.boolean,
419
- value: false
420
- };
421
- if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
422
- return {
423
- type: ValueType.boolean,
424
- value: resolvedA.value > resolvedB.value
425
- };
426
- }
427
- evaluateGte(expression) {
428
- if (expression.arguments.length !== 2) throw new Error(`Greater than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
429
- const resolvedA = this.eval(expression.arguments[0]);
430
- const resolvedB = this.eval(expression.arguments[1]);
431
- if (resolvedA === void 0 || resolvedB === void 0) return {
432
- type: ValueType.boolean,
433
- value: false
434
- };
435
- if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
436
- return {
437
- type: ValueType.boolean,
438
- value: resolvedA.value >= resolvedB.value
439
- };
440
- }
441
- evaluateLt(expression) {
442
- if (expression.arguments.length !== 2) throw new Error(`Less than function expects 2 arguments, got ${expression.arguments.length}`);
443
- const resolvedA = this.eval(expression.arguments[0]);
444
- const resolvedB = this.eval(expression.arguments[1]);
445
- if (resolvedA === void 0 || resolvedB === void 0) return {
446
- type: ValueType.boolean,
447
- value: false
448
- };
449
- if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
450
- return {
451
- type: ValueType.boolean,
452
- value: resolvedA.value < resolvedB.value
453
- };
454
- }
455
- evaluateLte(expression) {
456
- if (expression.arguments.length !== 2) throw new Error(`Less than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
457
- const resolvedA = this.eval(expression.arguments[0]);
458
- const resolvedB = this.eval(expression.arguments[1]);
459
- if (resolvedA === void 0 || resolvedB === void 0) return {
460
- type: ValueType.boolean,
461
- value: false
462
- };
463
- if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
464
- return {
465
- type: ValueType.boolean,
466
- value: resolvedA.value <= resolvedB.value
467
- };
468
- }
469
- evaluateInRange(expression) {
470
- if (expression.arguments.length !== 4) throw new Error(`In range function expects 4 arguments, got ${expression.arguments.length}`);
471
- const resolvedValue = this.eval(expression.arguments[0]);
472
- const resolvedMin = this.eval(expression.arguments[1]);
473
- const resolvedMax = this.eval(expression.arguments[2]);
474
- const resolvedInclusive = this.eval(expression.arguments[3])?.value === true;
475
- if (resolvedValue === void 0 || resolvedMin === void 0 || resolvedMax === void 0) return {
476
- type: ValueType.boolean,
477
- value: false
478
- };
479
- if (resolvedValue?.type !== ValueType.number || resolvedMin?.type !== ValueType.number || resolvedMax?.type !== ValueType.number) throw new Error(`In range function expects numbers for all arguments, got ${resolvedValue?.type} and ${resolvedMin?.type} and ${resolvedMax?.type}`);
480
- if (resolvedInclusive) return {
481
- type: ValueType.boolean,
482
- value: resolvedValue.value >= resolvedMin.value && resolvedValue.value <= resolvedMax.value
483
- };
484
- return {
485
- type: ValueType.boolean,
486
- value: resolvedValue.value > resolvedMin.value && resolvedValue.value < resolvedMax.value
487
- };
488
- }
489
- evaluateSum(expression) {
490
- if (expression.arguments.length < 1) throw new Error(`Sum function expects at least 1 argument, got ${expression.arguments.length}`);
491
- return expression.arguments.reduce((sum, arg) => {
492
- if (arg === void 0) return sum;
493
- const resolvedValue = this.eval(arg);
494
- if (resolvedValue === void 0) return sum;
495
- if (resolvedValue?.type === ValueType.number) return {
496
- type: ValueType.number,
497
- value: sum.value + resolvedValue.value
498
- };
499
- throw new Error(`Sum function expects all arguments to be numbers, got ${resolvedValue?.type}`);
500
- }, {
501
- type: ValueType.number,
502
- value: 0
503
- });
504
- }
505
- evaluateMin(expression) {
506
- if (expression.arguments.length < 1) throw new Error(`Min function expects at least 1 argument, got ${expression.arguments.length}`);
507
- return expression.arguments.reduce((min, arg) => {
508
- if (arg === void 0) return min;
509
- const resolvedValue = this.eval(arg);
510
- if (resolvedValue === void 0) return min;
511
- if (resolvedValue?.type === ValueType.number) return {
512
- type: ValueType.number,
513
- value: Math.min(min.value, resolvedValue.value)
514
- };
515
- throw new Error(`Min function expects all arguments to be numbers, got ${typeof resolvedValue}`);
516
- }, {
517
- type: ValueType.number,
518
- value: Infinity
519
- });
65
+ return current;
66
+ };
67
+ const getValueAtJsonPointer = (document, pointer) => {
68
+ const segments = parseJsonPointer(pointer);
69
+ return getValueAtSegments(document, segments, pointer);
70
+ };
71
+ const deepEqualJson = (left, right) => {
72
+ if (left === right) return true;
73
+ if (Array.isArray(left) || Array.isArray(right)) {
74
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
75
+ return left.every((entry, index) => deepEqualJson(entry, right[index]));
520
76
  }
521
- evaluateMax(expression) {
522
- if (expression.arguments.length < 1) throw new Error(`Max function expects at least 1 argument, got ${expression.arguments.length}`);
523
- return expression.arguments.reduce((max, arg) => {
524
- if (arg === void 0) return max;
525
- const resolvedValue = this.eval(arg);
526
- if (resolvedValue === void 0) return max;
527
- if (resolvedValue?.type === ValueType.number) return {
528
- type: ValueType.number,
529
- value: Math.max(max.value, resolvedValue.value)
530
- };
531
- throw new Error(`Max function expects all arguments to be numbers, got ${typeof resolvedValue}`);
532
- }, {
533
- type: ValueType.number,
534
- value: -Infinity
535
- });
77
+ if (isRecord(left) || isRecord(right)) {
78
+ if (!isRecord(left) || !isRecord(right)) return false;
79
+ const leftKeys = Object.keys(left).sort();
80
+ const rightKeys = Object.keys(right).sort();
81
+ if (leftKeys.length !== rightKeys.length) return false;
82
+ return leftKeys.every((key, index) => key === rightKeys[index] && deepEqualJson(left[key], right[key]));
536
83
  }
84
+ return false;
537
85
  };
538
- //#endregion
539
- //#region src/expressions/item-expression-registry.ts
540
- function getItemExpressionDefinition(definitions, expressionId) {
541
- return definitions?.find((definition) => definition.id === expressionId);
542
- }
543
- function buildItemExpression(item, definitions, expressionId, params) {
544
- const definition = getItemExpressionDefinition(definitions, expressionId);
545
- if (!definition) throw new Error(`Unknown item expression: ${expressionId}`);
546
- return definition.buildExpression(item, params);
547
- }
548
- //#endregion
549
- //#region src/expressions/reference-integrity.ts
550
- const ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY = {
551
- component: false,
552
- item: true,
553
- survey: false
554
- };
555
- const ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY = {
556
- "display-condition": true,
557
- "disabled-condition": false,
558
- validation: false,
559
- "prefill-condition": false,
560
- "prefill-source": false,
561
- "template-value": false
562
- };
563
- /**
564
- * Display-condition cycles are advisory because responses may be populated before visibility is
565
- * evaluated, including through runtime prefills that are unavailable to static survey analysis.
566
- * Concrete expression integrity failures remain blocking. This exhaustive map is the source of
567
- * truth copied onto every analyzer-produced diagnostic.
568
- */
569
- const SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY = {
570
- "invalid-expression": {
571
- category: "reference-integrity",
572
- disposition: "blocking"
573
- },
574
- "unknown-response-ref": {
575
- category: "reference-integrity",
576
- disposition: "blocking"
577
- },
578
- "expression-reference-type-mismatch": {
579
- category: "reference-integrity",
580
- disposition: "blocking"
581
- },
582
- "unknown-reference-value": {
583
- category: "reference-integrity",
584
- disposition: "blocking"
585
- },
586
- "display-condition-cycle": {
587
- category: "display-dependency",
588
- disposition: "advisory"
589
- }
590
- };
591
- const createExpressionReferenceDiagnostic = (diagnostic) => ({
592
- ...diagnostic,
593
- ...SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY[diagnostic.code]
594
- });
595
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
596
- const isJsonExpression = (value) => isRecord$1(value) && typeof value.type === "string";
597
- const collectItemExpressionLocations = (item) => {
598
- const locations = [];
599
- const add = (expression, location, ownerScope, surface) => {
600
- if (!isJsonExpression(expression)) return;
601
- locations.push({
602
- expression,
603
- location,
604
- ownerItemId: item.id,
605
- ownerItemKey: item.key,
606
- ownerScope,
607
- surface
86
+ const applyAdd = (document, path, value) => {
87
+ const segments = parseJsonPointer(path);
88
+ if (segments.length === 0) return structuredCloneMethod(value);
89
+ const parentSegments = segments.slice(0, -1);
90
+ const key = segments[segments.length - 1];
91
+ const parent = getValueAtSegments(document, parentSegments, path);
92
+ if (Array.isArray(parent)) {
93
+ const index = parseArrayIndex({
94
+ segment: key,
95
+ arrayLength: parent.length,
96
+ allowAppend: true,
97
+ allowEnd: true,
98
+ path
608
99
  });
609
- };
610
- add(item.displayConditions?.root, "displayConditions.root", "item", "display-condition");
611
- for (const [componentId, expression] of Object.entries(item.displayConditions?.components ?? {})) add(expression, `displayConditions.components.${componentId}`, "component", "display-condition");
612
- for (const [componentId, expression] of Object.entries(item.disabledConditions?.components ?? {})) add(expression, `disabledConditions.components.${componentId}`, "component", "disabled-condition");
613
- for (const [validationKey, expression] of Object.entries(item.validations ?? {})) add(expression, `validations.${validationKey}`, "item", "validation");
614
- for (const [prefillIndex, prefill] of (item.prefills ?? []).entries()) {
615
- const prefillId = prefill.id || String(prefillIndex);
616
- add(prefill.when, `prefills.${prefillId}.when`, "item", "prefill-condition");
617
- if (prefill.source.type === "expression") add(prefill.source.expression, `prefills.${prefillId}.source.expression`, "item", "prefill-source");
618
- }
619
- return locations;
620
- };
621
- const collectSurveyExpressionLocations = (survey) => [...survey.surveyItems.flatMap(collectItemExpressionLocations), ...Object.entries(survey.templateValues ?? {}).flatMap(([templateKey, templateValue]) => templateValue.expression ? [{
622
- expression: templateValue.expression,
623
- location: `templateValues.${templateKey}.expression`,
624
- ownerScope: "survey",
625
- surface: "template-value"
626
- }] : [])];
627
- const candidateRefs = (unknownRef, availableRefs) => {
628
- let parsed;
629
- try {
630
- parsed = new ValueReference(unknownRef);
631
- } catch {
632
- return availableRefs.slice(0, 5);
633
- }
634
- const sameItemAndMethod = availableRefs.filter((candidate) => {
635
- try {
636
- const candidateRef = new ValueReference(candidate);
637
- return candidateRef.itemId === parsed?.itemId && candidateRef.name === parsed?.name;
638
- } catch {
639
- return false;
640
- }
641
- });
642
- return (sameItemAndMethod.length > 0 ? sameItemAndMethod : availableRefs).slice(0, 5);
643
- };
644
- const findCyclicItemDisplayDependencyEdges = ({ edges, itemIds }) => {
645
- const adjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
646
- const reverseAdjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
647
- for (const edge of edges) {
648
- adjacency.get(edge.fromItemId)?.add(edge.toItemId);
649
- reverseAdjacency.get(edge.toItemId)?.add(edge.fromItemId);
650
- }
651
- const finishOrder = [];
652
- const visited = /* @__PURE__ */ new Set();
653
- for (const startItemId of itemIds) {
654
- if (visited.has(startItemId)) continue;
655
- const stack = [{
656
- expanded: false,
657
- itemId: startItemId
658
- }];
659
- while (stack.length > 0) {
660
- const current = stack.pop();
661
- if (!current) break;
662
- if (current.expanded) {
663
- finishOrder.push(current.itemId);
664
- continue;
665
- }
666
- if (visited.has(current.itemId)) continue;
667
- visited.add(current.itemId);
668
- stack.push({
669
- expanded: true,
670
- itemId: current.itemId
671
- });
672
- const dependencies = [...adjacency.get(current.itemId) ?? []];
673
- for (let index = dependencies.length - 1; index >= 0; index -= 1) {
674
- const dependencyId = dependencies[index];
675
- if (visited.has(dependencyId)) continue;
676
- stack.push({
677
- expanded: false,
678
- itemId: dependencyId
679
- });
680
- }
681
- }
100
+ parent.splice(index, 0, structuredCloneMethod(value));
101
+ return document;
682
102
  }
683
- const componentByItemId = /* @__PURE__ */ new Map();
684
- const componentMembers = /* @__PURE__ */ new Map();
685
- for (const startItemId of finishOrder.reverse()) {
686
- if (componentByItemId.has(startItemId)) continue;
687
- const componentId = componentMembers.size;
688
- const members = [];
689
- const stack = [startItemId];
690
- componentByItemId.set(startItemId, componentId);
691
- while (stack.length > 0) {
692
- const itemId = stack.pop();
693
- if (!itemId) break;
694
- members.push(itemId);
695
- for (const dependencyOwnerId of reverseAdjacency.get(itemId) ?? []) {
696
- if (componentByItemId.has(dependencyOwnerId)) continue;
697
- componentByItemId.set(dependencyOwnerId, componentId);
698
- stack.push(dependencyOwnerId);
699
- }
700
- }
701
- componentMembers.set(componentId, members.sort());
702
- }
703
- return edges.flatMap((edge) => {
704
- const componentId = componentByItemId.get(edge.fromItemId);
705
- if (componentId === void 0 || componentId !== componentByItemId.get(edge.toItemId)) return [];
706
- const cycleItemIds = componentMembers.get(componentId) ?? [];
707
- if (edge.fromItemId !== edge.toItemId && cycleItemIds.length < 2) return [];
708
- return [{
709
- edge,
710
- cycleItemIds
711
- }];
103
+ if (!isRecord(parent)) throw new JsonPatchError(`JSON pointer "${path}" parent is not an object or array.`, "invalid-target-parent", path);
104
+ Object.defineProperty(parent, key, {
105
+ configurable: true,
106
+ enumerable: true,
107
+ value: structuredCloneMethod(value),
108
+ writable: true
712
109
  });
110
+ return document;
713
111
  };
714
- const inferExpressionType = (expression, slots) => {
715
- if (!expression) return void 0;
716
- if (expression.type === ExpressionType.Const) return expression.value?.type;
717
- if (expression.type === ExpressionType.ResponseVariable) return slots.get(expression.variableRef)?.valueType;
718
- if (expression.type === ExpressionType.ContextVariable) return expression.contextType === "locale" ? ValueType.string : expression.asType;
719
- switch (expression.functionName) {
720
- case "and":
721
- case "or":
722
- case "not":
723
- case "eq":
724
- case "gt":
725
- case "gte":
726
- case "lt":
727
- case "lte":
728
- case "in_range":
729
- case "list_contains":
730
- case "str_eq":
731
- case "date_eq": return ValueType.boolean;
732
- case "sum":
733
- case "min":
734
- case "max": return ValueType.number;
735
- }
736
- };
737
- const itemTypeForList = (type) => {
738
- switch (type) {
739
- case ValueType.stringArray: return ValueType.string;
740
- case ValueType.referenceArray: return ValueType.reference;
741
- case ValueType.numberArray: return ValueType.number;
742
- case ValueType.dateArray: return ValueType.date;
743
- case ValueType.durationArray: return ValueType.duration;
744
- default: return;
745
- }
746
- };
747
- const inspectFunctionReferenceCompatibility = ({ diagnostics, expression, location, slots }) => {
748
- if (expression.type !== ExpressionType.Function) return;
749
- expression.arguments.forEach((argument) => {
750
- if (argument) inspectFunctionReferenceCompatibility({
751
- diagnostics,
752
- expression: argument,
753
- location,
754
- slots
755
- });
756
- });
757
- const [left, right] = expression.arguments;
758
- const leftType = inferExpressionType(left, slots);
759
- const rightType = inferExpressionType(right, slots);
760
- if (expression.functionName === "list_contains" ? Boolean(leftType && rightType && itemTypeForList(leftType) !== rightType) : expression.functionName === "eq" ? Boolean(leftType && rightType && leftType !== rightType) : false) diagnostics.push({
761
- code: "expression-reference-type-mismatch",
762
- message: `${location.location} compares incompatible values (${leftType} and ${rightType}).`,
763
- location: location.location,
764
- ownerItemId: location.ownerItemId,
765
- ownerItemKey: location.ownerItemKey
766
- });
767
- if (expression.functionName !== "eq" && expression.functionName !== "list_contains") return;
768
- const pairs = [[left, right], [right, left]];
769
- for (const [variable, constant] of pairs) {
770
- if (variable?.type !== ExpressionType.ResponseVariable || constant?.type !== ExpressionType.Const || constant.value?.type !== ValueType.reference) continue;
771
- const slot = slots.get(variable.variableRef);
772
- if (slot?.allowedReferenceValues && !slot.allowedReferenceValues.includes(constant.value.value)) diagnostics.push({
773
- code: "unknown-reference-value",
774
- message: `${location.location} compares "${variable.variableRef}" with missing option "${constant.value.value}".`,
775
- location: location.location,
776
- ownerItemId: location.ownerItemId,
777
- ownerItemKey: location.ownerItemKey,
778
- reference: variable.variableRef,
779
- referenceValue: constant.value.value,
780
- candidates: slot.allowedReferenceValues.slice(0, 5)
112
+ const applyRemove = (document, path) => {
113
+ const segments = parseJsonPointer(path);
114
+ if (segments.length === 0) throw new JsonPatchError("Removing the document root is not supported.", "remove-root", path);
115
+ const parentSegments = segments.slice(0, -1);
116
+ const key = segments[segments.length - 1];
117
+ const parent = getValueAtSegments(document, parentSegments, path);
118
+ if (Array.isArray(parent)) {
119
+ const index = parseArrayIndex({
120
+ segment: key,
121
+ arrayLength: parent.length,
122
+ allowAppend: false,
123
+ allowEnd: false,
124
+ path
781
125
  });
126
+ const [removedValue] = parent.splice(index, 1);
127
+ return {
128
+ document,
129
+ removedValue
130
+ };
782
131
  }
132
+ if (!isRecord(parent) || !hasOwn(parent, key)) throw new JsonPatchError(`JSON pointer "${path}" does not exist.`, "path-not-found", path);
133
+ const removedValue = parent[key];
134
+ delete parent[key];
135
+ return {
136
+ document,
137
+ removedValue
138
+ };
139
+ };
140
+ const applyReplace = (document, path, value) => {
141
+ const segments = parseJsonPointer(path);
142
+ if (segments.length === 0) return structuredCloneMethod(value);
143
+ getValueAtSegments(document, segments, path);
144
+ applyRemove(document, path);
145
+ return applyAdd(document, path, value);
146
+ };
147
+ const assertMoveTargetIsNotDescendant = (from, path) => {
148
+ if (from === "" || path === from || !path.startsWith(`${from}/`)) return;
149
+ throw new JsonPatchError(`JSON Patch cannot move "${from}" into its own descendant "${path}".`, "move-into-descendant", path);
783
150
  };
784
- const analyzeSurveyExpressionReferences = ({ responseSlots, survey }) => {
785
- const slots = new Map(responseSlots.map((slot) => [slot.ref, slot]));
786
- const availableRefs = responseSlots.map((slot) => slot.ref);
787
- const diagnostics = [];
788
- const surveyItemIds = survey.surveyItems.map((item) => item.id);
789
- const surveyItemIdSet = new Set(surveyItemIds);
790
- const itemDisplayDependencyEdges = [];
791
- for (const location of collectSurveyExpressionLocations(survey)) {
792
- let parsed;
151
+ const applyJsonPatch = (document, operations) => {
152
+ let patchedDocument = structuredCloneMethod(document);
153
+ operations.forEach((operation, operationIndex) => {
793
154
  try {
794
- parsed = Expression.deserialize(location.expression);
155
+ switch (operation.op) {
156
+ case "add":
157
+ patchedDocument = applyAdd(patchedDocument, operation.path, operation.value);
158
+ break;
159
+ case "remove":
160
+ patchedDocument = applyRemove(patchedDocument, operation.path).document;
161
+ break;
162
+ case "replace":
163
+ patchedDocument = applyReplace(patchedDocument, operation.path, operation.value);
164
+ break;
165
+ case "move": {
166
+ assertMoveTargetIsNotDescendant(operation.from, operation.path);
167
+ if (operation.from === operation.path) break;
168
+ const { document: withoutValue, removedValue } = applyRemove(patchedDocument, operation.from);
169
+ patchedDocument = applyAdd(withoutValue, operation.path, removedValue);
170
+ break;
171
+ }
172
+ case "copy": {
173
+ const value = getValueAtJsonPointer(patchedDocument, operation.from);
174
+ patchedDocument = applyAdd(patchedDocument, operation.path, value);
175
+ break;
176
+ }
177
+ case "test": {
178
+ const value = getValueAtJsonPointer(patchedDocument, operation.path);
179
+ if (!deepEqualJson(value, operation.value)) throw new JsonPatchError(`JSON Patch test failed at "${operation.path}".`, "test-failed", operation.path);
180
+ break;
181
+ }
182
+ }
795
183
  } catch (error) {
796
- diagnostics.push({
797
- code: "invalid-expression",
798
- message: `${location.location} contains invalid expression JSON: ${error instanceof Error ? error.message : String(error)}`,
799
- location: location.location,
800
- ownerItemId: location.ownerItemId,
801
- ownerItemKey: location.ownerItemKey
802
- });
803
- continue;
804
- }
805
- for (const ref of parsed?.responseVariableRefs ?? []) {
806
- const reference = ref.toString();
807
- if (location.ownerItemId && ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY[location.ownerScope] && ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY[location.surface] && surveyItemIdSet.has(ref.itemId)) itemDisplayDependencyEdges.push({
808
- fromItemId: location.ownerItemId,
809
- location,
810
- reference,
811
- toItemId: ref.itemId
812
- });
813
- if (!slots.has(reference)) diagnostics.push({
814
- code: "unknown-response-ref",
815
- message: `${location.location} references unknown response slot "${reference}".`,
816
- location: location.location,
817
- ownerItemId: location.ownerItemId,
818
- ownerItemKey: location.ownerItemKey,
819
- reference,
820
- candidates: candidateRefs(reference, availableRefs)
821
- });
822
- }
823
- inspectFunctionReferenceCompatibility({
824
- diagnostics,
825
- expression: location.expression,
826
- location,
827
- slots
828
- });
829
- }
830
- for (const { cycleItemIds, edge } of findCyclicItemDisplayDependencyEdges({
831
- edges: itemDisplayDependencyEdges,
832
- itemIds: surveyItemIds
833
- })) {
834
- const isSelfReference = edge.fromItemId === edge.toItemId;
835
- diagnostics.push({
836
- code: "display-condition-cycle",
837
- message: isSelfReference ? `${edge.location.location} for item "${edge.fromItemId}" references its own response "${edge.reference}", creating a circular display dependency.` : `${edge.location.location} for item "${edge.fromItemId}" references item "${edge.toItemId}" within a circular display dependency among ${cycleItemIds.map((itemId) => `"${itemId}"`).join(", ")}.`,
838
- location: edge.location.location,
839
- ownerItemId: edge.location.ownerItemId,
840
- ownerItemKey: edge.location.ownerItemKey,
841
- reference: edge.reference
842
- });
843
- }
844
- return diagnostics.map(createExpressionReferenceDiagnostic);
845
- };
846
- const expressionReferenceDiagnosticKey = (diagnostic) => [
847
- diagnostic.code,
848
- diagnostic.ownerItemId ?? "survey",
849
- diagnostic.location,
850
- diagnostic.reference ?? "",
851
- diagnostic.referenceValue ?? ""
852
- ].join("\0");
853
- const findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories = ({ after, before }) => {
854
- const existing = new Set(before.map(expressionReferenceDiagnosticKey));
855
- return after.filter((diagnostic) => !existing.has(expressionReferenceDiagnosticKey(diagnostic)));
856
- };
857
- const isBlockingSurveyExpressionReferenceDiagnostic = (diagnostic) => {
858
- switch (diagnostic.disposition) {
859
- case "blocking": return true;
860
- case "advisory": return false;
861
- default: return true;
862
- }
863
- };
864
- const findIntroducedBlockingExpressionReferenceDiagnostics = ({ after, before }) => findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories({
865
- after,
866
- before
867
- }).filter(isBlockingSurveyExpressionReferenceDiagnostic);
868
- const replaceResponseVariableReferences = (value, replacements) => {
869
- let changed = false;
870
- const visit = (current) => {
871
- if (Array.isArray(current)) return current.map(visit);
872
- if (!isRecord$1(current)) return current;
873
- const next = Object.fromEntries(Object.entries(current).map(([key, entry]) => [key, visit(entry)]));
874
- if (next.type === ExpressionType.ResponseVariable && typeof next.variableRef === "string" && replacements[next.variableRef]) {
875
- next.variableRef = replacements[next.variableRef];
876
- changed = true;
184
+ if (error instanceof JsonPatchError) throw new JsonPatchError(error.message, error.code, error.path, operationIndex);
185
+ throw error;
877
186
  }
878
- return next;
879
- };
880
- const rewritten = visit(value);
187
+ });
188
+ return patchedDocument;
189
+ };
190
+ //#endregion
191
+ //#region src/expressions/shortcuts/selection.ts
192
+ /**
193
+ * Build a selected-option test joined with `not(ownerState.disabled)` for that option.
194
+ * `multiple` chooses membership instead of equality. The result is an ordinary expression tree;
195
+ * no shortcut node is persisted or handled specially by the evaluator.
196
+ */
197
+ function selectedAndEnabled(selection, optionId, multiple) {
881
198
  return {
882
- changed,
883
- value: rewritten
199
+ type: "function",
200
+ functionName: "and",
201
+ arguments: [{
202
+ type: "function",
203
+ functionName: multiple ? "list_contains" : "eq",
204
+ arguments: [selection, {
205
+ type: "const",
206
+ value: {
207
+ type: "reference",
208
+ value: optionId
209
+ }
210
+ }]
211
+ }, {
212
+ type: "function",
213
+ functionName: "not",
214
+ arguments: [{
215
+ type: "ownerState",
216
+ state: "disabled",
217
+ targetRef: { ownerId: optionId }
218
+ }]
219
+ }]
884
220
  };
885
- };
886
- const responseValueReferenceIds = (values) => values?.flatMap((value) => value.type === ValueType.reference ? [value.value] : value.type === ValueType.referenceArray ? value.value : []);
221
+ }
222
+ //#endregion
223
+ //#region src/survey/utils/group-utils.ts
224
+ /**
225
+ * Shared utilities for array operations.
226
+ */
227
+ /**
228
+ * Swap two elements in an array by index.
229
+ * @returns A new array with the elements swapped
230
+ */
231
+ function swapArrayElements(arr, fromIndex, toIndex) {
232
+ const result = [...arr];
233
+ [result[fromIndex], result[toIndex]] = [result[toIndex], result[fromIndex]];
234
+ return result;
235
+ }
236
+ /**
237
+ * Move an element from one index to another.
238
+ * @returns A new array with the element moved
239
+ */
240
+ function moveArrayElement(arr, fromIndex, toIndex) {
241
+ const result = [...arr];
242
+ const [moved] = result.splice(fromIndex, 1);
243
+ result.splice(toIndex, 0, moved);
244
+ return result;
245
+ }
887
246
  //#endregion
888
247
  //#region src/expressions/editors/expression-editor.ts
248
+ const constantNode = (value, editorConfig) => {
249
+ const node = expressionSchema.parse({
250
+ type: "const",
251
+ value,
252
+ editorConfig
253
+ });
254
+ if (node.type !== "const") throw new Error("Expected constant node");
255
+ return node;
256
+ };
257
+ /** Editor-state builders produce the canonical JSON tree, never runtime wrappers. */
258
+ const createFunctionExpression = (functionName, args, editorConfig) => ({
259
+ type: "function",
260
+ functionName,
261
+ arguments: expressionArguments(args),
262
+ editorConfig
263
+ });
264
+ const contextNode = (contextType, key, args, asType, editorConfig) => ({
265
+ type: "contextVariable",
266
+ contextType,
267
+ ...key ? { key } : {},
268
+ ...args ? { arguments: expressionArguments(args) } : {},
269
+ asType,
270
+ editorConfig
271
+ });
889
272
  var ExpressionEditor = class {
890
273
  returnType;
891
274
  _editorConfig;
@@ -912,7 +295,7 @@ var ConstStringArrayEditor = class extends ExpressionEditor {
912
295
  this._values = values;
913
296
  }
914
297
  getExpression() {
915
- return new ConstExpression({
298
+ return constantNode({
916
299
  type: ValueType.stringArray,
917
300
  value: this._values
918
301
  }, this._editorConfig);
@@ -933,7 +316,7 @@ var ConstStringEditor = class extends ExpressionEditor {
933
316
  this._value = value;
934
317
  }
935
318
  getExpression() {
936
- return new ConstExpression({
319
+ return constantNode({
937
320
  type: ValueType.string,
938
321
  value: this._value
939
322
  }, this._editorConfig);
@@ -954,7 +337,7 @@ var ConstNumberEditor = class extends ExpressionEditor {
954
337
  this._value = value;
955
338
  }
956
339
  getExpression() {
957
- return new ConstExpression({
340
+ return constantNode({
958
341
  type: ValueType.number,
959
342
  value: this._value
960
343
  }, this._editorConfig);
@@ -975,7 +358,7 @@ var ConstBooleanEditor = class extends ExpressionEditor {
975
358
  this._value = value;
976
359
  }
977
360
  getExpression() {
978
- return new ConstExpression({
361
+ return constantNode({
979
362
  type: ValueType.boolean,
980
363
  value: this._value
981
364
  }, this._editorConfig);
@@ -996,8 +379,8 @@ var ConstDateEditor = class extends ExpressionEditor {
996
379
  this._value = value;
997
380
  }
998
381
  getExpression() {
999
- const value = Math.floor(this._value.getTime() / 1e3);
1000
- return new ConstExpression({
382
+ const value = this._value;
383
+ return constantNode({
1001
384
  type: ValueType.date,
1002
385
  value
1003
386
  }, this._editorConfig);
@@ -1018,7 +401,7 @@ var ConstNumberArrayEditor = class extends ExpressionEditor {
1018
401
  this._values = values;
1019
402
  }
1020
403
  getExpression() {
1021
- return new ConstExpression({
404
+ return constantNode({
1022
405
  type: ValueType.numberArray,
1023
406
  value: this._values
1024
407
  }, this._editorConfig);
@@ -1039,31 +422,44 @@ var ConstDateArrayEditor = class extends ExpressionEditor {
1039
422
  this._values = values;
1040
423
  }
1041
424
  getExpression() {
1042
- const value = this._values.map((date) => Math.floor(date.getTime() / 1e3));
1043
- return new ConstExpression({
425
+ const value = this._values;
426
+ return constantNode({
1044
427
  type: ValueType.dateArray,
1045
428
  value
1046
429
  }, this._editorConfig);
1047
430
  }
1048
431
  };
1049
432
  var ResponseVariableEditor = class extends ExpressionEditor {
1050
- _variableName;
1051
- _variableRef;
1052
- constructor(variableName, variableType, editorConfig) {
433
+ variableRef;
434
+ constructor(variableRef, variableType, editorConfig) {
1053
435
  super();
1054
- this._variableName = variableName;
1055
- this._variableRef = new ValueReference(variableName);
436
+ this.variableRef = variableRef;
1056
437
  this._editorConfig = editorConfig;
1057
- this.returnType = variableType;
438
+ this.returnType = variableRef.method === "isDefined" ? ValueType.boolean : variableType;
1058
439
  }
1059
- get variableName() {
1060
- return this._variableName;
440
+ getExpression() {
441
+ return {
442
+ type: "responseVariable",
443
+ variableRef: { ...this.variableRef },
444
+ editorConfig: this._editorConfig
445
+ };
1061
446
  }
1062
- get variableRef() {
1063
- return this._variableRef;
447
+ };
448
+ var DisabledStateEditor = class extends ExpressionEditor {
449
+ ownerId;
450
+ returnType = ValueType.boolean;
451
+ constructor(ownerId, editorConfig) {
452
+ super();
453
+ this.ownerId = ownerId;
454
+ this._editorConfig = editorConfig;
1064
455
  }
1065
456
  getExpression() {
1066
- return new ResponseVariableExpression(this._variableName, this._editorConfig);
457
+ return {
458
+ type: "ownerState",
459
+ state: "disabled",
460
+ targetRef: { ownerId: this.ownerId },
461
+ editorConfig: this._editorConfig
462
+ };
1067
463
  }
1068
464
  };
1069
465
  var ContextVariableEditor = class extends ExpressionEditor {
@@ -1080,48 +476,48 @@ var ContextVariableEditor = class extends ExpressionEditor {
1080
476
  this._editorConfig = editorConfig;
1081
477
  }
1082
478
  getExpression() {
1083
- return new ContextVariableExpression(this._contextType, this._key?.getExpression(), this._args?.map((arg) => arg.getExpression()), this._asType, this._editorConfig);
479
+ return contextNode(this._contextType, this._key?.getExpression(), this._args?.map((arg) => arg.getExpression()), this._asType, this._editorConfig);
1084
480
  }
1085
481
  };
1086
482
  var CtxLocaleEditor = class extends ContextVariableEditor {
1087
483
  returnType = ValueType.string;
1088
484
  constructor(editorConfig) {
1089
- super(ContextVariableType.Locale, void 0, void 0, ValueType.string, editorConfig);
485
+ super("locale", void 0, void 0, ValueType.string, editorConfig);
486
+ }
487
+ };
488
+ var CtxCurrentDateEditor = class extends ContextVariableEditor {
489
+ returnType = ValueType.date;
490
+ constructor(editorConfig) {
491
+ super("currentDate", void 0, void 0, ValueType.date, editorConfig);
1090
492
  }
1091
493
  };
1092
494
  var CtxPFlagIsDefinedEditor = class extends ContextVariableEditor {
1093
495
  returnType = ValueType.boolean;
1094
496
  constructor(key, editorConfig) {
1095
- super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.boolean, editorConfig);
497
+ super("participantFlag", key, void 0, ValueType.boolean, editorConfig);
1096
498
  }
1097
499
  };
1098
500
  var CtxPFlagStringEditor = class extends ContextVariableEditor {
1099
501
  returnType = ValueType.string;
1100
502
  constructor(key, editorConfig) {
1101
- super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.string, editorConfig);
503
+ super("participantFlag", key, void 0, ValueType.string, editorConfig);
1102
504
  }
1103
505
  };
1104
506
  var CtxPFlagNumEditor = class extends ContextVariableEditor {
1105
507
  returnType = ValueType.number;
1106
508
  constructor(key, editorConfig) {
1107
- super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.number, editorConfig);
509
+ super("participantFlag", key, void 0, ValueType.number, editorConfig);
1108
510
  }
1109
511
  };
1110
512
  var CtxPFlagDateEditor = class extends ContextVariableEditor {
1111
513
  returnType = ValueType.date;
1112
514
  constructor(key, editorConfig) {
1113
- super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.date, editorConfig);
515
+ super("participantFlag", key, void 0, ValueType.date, editorConfig);
1114
516
  }
1115
517
  };
1116
518
  var CtxCustomValueEditor = class extends ContextVariableEditor {
1117
519
  constructor(key, asType, editorConfig) {
1118
- super(ContextVariableType.CustomValue, key, void 0, asType, editorConfig);
1119
- this.returnType = asType;
1120
- }
1121
- };
1122
- var CtxCustomExpressionEditor = class extends ContextVariableEditor {
1123
- constructor(key, args, asType, editorConfig) {
1124
- super(ContextVariableType.CustomExpression, key, args, asType, editorConfig);
520
+ super("customValue", key, void 0, asType, editorConfig);
1125
521
  this.returnType = asType;
1126
522
  }
1127
523
  };
@@ -1146,10 +542,7 @@ var GroupExpressionEditor = class extends ExpressionEditor {
1146
542
  this._args[position] = arg;
1147
543
  }
1148
544
  swapArgs(activeIndex, overIndex) {
1149
- const newOrder = [...this._args];
1150
- newOrder.splice(activeIndex, 1);
1151
- newOrder.splice(overIndex, 0, this._args[activeIndex]);
1152
- this._args = newOrder;
545
+ this._args = moveArrayElement(this._args, activeIndex, overIndex);
1153
546
  }
1154
547
  };
1155
548
  var AndExpressionEditor = class extends GroupExpressionEditor {
@@ -1159,7 +552,7 @@ var AndExpressionEditor = class extends GroupExpressionEditor {
1159
552
  super(args, editorConfig);
1160
553
  }
1161
554
  getExpression() {
1162
- return new FunctionExpression("and", this.args.map((arg) => arg.getExpression()), this._editorConfig);
555
+ return createFunctionExpression("and", this.args.map((arg) => arg.getExpression()), this._editorConfig);
1163
556
  }
1164
557
  };
1165
558
  var OrExpressionEditor = class extends GroupExpressionEditor {
@@ -1169,7 +562,7 @@ var OrExpressionEditor = class extends GroupExpressionEditor {
1169
562
  super(args, editorConfig);
1170
563
  }
1171
564
  getExpression() {
1172
- return new FunctionExpression("or", this.args.map((arg) => arg.getExpression()), this._editorConfig);
565
+ return createFunctionExpression("or", this.args.map((arg) => arg.getExpression()), this._editorConfig);
1173
566
  }
1174
567
  };
1175
568
  var StrListContainsExpressionEditor = class extends ExpressionEditor {
@@ -1197,7 +590,7 @@ var StrListContainsExpressionEditor = class extends ExpressionEditor {
1197
590
  this._item = item;
1198
591
  }
1199
592
  getExpression() {
1200
- return new FunctionExpression("list_contains", [this._list?.getExpression(), this._item?.getExpression()], this._editorConfig);
593
+ return createFunctionExpression("list_contains", [this._list?.getExpression(), this._item?.getExpression()], this._editorConfig);
1201
594
  }
1202
595
  };
1203
596
  var StrEqExpressionEditor = class extends ExpressionEditor {
@@ -1223,7 +616,7 @@ var StrEqExpressionEditor = class extends ExpressionEditor {
1223
616
  this._b = b;
1224
617
  }
1225
618
  getExpression() {
1226
- return new FunctionExpression("str_eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
619
+ return createFunctionExpression("str_eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
1227
620
  }
1228
621
  };
1229
622
  var EqExpressionEditor = class extends ExpressionEditor {
@@ -1249,7 +642,7 @@ var EqExpressionEditor = class extends ExpressionEditor {
1249
642
  this._b = b;
1250
643
  }
1251
644
  getExpression() {
1252
- return new FunctionExpression("eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
645
+ return createFunctionExpression("eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
1253
646
  }
1254
647
  };
1255
648
  var GtExpressionEditor = class extends ExpressionEditor {
@@ -1275,7 +668,7 @@ var GtExpressionEditor = class extends ExpressionEditor {
1275
668
  this._b = b;
1276
669
  }
1277
670
  getExpression() {
1278
- return new FunctionExpression("gt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
671
+ return createFunctionExpression("gt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
1279
672
  }
1280
673
  };
1281
674
  var GteExpressionEditor = class extends ExpressionEditor {
@@ -1301,7 +694,7 @@ var GteExpressionEditor = class extends ExpressionEditor {
1301
694
  this._b = b;
1302
695
  }
1303
696
  getExpression() {
1304
- return new FunctionExpression("gte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
697
+ return createFunctionExpression("gte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
1305
698
  }
1306
699
  };
1307
700
  var LtExpressionEditor = class extends ExpressionEditor {
@@ -1327,7 +720,7 @@ var LtExpressionEditor = class extends ExpressionEditor {
1327
720
  this._b = b;
1328
721
  }
1329
722
  getExpression() {
1330
- return new FunctionExpression("lt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
723
+ return createFunctionExpression("lt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
1331
724
  }
1332
725
  };
1333
726
  var LteExpressionEditor = class extends ExpressionEditor {
@@ -1353,7 +746,7 @@ var LteExpressionEditor = class extends ExpressionEditor {
1353
746
  this._a = a;
1354
747
  }
1355
748
  getExpression() {
1356
- return new FunctionExpression("lte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
749
+ return createFunctionExpression("lte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
1357
750
  }
1358
751
  };
1359
752
  var InRangeExpressionEditor = class extends ExpressionEditor {
@@ -1371,7 +764,7 @@ var InRangeExpressionEditor = class extends ExpressionEditor {
1371
764
  this._editorConfig = editorConfig;
1372
765
  }
1373
766
  getExpression() {
1374
- return new FunctionExpression("in_range", [
767
+ return createFunctionExpression("in_range", [
1375
768
  this.value?.getExpression(),
1376
769
  this.min?.getExpression(),
1377
770
  this.max?.getExpression(),
@@ -1385,7 +778,7 @@ var SumExpressionEditor = class extends GroupExpressionEditor {
1385
778
  super(args, editorConfig);
1386
779
  }
1387
780
  getExpression() {
1388
- return new FunctionExpression("sum", this.args.map((arg) => arg.getExpression()), this._editorConfig);
781
+ return createFunctionExpression("sum", this.args.map((arg) => arg.getExpression()), this._editorConfig);
1389
782
  }
1390
783
  };
1391
784
  var MinExpressionEditor = class extends GroupExpressionEditor {
@@ -1394,7 +787,7 @@ var MinExpressionEditor = class extends GroupExpressionEditor {
1394
787
  super(args, editorConfig);
1395
788
  }
1396
789
  getExpression() {
1397
- return new FunctionExpression("min", this.args.map((arg) => arg.getExpression()), this._editorConfig);
790
+ return createFunctionExpression("min", this.args.map((arg) => arg.getExpression()), this._editorConfig);
1398
791
  }
1399
792
  };
1400
793
  var MaxExpressionEditor = class extends GroupExpressionEditor {
@@ -1403,7 +796,7 @@ var MaxExpressionEditor = class extends GroupExpressionEditor {
1403
796
  super(args, editorConfig);
1404
797
  }
1405
798
  getExpression() {
1406
- return new FunctionExpression("max", this.args.map((arg) => arg.getExpression()), this._editorConfig);
799
+ return createFunctionExpression("max", this.args.map((arg) => arg.getExpression()), this._editorConfig);
1407
800
  }
1408
801
  };
1409
802
  //#endregion
@@ -1453,6 +846,9 @@ const response_date_array = (valueRef) => {
1453
846
  const ctx_locale = () => {
1454
847
  return new CtxLocaleEditor();
1455
848
  };
849
+ const ctx_current_date = () => {
850
+ return new CtxCurrentDateEditor();
851
+ };
1456
852
  const ctx_pflag_is_defined = (key) => {
1457
853
  return new CtxPFlagIsDefinedEditor(key);
1458
854
  };
@@ -1468,9 +864,6 @@ const ctx_pflag_date = (key) => {
1468
864
  const ctx_custom_value = (key, dType) => {
1469
865
  return new CtxCustomValueEditor(key, dType);
1470
866
  };
1471
- const ctx_custom_expression = (key, args, returnType) => {
1472
- return new CtxCustomExpressionEditor(key, args, returnType);
1473
- };
1474
867
  const and = (...args) => {
1475
868
  return new AndExpressionEditor(args);
1476
869
  };
@@ -1480,6 +873,14 @@ const or = (...args) => {
1480
873
  const str_list_contains = (list, item) => {
1481
874
  return new StrListContainsExpressionEditor(list, item);
1482
875
  };
876
+ /** Number of list entries, or of entries that also appear in `filter` (e.g. listed options). */
877
+ const list_count = (list, filter) => new FunctionCallEditor("list_count", filter ? [list, filter] : [list], ValueType.number);
878
+ /** `then` when the condition is true, otherwise `otherwise` (missing when omitted). */
879
+ const if_else = (condition, then, otherwise) => new FunctionCallEditor("if", otherwise ? [
880
+ condition,
881
+ then,
882
+ otherwise
883
+ ] : [condition, then], then.returnType ?? otherwise?.returnType);
1483
884
  const str_eq = (a, b) => {
1484
885
  return new StrEqExpressionEditor(a, b);
1485
886
  };
@@ -1510,757 +911,827 @@ const min = (...args) => {
1510
911
  const max = (...args) => {
1511
912
  return new MaxExpressionEditor(args);
1512
913
  };
1513
- //#endregion
1514
- //#region src/engine/engine.ts
1515
- var SurveyEngineCore = class {
1516
- surveyDef;
1517
- renderedSurveyTree;
1518
- context;
1519
- locale;
1520
- sessionSeedValue;
1521
- responses;
1522
- prefills;
1523
- _events;
1524
- _openedAt;
1525
- dateLocales;
1526
- prefillEnvironment;
1527
- cache;
1528
- constructor(survey, context, prefills, dateLocales, options) {
1529
- this._openedAt = this._getTimestamp();
1530
- this.sessionSeedValue = generateId();
1531
- this.surveyDef = survey;
1532
- this.context = context ? context : { locale: "en" };
1533
- this.locale = this.context.locale;
1534
- this.prefills = prefills ? prefills.reduce((acc, p) => {
1535
- acc[p.itemId] = SurveyItemResponse.deserialize(p);
1536
- return acc;
1537
- }, {}) : void 0;
1538
- this.prefillEnvironment = options?.prefillEnvironment;
1539
- this.dateLocales = dateLocales || [{
1540
- code: "en",
1541
- locale: enUS
1542
- }];
1543
- this.responses = this.initResponseObject(this.surveyDef.surveyItems);
1544
- this.initCache();
1545
- this.applyConfiguredPrefills();
1546
- this.evalExpressions();
1547
- if (!survey.rootItem) throw new Error("constructor: root item not found");
1548
- this.renderedSurveyTree = this.renderGroup(survey.rootItem);
1549
- this._events = [{
1550
- type: SurveyEventTypes.openSurvey,
1551
- timestamp: this._getTimestamp(),
1552
- value: this.context.locale
1553
- }];
1554
- }
1555
- setContext(context) {
1556
- this.context = context;
1557
- }
1558
- getDateLocales() {
1559
- return this.dateLocales.slice();
1560
- }
1561
- getCurrentDateLocale() {
1562
- const found = this.dateLocales.find((dl) => dl.code === this.locale);
1563
- if (!found) {
1564
- console.warn(`Locale '${this.locale}' is not available. Using default locale.`);
1565
- if (this.dateLocales.length > 0) return this.dateLocales[0].locale;
1566
- return enUS;
1567
- }
1568
- return found?.locale;
1569
- }
1570
- updateContext(context) {
1571
- this.context = context;
1572
- this.locale = context.locale;
1573
- this._registerSurveyEvent({
1574
- type: SurveyEventTypes.languageChanged,
1575
- timestamp: this._getTimestamp(),
1576
- value: context.locale
1577
- });
1578
- this.evalExpressions();
1579
- this.reRenderSurveyTree();
1580
- }
1581
- setResponse(targetKey, response) {
1582
- const target = this.getResponseItem(targetKey);
1583
- if (!target) throw new Error("setResponse: target not found for key: " + targetKey);
1584
- target.response = response;
1585
- this._registerSurveyEvent({
1586
- type: SurveyEventTypes.responeChanged,
1587
- timestamp: this._getTimestamp(),
1588
- itemId: targetKey
1589
- });
1590
- this.evalExpressions();
1591
- this.reRenderSurveyTree();
1592
- }
1593
- get openedAt() {
1594
- return this._openedAt;
1595
- }
1596
- get sessionSeed() {
1597
- return this.sessionSeedValue;
1598
- }
1599
- get survey() {
1600
- return this.surveyDef;
1601
- }
1602
- getSurveyPages(size) {
1603
- const renderedSurvey = flattenTree(this.renderedSurveyTree);
1604
- const pages = new Array();
1605
- if (!size) size = "large";
1606
- let currentPage = [];
1607
- renderedSurvey.forEach((item) => {
1608
- if (item.type === "page-break") {
1609
- if (currentPage.length > 0) {
1610
- pages.push([...currentPage]);
1611
- currentPage = [];
1612
- }
1613
- return;
1614
- }
1615
- currentPage.push(item);
1616
- if (!this.surveyDef.maxItemsPerPage) return;
1617
- let max = 0;
1618
- switch (size) {
1619
- case "large":
1620
- max = this.surveyDef.maxItemsPerPage.large;
1621
- break;
1622
- case "small":
1623
- max = this.surveyDef.maxItemsPerPage.small;
1624
- break;
1625
- }
1626
- if (currentPage.length >= max) {
1627
- pages.push([...currentPage]);
1628
- currentPage = [];
1629
- }
1630
- });
1631
- if (currentPage.length > 0) pages.push([...currentPage]);
1632
- return pages;
1633
- }
1634
- getRenderedSurveyItem(itemId) {
1635
- return flattenTree(this.renderedSurveyTree).find((item) => item.id === itemId);
1636
- }
1637
- onPageChanged(pageIndex) {
1638
- this._registerSurveyEvent({
1639
- type: SurveyEventTypes.pageChanged,
1640
- timestamp: this._getTimestamp(),
1641
- value: pageIndex
1642
- });
1643
- }
1644
- getEvents() {
1645
- return this._events.slice();
914
+ var FunctionCallEditor = class extends ExpressionEditor {
915
+ name;
916
+ args;
917
+ returnType;
918
+ constructor(name, args, returnType) {
919
+ super();
920
+ this.name = name;
921
+ this.args = args;
922
+ this.returnType = returnType;
1646
923
  }
1647
- getResponses() {
1648
- const renderedPositions = /* @__PURE__ */ new Map();
1649
- flattenTree(this.renderedSurveyTree).filter((item) => item.type !== "page-break").forEach((item, index) => {
1650
- if (this.responses[item.id]) renderedPositions.set(item.id, index);
1651
- });
1652
- const responses = [];
1653
- this.surveyDef.surveyItems.forEach((item, itemId) => {
1654
- if (!item.isInteractive()) return;
1655
- const response = this.getResponseItem(itemId);
1656
- if (!response) return;
1657
- const position = renderedPositions.get(itemId);
1658
- response.meta = position === void 0 ? void 0 : { position };
1659
- responses.push(response);
1660
- });
1661
- return responses;
1662
- }
1663
- getDisplayConditionValue(itemKey, componentKey) {
1664
- if (componentKey) return this.cache.displayConditions[itemKey]?.components?.[componentKey]?.result;
1665
- return this.cache.displayConditions[itemKey]?.root?.result;
1666
- }
1667
- getDisabledConditionValue(itemKey, componentKey) {
1668
- return this.cache.disabledConditions[itemKey]?.components?.[componentKey]?.result;
1669
- }
1670
- getTemplateValue(templateValueKey) {
1671
- return this.cache.templateValues[templateValueKey]?.value;
1672
- }
1673
- getValidationValues(itemKey) {
1674
- const validations = this.cache.validations[itemKey];
1675
- if (!validations) return;
1676
- return Object.keys(validations).reduce((acc, validationKey) => {
1677
- acc[validationKey] = validations[validationKey].result;
1678
- return acc;
1679
- }, {});
1680
- }
1681
- initCache() {
1682
- this.cache = {
1683
- validations: {},
1684
- displayConditions: {},
1685
- templateValues: {},
1686
- disabledConditions: {}
1687
- };
1688
- this.surveyDef.getTemplateValueKeys().forEach((templateKey) => {
1689
- this.cache.templateValues[templateKey] = {
1690
- value: void 0,
1691
- templateDef: this.surveyDef.getTemplateValue(templateKey)
924
+ getExpression() {
925
+ return createFunctionExpression(this.name, this.args.map((arg) => arg.getExpression()), this.editorConfig);
926
+ }
927
+ };
928
+ const date_eq = (a, b) => new FunctionCallEditor("date_eq", [a, b], ValueType.boolean);
929
+ const date_gt = (a, b) => new FunctionCallEditor("date_gt", [a, b], ValueType.boolean);
930
+ const date_gte = (a, b) => new FunctionCallEditor("date_gte", [a, b], ValueType.boolean);
931
+ const date_lt = (a, b) => new FunctionCallEditor("date_lt", [a, b], ValueType.boolean);
932
+ const date_lte = (a, b) => new FunctionCallEditor("date_lte", [a, b], ValueType.boolean);
933
+ const date_add_days = (a, b) => new FunctionCallEditor("date_add_days", [a, b], ValueType.date);
934
+ const date_add_months = (a, b) => new FunctionCallEditor("date_add_months", [a, b], ValueType.date);
935
+ const date_add_years = (a, b) => new FunctionCallEditor("date_add_years", [a, b], ValueType.date);
936
+ /** Signed completed units in a - b, e.g. age as `date_diff(ctx_current_date(), birth, "years")`. */
937
+ const date_diff = (a, b, unit) => new FunctionCallEditor("date_diff", [
938
+ a,
939
+ b,
940
+ const_string(unit)
941
+ ], ValueType.number);
942
+ const date_min = (...args) => new FunctionCallEditor("date_min", args, ValueType.date);
943
+ const date_max = (...args) => new FunctionCallEditor("date_max", args, ValueType.date);
944
+ const is_disabled = (ownerId) => new DisabledStateEditor(ownerId);
945
+ //#endregion
946
+ //#region src/survey/composition/registry.ts
947
+ function defineElement(definition) {
948
+ const schema = contentOwner(z.strictObject({
949
+ ...elementShape,
950
+ elementType: z.literal(definition.elementType),
951
+ config: definition.config.nonoptional()
952
+ }), definition.roles ?? []);
953
+ return {
954
+ elementType: definition.elementType,
955
+ labelByLocale: definition.labelByLocale,
956
+ schema,
957
+ inspect(input) {
958
+ const node = schema.parse(input);
959
+ return {
960
+ slots: [],
961
+ bindings: [],
962
+ followUps: [],
963
+ fullWidth: false,
964
+ minWidthRem: 12,
965
+ componentParents: {},
966
+ ...definition.facts?.(node.config, node.id)
1692
967
  };
1693
- });
1694
- this.surveyDef.surveyItems.forEach((item, itemId) => {
1695
- if (item.validations && Object.keys(item.validations).length > 0) {
1696
- this.cache.validations[itemId] = {};
1697
- Object.keys(item.validations).forEach((validationKey) => {
1698
- const valExp = item.validations[validationKey];
1699
- if (!valExp) {
1700
- console.warn("initCache: validation expression not found: " + itemId + " " + validationKey);
1701
- return;
1702
- }
1703
- this.cache.validations[itemId][validationKey] = {
1704
- expression: valExp,
1705
- result: false
1706
- };
1707
- });
1708
- }
1709
- if (item.displayConditions !== void 0 && (item.displayConditions.root || item.displayConditions.components)) {
1710
- this.cache.displayConditions[itemId] = {};
1711
- if (item.displayConditions.root) this.cache.displayConditions[itemId].root = {
1712
- expression: item.displayConditions.root,
1713
- result: false
1714
- };
1715
- if (item.displayConditions.components) {
1716
- this.cache.displayConditions[itemId].components = {};
1717
- Object.keys(item.displayConditions.components).forEach((componentKey) => {
1718
- const compExp = item.displayConditions?.components?.[componentKey];
1719
- if (!compExp) {
1720
- console.warn("initCache: display condition component expression not found: " + itemId + " " + componentKey);
1721
- return;
1722
- }
1723
- this.cache.displayConditions[itemId].components[componentKey] = {
1724
- expression: compExp,
1725
- result: false
1726
- };
1727
- });
1728
- }
1729
- }
1730
- if (item.disabledConditions !== void 0 && item.disabledConditions.components !== void 0) {
1731
- this.cache.disabledConditions[itemId] = { components: {} };
1732
- Object.keys(item.disabledConditions.components).forEach((componentKey) => {
1733
- const compExp = item.disabledConditions?.components?.[componentKey];
1734
- if (!compExp) {
1735
- console.warn("initCache: disabled condition component expression not found: " + itemId + " " + componentKey);
1736
- return;
1737
- }
1738
- this.cache.disabledConditions[itemId].components[componentKey] = {
1739
- expression: compExp,
1740
- result: false
1741
- };
1742
- });
1743
- }
1744
- });
1745
- }
1746
- initResponseObject(items) {
1747
- const respGroup = {};
1748
- items.forEach((item, itemId) => {
1749
- if (!item.isInteractive) return;
1750
- else {
1751
- const prefill = this.prefills?.[itemId];
1752
- const applyPrefill = prefill && prefill.itemType === item.type;
1753
- respGroup[itemId] = new SurveyItemResponse({
1754
- itemId,
1755
- itemType: item.type
1756
- }, applyPrefill ? prefill.response : void 0);
1757
- }
1758
- });
1759
- return respGroup;
1760
- }
1761
- applyConfiguredPrefills() {
1762
- const evalEngine = new ExpressionEvaluator({
1763
- responses: this.responses,
1764
- surveyContext: this.context
1765
- });
1766
- const maxPasses = Math.max(this.surveyDef.surveyItems.size, 1);
1767
- for (let pass = 0; pass < maxPasses; pass++) {
1768
- this.evalTemplateValues(evalEngine);
1769
- let changedInPass = false;
1770
- this.surveyDef.surveyItems.forEach((item) => {
1771
- if (!item.isInteractive()) return;
1772
- changedInPass = this.applyConfiguredPrefillsForItem(item, evalEngine) || changedInPass;
1773
- });
1774
- if (!changedInPass) break;
1775
- }
1776
- this.evalTemplateValues(evalEngine);
1777
- }
1778
- applyConfiguredPrefillsForItem(item, evalEngine) {
1779
- const responseItem = this.responses[item.id];
1780
- if (!responseItem) return false;
1781
- let changed = false;
1782
- for (const prefill of item.prefills ?? []) {
1783
- const targetSlot = item.resolvePrefillTarget(prefill.target);
1784
- if (!targetSlot) continue;
1785
- if ((prefill.apply ?? SurveyItemPrefillApplyMode.ifEmpty) === SurveyItemPrefillApplyMode.ifEmpty && responseItem.response?.get(targetSlot.slotId) !== void 0) continue;
1786
- if (!this.shouldApplyConfiguredPrefill(prefill, evalEngine)) continue;
1787
- const resolvedValue = this.resolveConfiguredPrefillValue(prefill, targetSlot, evalEngine);
1788
- if (!resolvedValue) continue;
1789
- const normalizedValue = item.normalizePrefillValue(prefill, targetSlot, resolvedValue);
1790
- if (!normalizedValue) continue;
1791
- const nextResponse = responseItem.response?.clone() ?? new ResponseItem();
1792
- nextResponse.setSlotValue(targetSlot.slotId, normalizedValue);
1793
- responseItem.response = nextResponse.isEmpty() ? void 0 : nextResponse;
1794
- changed = true;
968
+ },
969
+ compute(input, slotId, read) {
970
+ return definition.compute?.(schema.parse(input).config, slotId, read);
971
+ },
972
+ validate(input, read, evaluate) {
973
+ const node = schema.parse(input);
974
+ return definition.validate?.(node.config, node.id, read, evaluate) ?? [];
1795
975
  }
1796
- return changed;
1797
- }
1798
- shouldApplyConfiguredPrefill(prefill, evalEngine) {
1799
- if (!prefill.when) return true;
1800
- const resolved = evalEngine.eval(prefill.when);
1801
- return resolved?.type === ValueType.boolean && resolved.value === true;
1802
- }
1803
- resolveConfiguredPrefillValue(prefill, _target, evalEngine) {
1804
- switch (prefill.source.type) {
1805
- case "static": return prefill.source.value;
1806
- case "expression": return evalEngine.eval(prefill.source.expression);
1807
- case "templateValue": return this.getTemplateValue(prefill.source.key);
1808
- case "previousResponse": {
1809
- if (!this.prefillEnvironment?.getPreviousResponse) return;
1810
- const refTarget = prefill.source.ref.target ?? prefill.target;
1811
- const resolved = this.prefillEnvironment.getPreviousResponse({
1812
- surveyKey: prefill.source.ref.surveyKey,
1813
- itemId: prefill.source.ref.itemId,
1814
- target: refTarget
1815
- });
1816
- if (!resolved) return;
1817
- return resolved;
1818
- }
1819
- default: return;
1820
- }
1821
- }
1822
- shouldRender(itemId, componentId) {
1823
- const displayConditionResult = this.getDisplayConditionValue(itemId, componentId);
1824
- if (displayConditionResult !== void 0) return displayConditionResult;
1825
- return true;
1826
- }
1827
- sequentialRender(groupDef) {
1828
- const newItems = [];
1829
- for (const itemId of groupDef.items || []) {
1830
- if (!this.shouldRender(itemId)) continue;
1831
- const itemDef = this.surveyDef.surveyItems.get(itemId);
1832
- if (!itemDef) {
1833
- console.warn("sequentialRender: item not found: " + itemId);
1834
- continue;
1835
- }
1836
- if (itemDef.type === "group") {
1837
- newItems.push(this.renderGroup(itemDef));
1838
- continue;
1839
- }
1840
- newItems.push({
1841
- id: itemId,
1842
- type: itemDef.type
1843
- });
976
+ };
977
+ }
978
+ /** A registry is finalized once. Its one recursive schema is shared by built-in and custom bodies. */
979
+ function createElementRegistry(create) {
980
+ const definitions = create(z.lazy(() => composition));
981
+ const byType = /* @__PURE__ */ new Map();
982
+ for (const definition of definitions) {
983
+ if (byType.has(definition.elementType)) throw new Error(`Duplicate element registration: ${definition.elementType}`);
984
+ byType.set(definition.elementType, definition);
985
+ }
986
+ const variants = definitions.map((definition) => definition.schema);
987
+ const element = variants.length ? z.union(variants) : z.never();
988
+ const composition = createCompositionSchema(element);
989
+ return {
990
+ definitions,
991
+ labels: new Map(definitions.map((definition) => [definition.elementType, definition.labelByLocale ?? {}])),
992
+ elementSchema: element,
993
+ bodySchema: composition,
994
+ surveySchema: createSurveySchema(composition),
995
+ get(elementType) {
996
+ const definition = byType.get(elementType);
997
+ if (!definition) throw new Error(`Unknown element type: ${elementType}`);
998
+ return definition;
1844
999
  }
1845
- return {
1846
- id: groupDef.id,
1847
- type: "group",
1848
- items: newItems
1849
- };
1000
+ };
1001
+ }
1002
+ //#endregion
1003
+ //#region src/survey/composition/semantics.ts
1004
+ /** Structural context is independent of an element's explicit accessible label. */
1005
+ function getElementSemanticContext(survey, elementId) {
1006
+ const owner = survey.owners.get(elementId);
1007
+ if (!owner) throw new Error(`Unknown element '${elementId}'`);
1008
+ let ancestor = owner.parentId ? survey.owners.get(owner.parentId) : void 0;
1009
+ while (ancestor && ancestor.followUpTriggerId === owner.followUpTriggerId) {
1010
+ if (ancestor.kind === "section" || ancestor.kind === "item") return ancestor;
1011
+ ancestor = ancestor.parentId ? survey.owners.get(ancestor.parentId) : void 0;
1850
1012
  }
1851
- randomizedItemRender(groupDef, parent) {
1852
- const newItems = parent.items?.filter((rItem) => this.shouldRender(rItem.id)) || [];
1853
- const itemIds = groupDef.items || [];
1854
- const shuffledIndices = shuffleIndices(itemIds.length);
1855
- for (const index of shuffledIndices) {
1856
- const itemId = itemIds[index];
1857
- if (parent.items?.find((rItem) => rItem.id === itemId)) continue;
1858
- if (!this.shouldRender(itemId)) continue;
1859
- const itemDef = this.surveyDef.surveyItems.get(itemId);
1860
- if (!itemDef) {
1861
- console.warn("randomizedItemRender: item not found: " + itemId);
1862
- continue;
1863
- }
1864
- if (itemDef.type === "group") {
1865
- newItems.push(this.renderGroup(itemDef, parent));
1866
- continue;
1867
- }
1868
- newItems.push({
1869
- id: itemId,
1870
- type: itemDef.type
1871
- });
1013
+ }
1014
+ function resolveElementName(survey, elementId, locales) {
1015
+ const owner = survey.owners.get(elementId);
1016
+ if (!owner) throw new Error(`Unknown element '${elementId}'`);
1017
+ const name = (ownerId, role) => {
1018
+ if (!survey.owners.get(ownerId)?.roles.some((entry) => entry.role === role)) return void 0;
1019
+ const resolved = resolveParticipantContent(survey, ownerId, role, locales);
1020
+ const text = getContentPlainText(resolved?.content).trim();
1021
+ return text && resolved ? {
1022
+ ownerId,
1023
+ role,
1024
+ text,
1025
+ content: resolved.content,
1026
+ locale: resolved.locale
1027
+ } : void 0;
1028
+ };
1029
+ const own = name(elementId, "label");
1030
+ if (own) return own;
1031
+ let ancestor = owner.parentId ? survey.owners.get(owner.parentId) : void 0;
1032
+ while (ancestor) {
1033
+ const current = ancestor;
1034
+ if ([...survey.elements.values()].filter((element) => {
1035
+ const candidate = survey.owners.get(element.node.id);
1036
+ return element.facts.bindings.length > 0 && candidate.followUpTriggerId === owner.followUpTriggerId && current.path.every((part, index) => candidate.path[index] === part);
1037
+ }).length === 1) {
1038
+ const inherited = name(ancestor.id, "title") ?? name(ancestor.id, "label");
1039
+ if (inherited) return inherited;
1872
1040
  }
1873
- return {
1874
- id: groupDef.id,
1875
- type: "group",
1876
- items: newItems
1877
- };
1878
- }
1879
- renderGroup(groupDef, parent) {
1880
- if (!parent) parent = {
1881
- id: groupDef.id,
1882
- type: "group",
1883
- items: []
1884
- };
1885
- if (groupDef.config.shuffleItems) return this.randomizedItemRender(groupDef, parent);
1886
- return this.sequentialRender(groupDef);
1041
+ if (ancestor.kind === "section" || ancestor.id === owner.followUpTriggerId) return void 0;
1042
+ ancestor = ancestor.parentId ? survey.owners.get(ancestor.parentId) : void 0;
1887
1043
  }
1888
- reRenderSurveyTree() {
1889
- if (!this.surveyDef.rootItem) throw new Error("reRenderSurveyTree: root item not found");
1890
- this.renderedSurveyTree = this.renderGroup(this.surveyDef.rootItem);
1891
- }
1892
- _registerSurveyEvent(event) {
1893
- this._events.push(event);
1894
- }
1895
- _getTimestamp() {
1896
- return Math.floor(Date.now() / 1e3);
1897
- }
1898
- getResponseItem(itemId) {
1899
- return this.responses[itemId];
1900
- }
1901
- evalExpressions() {
1902
- const evalEngine = new ExpressionEvaluator({
1903
- responses: this.responses,
1904
- surveyContext: this.context
1905
- });
1906
- this.evalTemplateValues(evalEngine);
1907
- this.evalDisplayConditions(evalEngine);
1908
- this.evalDisableConditions(evalEngine);
1909
- this.evalValidations(evalEngine);
1910
- }
1911
- evalTemplateValues(evalEngine) {
1912
- Object.keys(this.cache.templateValues).forEach((templateValueKey) => {
1913
- const templateValue = this.cache.templateValues[templateValueKey];
1914
- this.cache.templateValues[templateValueKey].value = void 0;
1915
- if (!templateValue.templateDef?.expression) {
1916
- console.warn("evalTemplateValues: template value expression not found: " + templateValueKey);
1917
- return;
1918
- }
1919
- let evaluatedValue = evalEngine.eval(templateValue.templateDef.expression);
1920
- if (evaluatedValue === void 0) {
1921
- console.warn("evalTemplateValues: template value expression returned undefined: " + templateValueKey);
1922
- return;
1923
- }
1924
- switch (templateValue.templateDef.type) {
1925
- case "date2string":
1926
- if (evaluatedValue.type !== ValueType.date) {
1927
- console.warn("evalTemplateValues: template value expression returned non-date value: " + templateValueKey);
1928
- return;
1929
- }
1930
- evaluatedValue = {
1931
- type: ValueType.string,
1932
- value: format(/* @__PURE__ */ new Date(evaluatedValue.value * 1e3), templateValue.templateDef.dateFormat)
1933
- };
1934
- break;
1935
- default: break;
1936
- }
1937
- this.cache.templateValues[templateValueKey].value = evaluatedValue;
1938
- });
1939
- }
1940
- evalDisplayConditions(evalEngine) {
1941
- Object.keys(this.cache.displayConditions).forEach((itemKey) => {
1942
- const displayCondition = this.cache.displayConditions[itemKey];
1943
- if (displayCondition.root) {
1944
- const resolvedValue = evalEngine.eval(displayCondition.root.expression);
1945
- if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
1946
- console.warn("evalDisplayConditions: display condition expression returned undefined: " + itemKey);
1947
- return;
1948
- }
1949
- this.cache.displayConditions[itemKey].root.result = resolvedValue.value;
1950
- }
1951
- if (displayCondition.components) Object.keys(displayCondition.components).forEach((componentKey) => {
1952
- const resolvedValue = evalEngine.eval(displayCondition.components[componentKey].expression);
1953
- if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
1954
- console.warn("evalDisplayConditions: display condition component expression returned undefined: " + itemKey + "." + componentKey);
1955
- return;
1956
- }
1957
- this.cache.displayConditions[itemKey].components[componentKey].result = resolvedValue.value;
1958
- });
1959
- });
1960
- }
1961
- evalDisableConditions(evalEngine) {
1962
- Object.keys(this.cache.disabledConditions).forEach((itemKey) => {
1963
- const disableCondition = this.cache.disabledConditions[itemKey];
1964
- if (disableCondition.components) Object.keys(disableCondition.components).forEach((componentKey) => {
1965
- const resolvedValue = evalEngine.eval(disableCondition.components[componentKey].expression);
1966
- if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
1967
- console.warn("evalDisableConditions: disable condition component expression returned undefined: " + itemKey + "." + componentKey);
1968
- return;
1969
- }
1970
- this.cache.disabledConditions[itemKey].components[componentKey].result = resolvedValue.value;
1971
- });
1972
- });
1973
- }
1974
- evalValidations(evalEngine) {
1975
- Object.keys(this.cache.validations).forEach((itemId) => {
1976
- const validation = this.cache.validations[itemId];
1977
- Object.keys(validation).forEach((validationKey) => {
1978
- const resolvedValue = evalEngine.eval(validation[validationKey].expression);
1979
- if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
1980
- console.warn("evalValidations: validation expression returned undefined: " + itemId + "." + validationKey);
1981
- return;
1982
- }
1983
- this.cache.validations[itemId][validationKey].result = resolvedValue.value;
1984
- });
1985
- });
1986
- }
1987
- };
1988
- const flattenTree = (itemTree) => {
1989
- const flatTree = new Array();
1990
- itemTree.items?.forEach((item) => {
1991
- if (item.type === "group") flatTree.push(...flattenTree(item));
1992
- else flatTree.push({ ...item });
1993
- });
1994
- return flatTree;
1995
- };
1044
+ }
1996
1045
  //#endregion
1997
- //#region src/json-patch.ts
1998
- var JsonPatchError = class extends Error {
1999
- constructor(message, code, path, operationIndex) {
2000
- super(message);
2001
- this.code = code;
2002
- this.path = path;
2003
- this.operationIndex = operationIndex;
2004
- this.name = "JsonPatchError";
2005
- }
2006
- };
2007
- const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
2008
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2009
- const assertValidPointerSegment = (segment, pointer) => {
2010
- if (/~(?![01])/.test(segment)) throw new JsonPatchError(`Invalid JSON pointer segment "${segment}" in "${pointer}". Only "~0" and "~1" escapes are allowed.`, "invalid-json-pointer", pointer);
2011
- };
2012
- const decodeJsonPointerSegment = (segment) => {
2013
- assertValidPointerSegment(segment, segment);
2014
- return segment.replace(/~1/g, "/").replace(/~0/g, "~");
2015
- };
2016
- const encodeJsonPointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
2017
- const parseJsonPointer = (pointer) => {
2018
- if (pointer === "") return [];
2019
- if (!pointer.startsWith("/")) throw new JsonPatchError(`Invalid JSON pointer "${pointer}". It must be empty or start with "/".`, "invalid-json-pointer", pointer);
2020
- return pointer.slice(1).split("/").map((segment) => {
2021
- assertValidPointerSegment(segment, pointer);
2022
- return segment.replace(/~1/g, "/").replace(/~0/g, "~");
1046
+ //#region src/engine/validation.ts
1047
+ function validateSurveyAnswers(plan, answers, context, options) {
1048
+ const snapshot = plan.evaluate({
1049
+ answers,
1050
+ context
2023
1051
  });
2024
- };
2025
- const parseArrayIndex = ({ segment, arrayLength, allowAppend, allowEnd, path }) => {
2026
- if (segment === "-") {
2027
- if (allowAppend) return arrayLength;
2028
- throw new JsonPatchError(`JSON pointer "${path}" uses "-" where an existing array index is required.`, "invalid-array-index", path);
2029
- }
2030
- if (!/^(0|[1-9][0-9]*)$/.test(segment)) throw new JsonPatchError(`JSON pointer "${path}" uses invalid array index "${segment}".`, "invalid-array-index", path);
2031
- const index = Number(segment);
2032
- const maximum = allowEnd ? arrayLength : arrayLength - 1;
2033
- if (index < 0 || index > maximum) throw new JsonPatchError(`JSON pointer "${path}" array index ${index} is out of bounds.`, "array-index-out-of-bounds", path);
2034
- return index;
2035
- };
2036
- const getValueAtSegments = (document, segments, path) => {
2037
- let current = document;
2038
- for (const segment of segments) {
2039
- if (Array.isArray(current)) {
2040
- const index = parseArrayIndex({
2041
- segment,
2042
- arrayLength: current.length,
2043
- allowAppend: false,
2044
- allowEnd: false,
2045
- path
2046
- });
2047
- current = current[index];
2048
- continue;
2049
- }
2050
- if (!isRecord(current) || !hasOwn(current, segment)) throw new JsonPatchError(`JSON pointer "${path}" does not exist.`, "path-not-found", path);
2051
- current = current[segment];
2052
- }
2053
- return current;
2054
- };
2055
- const getValueAtJsonPointer = (document, pointer) => {
2056
- return getValueAtSegments(document, parseJsonPointer(pointer), pointer);
2057
- };
2058
- const deepEqualJson = (left, right) => {
2059
- if (left === right) return true;
2060
- if (Array.isArray(left) || Array.isArray(right)) {
2061
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
2062
- return left.every((entry, index) => deepEqualJson(entry, right[index]));
1052
+ const evaluationDiagnostics = [];
1053
+ const eligible = (slotId) => {
1054
+ const binding = plan.bindings.get(slotId);
1055
+ return !!binding && hasPermittedChange(binding, snapshot);
1056
+ };
1057
+ const evaluate = (expression, path) => {
1058
+ try {
1059
+ return evaluateExpression(expression, {
1060
+ ...context,
1061
+ answer: snapshot.answer,
1062
+ disabled: snapshot.disabled
1063
+ });
1064
+ } catch (error) {
1065
+ if (!(error instanceof ExpressionEvaluationError)) throw error;
1066
+ evaluationDiagnostics.push({
1067
+ code: "validation-evaluation",
1068
+ severity: "warning",
1069
+ path,
1070
+ message: error.message
1071
+ });
1072
+ return;
1073
+ }
1074
+ };
1075
+ const issues = [];
1076
+ const authorWarnings = [];
1077
+ const active = (slotId) => {
1078
+ const binding = plan.bindings.get(slotId);
1079
+ return !!binding && [binding.controlId, ...binding.visibilityIds ?? []].every(snapshot.visible);
1080
+ };
1081
+ const included = (itemId) => !options.itemIds || options.itemIds.includes(itemId);
1082
+ for (const element of plan.document.elements.values()) {
1083
+ if (!included(element.itemId)) continue;
1084
+ for (const binding of element.facts.bindings) {
1085
+ if (!binding.selection || !active(binding.slotId) || [binding.controlId, ...binding.visibilityIds ?? []].some(snapshot.disabled)) continue;
1086
+ if (!hasPermittedChange(binding, snapshot)) authorWarnings.push({
1087
+ ownerId: binding.controlId,
1088
+ code: "non-interactive-choice",
1089
+ message: "Option locks, visibility, or the selection limit leave this choice with no permitted participant change. Its constraints will not block progress; its active answer is still submitted."
1090
+ });
1091
+ if (binding.selection.multiple) {
1092
+ const answer = snapshot.answer(binding.slotId);
1093
+ if (answer?.type === "reference[]") for (const optionId of answer.value.filter(snapshot.disabled)) authorWarnings.push({
1094
+ ownerId: optionId,
1095
+ code: "locked-checkbox-selection",
1096
+ message: "This selected checkbox is now locked and cannot be unticked. It remains an active, submitted answer. Use visibility if the answer should stop applying."
1097
+ });
1098
+ }
1099
+ }
1100
+ for (const issue of plan.document.registry.get(element.node.elementType).validate(element.node, snapshot.answer, (expression) => evaluate(expression, element.path))) if (eligible(issue.slotId)) issues.push(issue);
1101
+ else if (active(issue.slotId)) authorWarnings.push({
1102
+ ownerId: issue.messageOwnerId ?? plan.bindings.get(issue.slotId).controlId,
1103
+ code: "locked-input-constraint",
1104
+ message: `The ${issue.code} constraint is not satisfied, but this input currently permits no participant change. It will not block progress; its active answer is still submitted.`
1105
+ });
2063
1106
  }
2064
- if (isRecord(left) || isRecord(right)) {
2065
- if (!isRecord(left) || !isRecord(right)) return false;
2066
- const leftKeys = Object.keys(left).sort();
2067
- const rightKeys = Object.keys(right).sort();
2068
- if (leftKeys.length !== rightKeys.length) return false;
2069
- return leftKeys.every((key, index) => key === rightKeys[index] && deepEqualJson(left[key], right[key]));
1107
+ for (const item of plan.document.data.surveyItems) {
1108
+ if (item.itemType !== "content" || !included(item.id)) continue;
1109
+ for (const rule of item.config.validationRules ?? []) {
1110
+ if (rule.phase === "submit" && options.phase !== "submit") continue;
1111
+ const targets = rule.targets.filter(eligible);
1112
+ if (!rule.targets.filter(active).length) continue;
1113
+ if (rule.when) {
1114
+ const result = evaluate(rule.when, [...plan.document.owners.get(rule.id).path, "when"]);
1115
+ if (result?.type !== "boolean" || !result.value) continue;
1116
+ }
1117
+ const result = evaluate(rule.expression, [...plan.document.owners.get(rule.id).path, "expression"]);
1118
+ if (result?.type !== "boolean" || !result.value) {
1119
+ issues.push(...targets.map((slotId) => ({
1120
+ slotId,
1121
+ code: "rule",
1122
+ messageOwnerId: rule.id
1123
+ })));
1124
+ if (!targets.length) authorWarnings.push({
1125
+ ownerId: rule.id,
1126
+ code: "locked-rule-targets",
1127
+ message: "This applicable rule fails, but none of its active targets permits a participant change. It will not block progress. Check the rule and disabling conditions."
1128
+ });
1129
+ }
1130
+ }
2070
1131
  }
2071
- return false;
2072
- };
2073
- const applyAdd = (document, path, value) => {
2074
- const segments = parseJsonPointer(path);
2075
- if (segments.length === 0) return structuredCloneMethod(value);
2076
- const parentSegments = segments.slice(0, -1);
2077
- const key = segments[segments.length - 1];
2078
- const parent = getValueAtSegments(document, parentSegments, path);
2079
- if (Array.isArray(parent)) {
2080
- const index = parseArrayIndex({
2081
- segment: key,
2082
- arrayLength: parent.length,
2083
- allowAppend: true,
2084
- allowEnd: true,
2085
- path
1132
+ return {
1133
+ issues,
1134
+ diagnostics: [...snapshot.diagnostics, ...evaluationDiagnostics],
1135
+ authorWarnings
1136
+ };
1137
+ }
1138
+ const authoringDependencyPlans = /* @__PURE__ */ new WeakMap();
1139
+ const staticWarnings = /* @__PURE__ */ new WeakMap();
1140
+ /** Conservative proofs only: dynamic conditions and prefills are checked in a real preview. */
1141
+ function staticConstraintWarnings(survey, plan) {
1142
+ const literal = (expression, value) => expression?.type === "const" && expression.value?.type === "boolean" && expression.value.value === value;
1143
+ const parents = (id) => {
1144
+ const owner = survey.owners.get(id);
1145
+ return [...owner.parentId ? [owner.parentId] : [], ...owner.elementId ? ownValue(survey.elements.get(owner.elementId)?.facts.componentParents, id) ?? [] : []];
1146
+ };
1147
+ const visible = (id) => {
1148
+ const owner = survey.owners.get(id);
1149
+ return !owner.followUpTriggerId && (!owner.visibility || literal(owner.visibility, true)) && parents(id).every(visible);
1150
+ };
1151
+ const hidden = (id) => literal(survey.owners.get(id).visibility, false) || parents(id).some(hidden);
1152
+ const disabled = (id) => literal(survey.owners.get(id).disabled, true) || parents(id).some(disabled);
1153
+ const locked = (slotId) => {
1154
+ const binding = plan.bindings.get(slotId);
1155
+ if (!binding) return false;
1156
+ const controls = [binding.controlId, ...binding.visibilityIds ?? []];
1157
+ return controls.every(visible) && (controls.some(disabled) || !!binding.selection?.options.every((id) => hidden(id) || disabled(id)));
1158
+ };
1159
+ const warnings = [];
1160
+ for (const element of survey.elements.values()) {
1161
+ if (!element.facts.bindings.some((binding) => locked(binding.slotId))) continue;
1162
+ for (const issue of survey.registry.get(element.node.elementType).validate(element.node, () => void 0, () => void 0)) if ((issue.code === "required" || issue.code === "mustBeTrue") && locked(issue.slotId) && !ownValue(element.node.prefills, issue.slotId)?.length) warnings.push({
1163
+ ownerId: issue.messageOwnerId ?? plan.bindings.get(issue.slotId).controlId,
1164
+ code: "locked-input-constraint",
1165
+ message: `The ${issue.code} constraint starts unsatisfied on an input with no permitted participant change and no prefill. It will not block progress. Check the requirement and disabling conditions.`
2086
1166
  });
2087
- parent.splice(index, 0, structuredCloneMethod(value));
2088
- return document;
2089
1167
  }
2090
- if (!isRecord(parent)) throw new JsonPatchError(`JSON pointer "${path}" parent is not an object or array.`, "invalid-target-parent", path);
2091
- Object.defineProperty(parent, key, {
2092
- configurable: true,
2093
- enumerable: true,
2094
- value: structuredCloneMethod(value),
2095
- writable: true
1168
+ for (const item of survey.data.surveyItems) {
1169
+ if (item.itemType !== "content") continue;
1170
+ for (const rule of item.config.validationRules ?? []) {
1171
+ if (!literal(rule.expression, false) || rule.when && !literal(rule.when, true)) continue;
1172
+ if (rule.targets.length && rule.targets.every(locked)) warnings.push({
1173
+ ownerId: rule.id,
1174
+ code: "locked-rule-targets",
1175
+ message: "This rule always fails, but all its targets are locked. It will not block progress. Check the rule and disabling conditions."
1176
+ });
1177
+ }
1178
+ }
1179
+ return warnings;
1180
+ }
1181
+ function getAuthoringWarnings(survey, locale) {
1182
+ const warnings = [];
1183
+ const itemsWithElements = new Set([...survey.elements.values()].map((element) => element.itemId));
1184
+ for (const item of survey.data.surveyItems) if (item.itemType === "content" && !itemsWithElements.has(item.id)) warnings.push({
1185
+ ownerId: item.id,
1186
+ code: "empty-content-item",
1187
+ message: "Add an element to this empty item, or remove the item."
2096
1188
  });
2097
- return document;
2098
- };
2099
- const applyRemove = (document, path) => {
2100
- const segments = parseJsonPointer(path);
2101
- if (segments.length === 0) throw new JsonPatchError("Removing the document root is not supported.", "remove-root", path);
2102
- const parentSegments = segments.slice(0, -1);
2103
- const key = segments[segments.length - 1];
2104
- const parent = getValueAtSegments(document, parentSegments, path);
2105
- if (Array.isArray(parent)) {
2106
- const index = parseArrayIndex({
2107
- segment: key,
2108
- arrayLength: parent.length,
2109
- allowAppend: false,
2110
- allowEnd: false,
2111
- path
1189
+ for (const owner of survey.owners.values()) for (const role of owner.roles.filter((role) => role.recommended)) if (!hasLocalizationText(readLocalizationContent(survey, {
1190
+ scope: "content",
1191
+ ownerId: owner.id,
1192
+ role: role.role
1193
+ }, locale))) warnings.push({
1194
+ ownerId: owner.id,
1195
+ code: "missing-content",
1196
+ message: `Add ${role.role} to make this structure understandable`
1197
+ });
1198
+ let plan = authoringDependencyPlans.get(survey.elements);
1199
+ if (!plan) {
1200
+ plan = new SurveyStateGraph(survey, { admission: "draft" });
1201
+ authoringDependencyPlans.set(survey.elements, plan);
1202
+ }
1203
+ let conflicts = staticWarnings.get(survey.elements);
1204
+ if (!conflicts) {
1205
+ conflicts = staticConstraintWarnings(survey, plan);
1206
+ staticWarnings.set(survey.elements, conflicts);
1207
+ }
1208
+ warnings.push(...conflicts);
1209
+ for (const item of survey.data.surveyItems) {
1210
+ if (item.itemType !== "content") continue;
1211
+ for (const rule of item.config.validationRules ?? []) {
1212
+ if (rule.phase === "submit") continue;
1213
+ if ([rule.expression, ...rule.when ? [rule.when] : []].flatMap((expression) => [...plan.storedDependencies(expression)]).some((id) => {
1214
+ const dependencyItem = survey.slots.get(id)?.itemId;
1215
+ return dependencyItem && dependencyItem !== item.id && !isStaticallyBefore(survey, dependencyItem, item.id);
1216
+ })) warnings.push({
1217
+ ownerId: rule.id,
1218
+ code: "cross-item-page-rule",
1219
+ message: "This rule runs before leaving the page and reads answers from another item. If those answers are on a later page, the participant may be unable to continue. Consider submit-only validation or an explicit condition for unanswered dependencies."
1220
+ });
1221
+ }
1222
+ }
1223
+ for (const element of survey.elements.values()) for (const binding of element.facts.bindings) if (binding.labelRefs) {
1224
+ for (const ref of binding.labelRefs) if (!hasLocalizationText(readLocalizationContent(survey, {
1225
+ scope: "content",
1226
+ ...ref
1227
+ }, locale)) && !warnings.some((warning) => warning.ownerId === ref.ownerId && warning.code === "missing-content")) warnings.push({
1228
+ ownerId: ref.ownerId,
1229
+ code: "missing-accessible-name",
1230
+ message: `Add ${ref.role} to name this input in the selected language`
2112
1231
  });
2113
- const [removedValue] = parent.splice(index, 1);
1232
+ } else if (!resolveElementName(survey, element.node.id, [locale]) && !warnings.some((warning) => warning.ownerId === element.node.id && warning.code === "missing-accessible-name")) warnings.push({
1233
+ ownerId: element.node.id,
1234
+ code: "missing-accessible-name",
1235
+ message: "Add a label, or an unambiguous enclosing title, in the selected language"
1236
+ });
1237
+ for (const element of survey.elements.values()) for (const binding of element.facts.bindings) {
1238
+ if (!binding.selection?.multiple) continue;
1239
+ for (const id of binding.selection.options) {
1240
+ const condition = survey.owners.get(id)?.disabled;
1241
+ if (condition && [...plan.storedDependencies(condition)].some((slotId) => slotId !== binding.slotId)) warnings.push({
1242
+ ownerId: id,
1243
+ code: "external-checkbox-lock",
1244
+ message: "This condition depends on another answer. An already selected checkbox can become locked and will still be submitted. Use visibility if the selection should stop applying."
1245
+ });
1246
+ }
1247
+ }
1248
+ return warnings;
1249
+ }
1250
+ /** A shared shuffled ancestor only changes order between its distinct children. */
1251
+ function isStaticallyBefore(survey, earlierId, laterId) {
1252
+ const earlier = [...survey.getItemPath(earlierId), earlierId];
1253
+ const later = [...survey.getItemPath(laterId), laterId];
1254
+ const branch = earlier.findIndex((id, index) => id !== later[index]);
1255
+ if (branch < 1) return false;
1256
+ const parent = survey.getItem(earlier[branch - 1]);
1257
+ return parent?.itemType === "group" && !parent.config.shuffleItems && parent.config.items.indexOf(earlier[branch]) < parent.config.items.indexOf(later[branch]);
1258
+ }
1259
+ //#endregion
1260
+ //#region src/survey/composition/diagnostics.ts
1261
+ /** Observed author-only conflicts use the same validation result as the player and host. */
1262
+ function getSessionDiagnostics(session) {
1263
+ const result = session.validate("submit");
1264
+ return [...result.authorWarnings.map((warning) => {
1265
+ const owner = session.document.owners.get(warning.ownerId);
2114
1266
  return {
2115
- document,
2116
- removedValue
1267
+ ...warning,
1268
+ severity: "warning",
1269
+ path: owner?.path ?? [],
1270
+ ownerItemId: owner?.itemId
2117
1271
  };
2118
- }
2119
- if (!isRecord(parent) || !hasOwn(parent, key)) throw new JsonPatchError(`JSON pointer "${path}" does not exist.`, "path-not-found", path);
2120
- const removedValue = parent[key];
2121
- delete parent[key];
2122
- return {
2123
- document,
2124
- removedValue
2125
- };
2126
- };
2127
- const applyReplace = (document, path, value) => {
2128
- const segments = parseJsonPointer(path);
2129
- if (segments.length === 0) return structuredCloneMethod(value);
2130
- getValueAtSegments(document, segments, path);
2131
- applyRemove(document, path);
2132
- return applyAdd(document, path, value);
2133
- };
2134
- const assertMoveTargetIsNotDescendant = (from, path) => {
2135
- if (from === "" || path === from || !path.startsWith(`${from}/`)) return;
2136
- throw new JsonPatchError(`JSON Patch cannot move "${from}" into its own descendant "${path}".`, "move-into-descendant", path);
2137
- };
2138
- const applyJsonPatch = (document, operations) => {
2139
- let patchedDocument = structuredCloneMethod(document);
2140
- operations.forEach((operation, operationIndex) => {
1272
+ }), ...result.diagnostics.map((diagnostic) => {
1273
+ const owner = ownerAtPath(session.document, diagnostic.path);
1274
+ return {
1275
+ ...diagnostic,
1276
+ ownerId: owner?.id,
1277
+ ownerItemId: owner?.itemId
1278
+ };
1279
+ })];
1280
+ }
1281
+ function ownerAtPath(survey, path) {
1282
+ return [...survey.owners.values()].filter((owner) => owner.path.length <= path.length && owner.path.every((part, index) => part === path[index])).sort((a, b) => b.path.length - a.path.length)[0];
1283
+ }
1284
+ /**
1285
+ * Every dangling reference an editor draft admits: missing answers, state targets, options,
1286
+ * owners and non-flow items from the document index, plus comparisons with removed option
1287
+ * identities reported by the compiled graph. Always empty for a runnable survey.
1288
+ */
1289
+ function getBrokenReferences(survey) {
1290
+ return getDefinitionDiagnostics(survey).filter((diagnostic) => diagnostic.severity === "error" && diagnostic.kind === "reference");
1291
+ }
1292
+ const compiledDiagnostics = /* @__PURE__ */ new WeakMap();
1293
+ /** Use the authoritative definition/compiler diagnostics, including state reads and custom definitions. */
1294
+ function getDefinitionDiagnostics(survey, locale) {
1295
+ let compiled = compiledDiagnostics.get(survey.elements);
1296
+ if (!compiled) {
2141
1297
  try {
2142
- switch (operation.op) {
2143
- case "add":
2144
- patchedDocument = applyAdd(patchedDocument, operation.path, operation.value);
2145
- break;
2146
- case "remove":
2147
- patchedDocument = applyRemove(patchedDocument, operation.path).document;
2148
- break;
2149
- case "replace":
2150
- patchedDocument = applyReplace(patchedDocument, operation.path, operation.value);
2151
- break;
2152
- case "move": {
2153
- assertMoveTargetIsNotDescendant(operation.from, operation.path);
2154
- if (operation.from === operation.path) break;
2155
- const { document: withoutValue, removedValue } = applyRemove(patchedDocument, operation.from);
2156
- patchedDocument = applyAdd(withoutValue, operation.path, removedValue);
2157
- break;
2158
- }
2159
- case "copy": {
2160
- const value = getValueAtJsonPointer(patchedDocument, operation.from);
2161
- patchedDocument = applyAdd(patchedDocument, operation.path, value);
2162
- break;
2163
- }
2164
- case "test":
2165
- if (!deepEqualJson(getValueAtJsonPointer(patchedDocument, operation.path), operation.value)) throw new JsonPatchError(`JSON Patch test failed at "${operation.path}".`, "test-failed", operation.path);
2166
- break;
2167
- }
1298
+ compiled = new SurveyStateGraph(survey, { admission: "draft" }).admittedDiagnostics;
2168
1299
  } catch (error) {
2169
- if (error instanceof JsonPatchError) throw new JsonPatchError(error.message, error.code, error.path, operationIndex);
2170
- throw error;
1300
+ if (error instanceof DefinitionError) compiled = error.diagnostics;
1301
+ else throw error;
2171
1302
  }
1303
+ compiledDiagnostics.set(survey.elements, compiled);
1304
+ }
1305
+ const located = [...survey.diagnostics, ...compiled].map((diagnostic) => {
1306
+ const owner = ownerAtPath(survey, diagnostic.path) ?? (diagnostic.path[0] === "translations" && diagnostic.path[2] === "content" ? survey.owners.get(String(diagnostic.path[3])) : void 0);
1307
+ return {
1308
+ ...diagnostic,
1309
+ ownerItemId: owner?.itemId,
1310
+ ownerId: owner?.id
1311
+ };
2172
1312
  });
2173
- return patchedDocument;
2174
- };
2175
- //#endregion
2176
- //#region src/response-exporter/types.ts
2177
- /** Meta column keys in fixed order (common attributes first). */
2178
- const META_COLUMN_ORDER = [
2179
- "submittedAt",
2180
- "participantId",
2181
- "responseId",
2182
- "versionId"
2183
- ];
2184
- /** Built-in transform modes for slot export. */
2185
- const SlotTransformMode = {
2186
- default: "default",
2187
- mask: "mask",
2188
- dateFormat: "dateFormat"
2189
- };
2190
- /** Separator between item key path and slot coding path in default export headers. */
2191
- const EXPORT_COLUMN_SLOT_SEPARATOR = "__";
2192
- /** Source of an export column. */
2193
- const ExportColumnSource = {
2194
- meta: "meta",
2195
- context: "context",
2196
- response: "response"
2197
- };
1313
+ if (locale) try {
1314
+ for (const warning of getAuthoringWarnings(survey, locale)) {
1315
+ const owner = survey.owners.get(warning.ownerId);
1316
+ located.push({
1317
+ ...warning,
1318
+ severity: "warning",
1319
+ path: owner?.path ?? [],
1320
+ ownerItemId: owner?.itemId
1321
+ });
1322
+ }
1323
+ } catch (error) {
1324
+ if (!(error instanceof DefinitionError)) throw error;
1325
+ }
1326
+ return [...new Map(located.map((issue) => [JSON.stringify([
1327
+ issue.code,
1328
+ issue.path,
1329
+ issue.message
1330
+ ]), issue])).values()];
1331
+ }
2198
1332
  //#endregion
2199
- //#region src/response-exporter/slot-formatter.ts
2200
- /** Default transform config when no profile override. */
2201
- const DEFAULT_TRANSFORM = { mode: SlotTransformMode.default };
2202
- /**
2203
- * Export a single response slot to a string. Pure function for unit testing.
2204
- * Uses default formatting by ValueType unless overridden by transformConfig.
2205
- */
2206
- function exportSingleResponseSlot(params) {
2207
- const { value, valueType, transformConfig } = params;
2208
- if (value === void 0 || value === null) return "";
2209
- const mode = transformConfig.mode;
2210
- if (mode === SlotTransformMode.mask) return maskValue(value, transformConfig.maskChar ?? "***");
2211
- if (mode === SlotTransformMode.dateFormat && (valueType === ValueType.date || valueType === ValueType.dateArray)) return formatDateValue(value, transformConfig.dateFormat?.pattern ?? "yyyy-MM-dd");
2212
- return formatDefault(value);
1333
+ //#region src/engine/session.ts
1334
+ const sessionSnapshotSchema = z.strictObject({
1335
+ schemaVersion: z.literal(1),
1336
+ binding: z.strictObject({
1337
+ sessionId: z.string().min(1),
1338
+ definitionId: z.string().min(1)
1339
+ }),
1340
+ answers: stringRecord(z.string().min(1), responseValueSchema),
1341
+ initialization: z.literal("complete"),
1342
+ navigation: z.strictObject({
1343
+ pageIndex: z.number().int().nonnegative(),
1344
+ viewport: z.enum(["small", "large"])
1345
+ }),
1346
+ randomization: z.strictObject({ seed: z.string().min(1) }),
1347
+ context: z.strictObject({
1348
+ locale: z.string().min(1),
1349
+ /** Defaults to the participant-local day when a new session starts; kept on resume. */
1350
+ currentDate: calendarDaySchema.optional(),
1351
+ participantFlags: stringRecord(z.string(), z.string()).optional(),
1352
+ customValues: stringRecord(z.string(), responseValueSchema).optional()
1353
+ }),
1354
+ events: z.array(surveyEventSchema)
1355
+ });
1356
+ const eventTimestamp = () => Math.floor(Date.now() / 1e3);
1357
+ const comparableAnswer = (value) => JSON.stringify(value, Object.keys(value).sort());
1358
+ function freezeAnswer(value) {
1359
+ if (Array.isArray(value.value)) Object.freeze(value.value);
1360
+ return Object.freeze(value);
1361
+ }
1362
+ function sameRecord(left, right, equal) {
1363
+ const entries = Object.entries(left ?? {});
1364
+ return entries.length === Object.keys(right ?? {}).length && entries.every(([key, value]) => {
1365
+ const other = ownValue(right, key);
1366
+ return other !== void 0 && equal(value, other);
1367
+ });
2213
1368
  }
2214
- const BooleanExportValues = {
2215
- TRUE: "TRUE",
2216
- FALSE: "FALSE"
2217
- };
2218
- function formatDefault(value) {
2219
- switch (value.type) {
2220
- case ValueType.string: return String(value.value);
2221
- case ValueType.reference: return String(value.value);
2222
- case ValueType.boolean: return value.value ? BooleanExportValues.TRUE : BooleanExportValues.FALSE;
2223
- case ValueType.number: return value.precision === NumberPrecision.int ? String(Math.floor(value.value)) : String(value.value);
2224
- case ValueType.date: return (/* @__PURE__ */ new Date(value.value * 1e3)).toISOString();
2225
- case ValueType.duration: {
2226
- const d = value;
2227
- return `${d.precision === NumberPrecision.int ? Math.floor(d.value) : d.value} ${d.unit}`;
1369
+ /** Owns retained answers; participant adapters receive no privileged initialization write path. */
1370
+ var SurveySession = class SurveySession {
1371
+ currentDocument;
1372
+ state;
1373
+ stateGraph;
1374
+ revision = 0;
1375
+ presentationRevision = 0;
1376
+ itemPresentationRevisions = /* @__PURE__ */ new Map();
1377
+ initializationDiagnostics = [];
1378
+ listeners = /* @__PURE__ */ new Set();
1379
+ subscribe = (listener) => {
1380
+ this.listeners.add(listener);
1381
+ return () => {
1382
+ this.listeners.delete(listener);
1383
+ };
1384
+ };
1385
+ getRevision = () => this.revision;
1386
+ /** State/context changes notify every item; wording changes only notify their owning item. */
1387
+ getItemRevision = (itemId) => this.revision - this.presentationRevision + (this.itemPresentationRevisions.get(itemId) ?? 0);
1388
+ /** Prefer the current page, then the next surviving item, then the previous one. */
1389
+ pageAnchors(writtenItems = /* @__PURE__ */ new Set()) {
1390
+ const pages = this.pages();
1391
+ return [
1392
+ ...(pages[this.pageIndex] ?? []).filter((id) => writtenItems.has(id)),
1393
+ ...pages.slice(this.pageIndex).flat(),
1394
+ ...pages.slice(0, this.pageIndex).flat().reverse()
1395
+ ];
1396
+ }
1397
+ reconcilePageAndNotify(anchors) {
1398
+ const pages = this.pages();
1399
+ const pageByItem = new Map(pages.flatMap((items, index) => items.map((id) => [id, index])));
1400
+ const anchoredPage = anchors?.map((id) => pageByItem.get(id)).find((index) => index !== void 0);
1401
+ this.state.navigation.pageIndex = anchoredPage ?? Math.min(this.pageIndex, Math.max(0, pages.length - 1));
1402
+ this.revision++;
1403
+ this.listeners.forEach((listener) => listener());
1404
+ }
1405
+ constructor(currentDocument, state, admission = "strict") {
1406
+ this.currentDocument = currentDocument;
1407
+ this.state = state;
1408
+ this.stateGraph = new SurveyStateGraph(currentDocument, { admission });
1409
+ this.validateAnswers(state.answers);
1410
+ for (const value of Object.values(state.answers)) freezeAnswer(value);
1411
+ for (const value of Object.values(state.context.customValues ?? {})) freezeAnswer(value);
1412
+ }
1413
+ get document() {
1414
+ return this.currentDocument;
1415
+ }
1416
+ /** Wording-only authoring revisions share the compiler indexes and cannot change execution.
1417
+ * Keep the compiled graph, answers, navigation and initialization when updating a live preview.
1418
+ * A structural/configuration revision must create a new session instead.
1419
+ */
1420
+ updatePresentation(document) {
1421
+ if (document === this.currentDocument) return;
1422
+ if (document.elements !== this.currentDocument.elements) throw new Error("Only wording revisions can update a session presentation");
1423
+ const affected = /* @__PURE__ */ new Set();
1424
+ let sharedContentChanged = false;
1425
+ for (const locale of /* @__PURE__ */ new Set([...document.locales, ...this.currentDocument.locales])) {
1426
+ const before = this.currentDocument.data.translations?.[locale];
1427
+ const after = document.data.translations?.[locale];
1428
+ if (before === after) continue;
1429
+ sharedContentChanged ||= before?.navigationContent !== after?.navigationContent || before?.validationMessages !== after?.validationMessages;
1430
+ for (const owner of document.owners.values()) if (before?.content[owner.id] !== after?.content[owner.id]) affected.add(owner.itemId);
2228
1431
  }
2229
- case ValueType.stringArray: return value.value.join("; ");
2230
- case ValueType.referenceArray: return value.value.join("; ");
2231
- case ValueType.numberArray: {
2232
- const arr = value.value;
2233
- const prec = value.precision;
2234
- const fmt = (n) => prec === NumberPrecision.int ? Math.floor(n) : n;
2235
- return arr.map(fmt).join("; ");
1432
+ if (!sharedContentChanged) {
1433
+ this.presentationRevision++;
1434
+ for (const id of affected) this.itemPresentationRevisions.set(id, (this.itemPresentationRevisions.get(id) ?? 0) + 1);
2236
1435
  }
2237
- case ValueType.dateArray: return value.value.map((ts) => (/* @__PURE__ */ new Date(ts * 1e3)).toISOString()).join("; ");
2238
- case ValueType.durationArray: {
2239
- const d = value;
2240
- const prec = d.precision === NumberPrecision.int;
2241
- const fmt = (n) => prec ? Math.floor(n) : n;
2242
- return d.value.map((v) => `${fmt(v)} ${d.unit}`).join("; ");
1436
+ this.currentDocument = document;
1437
+ this.reconcilePageAndNotify();
1438
+ }
1439
+ static async create(document, input, sources = {}) {
1440
+ if (input.mode === "resumed") {
1441
+ const snapshot = sessionSnapshotSchema.parse(input.snapshot);
1442
+ if (snapshot.binding.sessionId !== input.binding.sessionId || snapshot.binding.definitionId !== input.binding.definitionId) throw new Error("Resume snapshot does not match the session and exact survey definition");
1443
+ const session = new SurveySession(document, snapshot);
1444
+ if (session.pageIndex >= Math.max(1, session.pages().length)) throw new Error("Resume page index is outside the survey");
1445
+ return session;
2243
1446
  }
2244
- default: return String(value.value ?? "");
1447
+ const session = new SurveySession(document, sessionSnapshotSchema.parse({
1448
+ schemaVersion: 1,
1449
+ binding: input.binding,
1450
+ answers: input.answers ?? {},
1451
+ initialization: "complete",
1452
+ navigation: {
1453
+ pageIndex: 0,
1454
+ viewport: "large"
1455
+ },
1456
+ randomization: { seed: input.seed ?? crypto.randomUUID() },
1457
+ context: {
1458
+ ...input.context,
1459
+ currentDate: input.context.currentDate ?? calendarDateFromLocalDate(/* @__PURE__ */ new Date())
1460
+ },
1461
+ events: [{
1462
+ type: "openSurvey",
1463
+ timestamp: eventTimestamp(),
1464
+ value: input.context.locale
1465
+ }]
1466
+ }), input.admission);
1467
+ await session.initialize(sources);
1468
+ return session;
1469
+ }
1470
+ snapshot() {
1471
+ return structuredClone(this.state);
1472
+ }
1473
+ itemMetadata() {
1474
+ return Object.fromEntries(this.pages().flat().map((itemId, position) => [itemId, { position }]));
1475
+ }
1476
+ active() {
1477
+ return this.stateGraph.evaluate({
1478
+ answers: this.state.answers,
1479
+ context: this.state.context
1480
+ });
2245
1481
  }
2246
- }
2247
- function formatDateValue(value, pattern) {
2248
- if (value.type === ValueType.date) return format(/* @__PURE__ */ new Date(value.value * 1e3), pattern);
2249
- if (value.type === ValueType.dateArray) return value.value.map((ts) => format(/* @__PURE__ */ new Date(ts * 1e3), pattern)).join("; ");
2250
- return formatDefault(value);
2251
- }
2252
- function maskValue(value, maskChar) {
2253
- return valueToString(value) ? maskChar : "";
2254
- }
2255
- function valueToString(value) {
2256
- switch (value.type) {
2257
- case ValueType.string:
2258
- case ValueType.reference: return String(value.value);
2259
- case ValueType.stringArray:
2260
- case ValueType.referenceArray: return value.value.join(" ");
2261
- default: return String(value.value ?? "");
1482
+ get locale() {
1483
+ return this.state.context.locale;
2262
1484
  }
2263
- }
1485
+ get pageIndex() {
1486
+ return this.state.navigation.pageIndex;
1487
+ }
1488
+ setLocale(locale) {
1489
+ this.setContext({
1490
+ ...this.state.context,
1491
+ locale
1492
+ });
1493
+ }
1494
+ /** Replace host context without restarting initialization or losing retained answers. */
1495
+ setContext(context) {
1496
+ const parsed = sessionSnapshotSchema.shape.context.parse(context);
1497
+ const previous = this.state.context;
1498
+ const next = {
1499
+ ...parsed,
1500
+ currentDate: parsed.currentDate ?? previous.currentDate
1501
+ };
1502
+ if (!next.locale.trim()) throw new Error("A locale is required");
1503
+ const localeChanged = next.locale !== previous.locale;
1504
+ if (!localeChanged && next.currentDate === previous.currentDate && sameRecord(previous.participantFlags, next.participantFlags, (a, b) => a === b) && sameRecord(previous.customValues, next.customValues, (a, b) => JSON.stringify(a) === JSON.stringify(b))) return;
1505
+ for (const value of Object.values(next.customValues ?? {})) freezeAnswer(value);
1506
+ const anchors = this.pageAnchors();
1507
+ this.state = {
1508
+ ...this.state,
1509
+ context: next,
1510
+ events: localeChanged ? [...this.state.events, {
1511
+ type: "languageChanged",
1512
+ timestamp: eventTimestamp(),
1513
+ value: next.locale
1514
+ }] : this.state.events
1515
+ };
1516
+ this.reconcilePageAndNotify(anchors);
1517
+ }
1518
+ orderedIds(ids, ownerId) {
1519
+ return shuffleArray(ids, createSeededRandom(`${this.state.randomization.seed}:${ownerId}`));
1520
+ }
1521
+ setViewport(viewport) {
1522
+ if (viewport === this.state.navigation.viewport) return;
1523
+ const anchors = this.pageAnchors();
1524
+ this.state.navigation.viewport = viewport;
1525
+ this.reconcilePageAndNotify(anchors);
1526
+ }
1527
+ pages(viewport = this.state.navigation.viewport) {
1528
+ const active = this.active(), byId = new Map(this.document.data.surveyItems.map((item) => [item.id, item]));
1529
+ const pages = [[]];
1530
+ const limit = this.document.data.maxItemsPerPage?.[viewport] ?? Infinity;
1531
+ const next = () => {
1532
+ if (pages.at(-1).length) pages.push([]);
1533
+ };
1534
+ const visit = (id) => {
1535
+ const item = byId.get(id);
1536
+ if (!item || !active.visible(id)) return;
1537
+ if (item.itemType === "group") (item.config.shuffleItems ? this.orderedIds(item.config.items, item.id) : item.config.items).forEach(visit);
1538
+ else if (item.itemType === "page-break") next();
1539
+ else {
1540
+ if (pages.at(-1).length >= limit) next();
1541
+ pages.at(-1).push(id);
1542
+ }
1543
+ };
1544
+ visit(this.document.rootId);
1545
+ return pages.filter((page) => page.length);
1546
+ }
1547
+ validate(phase, itemIds) {
1548
+ const result = validateSurveyAnswers(this.stateGraph, this.state.answers, this.state.context, {
1549
+ phase,
1550
+ itemIds
1551
+ });
1552
+ return {
1553
+ ...result,
1554
+ diagnostics: [...this.initializationDiagnostics, ...result.diagnostics]
1555
+ };
1556
+ }
1557
+ goToPage(index, viewport = this.state.navigation.viewport) {
1558
+ const pages = this.pages(viewport);
1559
+ if (!Number.isInteger(index) || index < 0 || index >= pages.length) throw new Error("Invalid page index");
1560
+ if (index > this.pageIndex) {
1561
+ if (this.validate("page", pages[this.pageIndex]).issues.length) throw new Error("Current page has blocking validation errors");
1562
+ }
1563
+ this.state = {
1564
+ ...this.state,
1565
+ navigation: {
1566
+ pageIndex: index,
1567
+ viewport
1568
+ },
1569
+ events: [...this.state.events, {
1570
+ type: "pageChanged",
1571
+ timestamp: eventTimestamp(),
1572
+ value: index
1573
+ }]
1574
+ };
1575
+ this.reconcilePageAndNotify();
1576
+ }
1577
+ submission(responseId) {
1578
+ if (this.validate("submit").issues.length) throw new Error("Survey has blocking validation errors");
1579
+ return structuredClone({
1580
+ schemaVersion: 2,
1581
+ responseId,
1582
+ definitionId: this.state.binding.definitionId,
1583
+ answers: this.active().activeAnswers(),
1584
+ events: this.state.events,
1585
+ itemMetadata: this.itemMetadata(),
1586
+ customValues: this.state.context.customValues,
1587
+ evaluationContext: {
1588
+ locale: this.state.context.locale,
1589
+ currentDate: this.state.context.currentDate,
1590
+ participantFlags: this.state.context.participantFlags
1591
+ },
1592
+ randomization: this.state.randomization
1593
+ });
1594
+ }
1595
+ validateAnswers(answers) {
1596
+ for (const [slotId, value] of Object.entries(answers)) {
1597
+ const slot = this.document.slots.get(slotId);
1598
+ if (!slot || slot.computed) throw new Error(`Unknown or computed answer target '${slotId}'`);
1599
+ const errors = validateResponseSlotValue(slot, value);
1600
+ if (errors.length) throw new Error(errors.join("; "));
1601
+ if (value.type === "reference[]" && new Set(value.value).size !== value.value.length) throw new Error(`Duplicate selected options in '${slotId}'`);
1602
+ }
1603
+ }
1604
+ write(writes) {
1605
+ if (new Set(writes.map((write) => write.slotId)).size !== writes.length) throw new Error("One action cannot write a slot twice");
1606
+ const before = this.active();
1607
+ const changed = writes.filter(({ slotId, value }) => {
1608
+ const previous = ownValue(this.state.answers, slotId);
1609
+ return previous === void 0 || value === void 0 ? previous !== value : comparableAnswer(previous) !== comparableAnswer(value);
1610
+ });
1611
+ for (const { slotId, value } of writes) {
1612
+ const slot = this.document.slots.get(slotId);
1613
+ const binding = this.stateGraph.bindings.get(slotId);
1614
+ if (!slot || slot.computed || !binding) throw new Error(`Slot '${slotId}' is not a writable input`);
1615
+ if (value !== void 0) this.validateAnswers({ [slotId]: value });
1616
+ }
1617
+ for (const { slotId, value } of changed) {
1618
+ const binding = this.stateGraph.bindings.get(slotId);
1619
+ if (!canWriteSelection(binding, ownValue(this.state.answers, slotId), value, before)) throw new Error(`Participant write to '${slotId}' is not permitted in the current state`);
1620
+ }
1621
+ if (!changed.length) return;
1622
+ const anchors = this.pageAnchors(new Set(changed.map(({ slotId }) => this.document.slots.get(slotId).itemId)));
1623
+ const answers = { ...this.state.answers };
1624
+ const timestamp = eventTimestamp();
1625
+ for (const { slotId, value } of changed) if (value === void 0) delete answers[slotId];
1626
+ else setOwnValue(answers, slotId, freezeAnswer(structuredClone(value)));
1627
+ const events = [...this.state.events];
1628
+ for (const { slotId, value } of changed) {
1629
+ const event = {
1630
+ type: "responseChanged",
1631
+ slotId,
1632
+ operation: value === void 0 ? "clear" : "set",
1633
+ timestamp
1634
+ };
1635
+ const previous = events.at(-1);
1636
+ if (previous?.type === event.type && previous.slotId === slotId && previous.operation === event.operation) events[events.length - 1] = event;
1637
+ else events.push(event);
1638
+ }
1639
+ this.state = {
1640
+ ...this.state,
1641
+ answers,
1642
+ events
1643
+ };
1644
+ this.reconcilePageAndNotify(anchors);
1645
+ }
1646
+ evaluate(expression) {
1647
+ const active = this.active();
1648
+ return evaluateExpression(expression, {
1649
+ ...this.state.context,
1650
+ answer: active.answer,
1651
+ disabled: active.disabled
1652
+ });
1653
+ }
1654
+ async initialize(sources) {
1655
+ const prefills = new Map([...this.document.elements.values()].flatMap((element) => Object.entries(element.node.prefills ?? {})));
1656
+ for (const slotId of this.stateGraph.initializationOrder) {
1657
+ if (Object.hasOwn(this.state.answers, slotId)) continue;
1658
+ for (const [index, candidate] of (prefills.get(slotId) ?? []).entries()) {
1659
+ const path = [
1660
+ ...this.document.elements.get(this.document.slots.get(slotId).elementId).path,
1661
+ "prefills",
1662
+ slotId,
1663
+ index
1664
+ ];
1665
+ const warn = (message) => this.initializationDiagnostics.push({
1666
+ code: "prefill-value",
1667
+ severity: "warning",
1668
+ path,
1669
+ message
1670
+ });
1671
+ const evaluate = (expression) => {
1672
+ try {
1673
+ return this.evaluate(expression);
1674
+ } catch (error) {
1675
+ if (!(error instanceof ExpressionEvaluationError)) throw error;
1676
+ warn(`Prefill '${candidate.id}' was skipped: ${error.message}`);
1677
+ return;
1678
+ }
1679
+ };
1680
+ if (candidate.when) {
1681
+ const result = evaluate(candidate.when);
1682
+ if (result?.type !== "boolean" || !result.value) continue;
1683
+ }
1684
+ let value;
1685
+ switch (candidate.source.type) {
1686
+ case "static":
1687
+ value = candidate.source.value;
1688
+ break;
1689
+ case "expression":
1690
+ value = evaluate(candidate.source.expression);
1691
+ break;
1692
+ case "templateValue": {
1693
+ const template = ownValue(this.document.data.templateValues, candidate.source.key);
1694
+ if (template?.expression) {
1695
+ value = evaluate(template.expression);
1696
+ if (template.type === "formatDate" && value?.type === "date") value = {
1697
+ type: "string",
1698
+ value: formatCalendarDate(value.value, template.dateFormat)
1699
+ };
1700
+ }
1701
+ break;
1702
+ }
1703
+ case "previousResponse":
1704
+ if (!sources.previousResponse && candidate.source.ifUnsupported !== "skip") throw new Error("This host does not supply previous responses for prefills");
1705
+ value = await sources.previousResponse?.(candidate.source.ref);
1706
+ }
1707
+ if (value === void 0) continue;
1708
+ if (candidate.source.type === "previousResponse") {
1709
+ const parsed = responseValueSchema.safeParse(value);
1710
+ const slot = this.document.slots.get(slotId);
1711
+ if (parsed.success && parsed.data.type === "reference[]" && slot.valueType === "reference[]" && slot.allowedValues) {
1712
+ const allowed = new Set(slot.allowedValues.flatMap(responseValueElements).flatMap((option) => option.type === "reference" ? [option.value] : []));
1713
+ const retained = parsed.data.value.filter((id) => allowed.has(id));
1714
+ if (retained.length !== parsed.data.value.length) {
1715
+ warn(`Prefill '${candidate.id}' discarded options outside the current response domain`);
1716
+ if (!retained.length) continue;
1717
+ value = {
1718
+ type: "reference[]",
1719
+ value: retained
1720
+ };
1721
+ }
1722
+ }
1723
+ }
1724
+ const issues = this.document.slots.validateAnswers({ [slotId]: value });
1725
+ if (issues.length) {
1726
+ warn(`Prefill '${candidate.id}' was skipped: ${issues.map((issue) => issue.message).join("; ")}`);
1727
+ continue;
1728
+ }
1729
+ setOwnValue(this.state.answers, slotId, freezeAnswer(structuredClone(value)));
1730
+ break;
1731
+ }
1732
+ }
1733
+ }
1734
+ };
2264
1735
  //#endregion
2265
1736
  //#region src/response-exporter/csv-serializer.ts
2266
1737
  const DEFAULT_OPTIONS = {
@@ -2296,182 +1767,229 @@ function serializeToCsv(columns, rows, options) {
2296
1767
  return lines.join(opts.lineEnding);
2297
1768
  }
2298
1769
  //#endregion
2299
- //#region src/response-exporter/exporter.ts
2300
- function slotOverrideKey(itemId, slotId) {
2301
- return `${itemId}::${slotId}`;
2302
- }
2303
- function responseColumnId(itemId, slotId) {
2304
- return `${itemId}::${slotId}`;
2305
- }
2306
- function metaColumnId(metaField) {
2307
- return `meta:${metaField}`;
2308
- }
2309
- function contextColumnId(contextKey) {
2310
- return `context:${contextKey}`;
2311
- }
2312
- function formatMetaValue(response, metaField) {
2313
- switch (metaField) {
2314
- case "submittedAt":
2315
- if (response.submittedAt == null || response.submittedAt === 0) return "";
2316
- return format(/* @__PURE__ */ new Date(response.submittedAt * 1e3), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx");
2317
- case "participantId": return response.participantId ?? "";
2318
- case "responseId": return response.key;
2319
- case "versionId": return response.versionId;
2320
- default: return "";
2321
- }
1770
+ //#region src/response-exporter/validate-configuration.ts
1771
+ /** Export requires an executable definition, including complete expression operands. */
1772
+ function validateExportConfiguration(survey, registry = survey.slots) {
1773
+ registry.assertValid();
1774
+ new SurveyStateGraph(survey);
2322
1775
  }
2323
- var SurveyResponseExporter = class {
2324
- versions = /* @__PURE__ */ new Map();
2325
- constructor(versionBindings, profiles = []) {
2326
- this.profiles = profiles;
2327
- for (const vb of versionBindings) this.versions.set(vb.versionId, vb);
2328
- }
2329
- getSurveyForVersion(versionId) {
2330
- return this.versions.get(versionId);
2331
- }
2332
- /**
2333
- * Compute response-only columns from survey definition.
2334
- */
2335
- computeResponseColumns(versionId) {
2336
- const binding = this.versions.get(versionId);
2337
- if (!binding) return [];
2338
- const survey = binding.survey;
2339
- const keyIndex = survey.keyIndex;
2340
- const keyByItemId = new Map(keyIndex.map((r) => [r.itemId, r]));
2341
- const profile = this.resolveProfileForVersion(versionId);
2342
- const columns = [];
2343
- for (const { itemId, slots } of survey.getResponseSlotDefinitions()) {
2344
- const keyInfo = keyByItemId.get(itemId);
2345
- const itemBaseKey = keyInfo ? [...keyInfo.keyPath, keyInfo.key].join(".") : itemId;
2346
- for (const slot of slots) {
2347
- const columnId = responseColumnId(itemId, slot.slotId);
2348
- const codingPath = [...slot.keyPath, slot.key].filter((part) => part.trim().length > 0);
2349
- const defaultHeader = codingPath.length > 0 ? `${itemBaseKey}__${codingPath.join(".")}` : `${itemBaseKey}__${slot.slotId}`;
2350
- const header = this.resolveColumnHeader(profile, columnId, defaultHeader);
2351
- columns.push({
2352
- id: columnId,
2353
- header,
2354
- source: ExportColumnSource.response,
2355
- itemId,
2356
- slotId: slot.slotId,
2357
- valueType: slot.valueType
2358
- });
1776
+ //#endregion
1777
+ //#region src/response-exporter/exporter.ts
1778
+ const canonical = (value) => JSON.stringify(value, (_key, entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b))) : entry);
1779
+ /** Export only against schemas selected by the host, never against response version tags. */
1780
+ var SurveyResponseExporter = class SurveyResponseExporter {
1781
+ batches;
1782
+ options;
1783
+ constructor(batches, options = {}) {
1784
+ this.batches = batches;
1785
+ this.options = options;
1786
+ if (!batches.length) throw new Error("Supply at least one schema-bound export batch");
1787
+ if (batches.some((batch) => !batch.schema || !Array.isArray(batch.responses))) throw new Error("Each export batch requires a schema and responses array");
1788
+ if (batches.length > 1 && !options.codingPolicy) throw new Error("Combined export requires an explicit original or harmonized coding policy");
1789
+ }
1790
+ batch(index) {
1791
+ const batch = this.batches[index];
1792
+ if (!batch) throw new Error(`Unknown export batch: ${index}`);
1793
+ return batch;
1794
+ }
1795
+ profile(batch) {
1796
+ if (!this.options.profile) return batch.profile;
1797
+ return {
1798
+ ...batch.profile,
1799
+ ...this.options.profile,
1800
+ slotOverrides: {
1801
+ ...batch.profile?.slotOverrides,
1802
+ ...this.options.profile.slotOverrides
1803
+ },
1804
+ columnHeaders: {
1805
+ ...batch.profile?.columnHeaders,
1806
+ ...this.options.profile.columnHeaders
1807
+ },
1808
+ contextOverrides: {
1809
+ ...batch.profile?.contextOverrides,
1810
+ ...this.options.profile.contextOverrides
2359
1811
  }
2360
- }
2361
- return columns;
1812
+ };
2362
1813
  }
2363
- /**
2364
- * Compute full export columns: meta first, then context (from responses), then response columns.
2365
- * Column order: meta (fixed) -> context (profile order or alphabetical) -> response (profile order + alphabetical).
2366
- */
2367
- computeColumns(versionId, responses, versionIds) {
2368
- const vids = versionIds ?? [versionId];
2369
- const responseCols = [];
2370
- const seen = /* @__PURE__ */ new Set();
2371
- for (const vid of vids) for (const c of this.computeResponseColumns(vid)) if (!seen.has(c.id)) {
2372
- seen.add(c.id);
2373
- responseCols.push(c);
2374
- }
2375
- const profile = this.resolveProfileForVersion(versionId);
2376
- const metaColumns = META_COLUMN_ORDER.map((metaField) => ({
2377
- id: metaColumnId(metaField),
2378
- header: this.resolveColumnHeader(profile, metaColumnId(metaField), metaField),
2379
- source: ExportColumnSource.meta,
2380
- metaField
2381
- }));
2382
- const contextKeys = /* @__PURE__ */ new Set();
2383
- for (const r of responses ?? []) if (r.context) for (const k of Object.keys(r.context)) contextKeys.add(k);
2384
- const contextOrder = profile?.contextColumnOrder;
2385
- const contextCols = Array.from(contextKeys).sort(contextOrder ? (a, b) => {
2386
- const ia = contextOrder.indexOf(a);
2387
- const ib = contextOrder.indexOf(b);
2388
- if (ia >= 0 && ib >= 0) return ia - ib;
2389
- if (ia >= 0) return -1;
2390
- if (ib >= 0) return 1;
2391
- return a.localeCompare(b);
2392
- } : void 0).map((contextKey) => ({
2393
- id: contextColumnId(contextKey),
2394
- header: this.resolveColumnHeader(profile, contextColumnId(contextKey), `context.${contextKey}`),
2395
- source: ExportColumnSource.context,
2396
- contextKey
1814
+ compileContexts() {
1815
+ return new Map(this.batches.map((batch) => {
1816
+ const registry = batch.schema.slots;
1817
+ validateExportConfiguration(batch.schema, registry);
1818
+ const slots = new Map(localizedExportSlots(batch.schema).map((slot) => [slot.slotId, slot]));
1819
+ const profile = this.profile(batch);
1820
+ const transforms = new Map([...slots.values()].map((slot) => [slot.slotId, {
1821
+ ...DEFAULT_TRANSFORM,
1822
+ ...slot.exportSettings,
1823
+ ...ownValue(profile?.slotOverrides, slot.slotId)
1824
+ }]));
1825
+ const columns = [...slots.values()].flatMap((slot) => projectSlot(slot, transforms.get(slot.slotId)).map((column) => ({
1826
+ ...column,
1827
+ header: ownValue(profile?.columnHeaders, column.id)?.trim() ?? column.header
1828
+ })));
1829
+ const encodings = /* @__PURE__ */ new Map();
1830
+ for (const slotId of new Set(columns.map((column) => column.slotId))) {
1831
+ const { include: _include, ...encoding } = resolvedSlotEncoding(slots.get(slotId), transforms.get(slotId));
1832
+ encodings.set(slotId, canonical(encoding));
1833
+ }
1834
+ return [batch, {
1835
+ registry,
1836
+ slots,
1837
+ profile,
1838
+ transforms,
1839
+ encodings,
1840
+ columns,
1841
+ included: new Set(columns.map((column) => column.id)),
1842
+ stateGraph: new SurveyStateGraph(batch.schema)
1843
+ }];
2397
1844
  }));
2398
- const respOrder = profile?.responseColumnOrder ?? [];
2399
- const byOrder = respOrder.filter((id) => responseCols.some((c) => c.id === id)).map((id) => responseCols.find((c) => c.id === id));
2400
- const remaining = responseCols.filter((c) => !respOrder.includes(c.id)).sort((a, b) => a.header.localeCompare(b.header));
2401
- const respCols = [...byOrder, ...remaining];
2402
- const columns = [
2403
- ...metaColumns,
2404
- ...contextCols,
2405
- ...respCols
2406
- ];
2407
- this.assertUniqueHeaders(columns);
2408
- return columns;
2409
- }
2410
- resolveProfileForVersion(versionId) {
2411
- return this.profiles.find((p) => !p.versionId || p.versionId === versionId);
2412
- }
2413
- resolveColumnHeader(profile, columnId, defaultHeader) {
2414
- const override = profile?.columnHeaders?.[columnId]?.trim();
2415
- return override && override.length > 0 ? override : defaultHeader;
2416
- }
2417
- assertUniqueHeaders(columns) {
2418
- const seen = /* @__PURE__ */ new Map();
2419
- for (const column of columns) {
2420
- const existingId = seen.get(column.header);
2421
- if (existingId && existingId !== column.id) throw new Error(`Duplicate export column header '${column.header}' for columns '${existingId}' and '${column.id}'`);
2422
- seen.set(column.header, column.id);
2423
- }
2424
1845
  }
2425
- getTransformConfig(versionId, itemId, slotId) {
2426
- return this.resolveProfileForVersion(versionId)?.slotOverrides?.[slotOverrideKey(itemId, slotId)] ?? DEFAULT_TRANSFORM;
2427
- }
2428
- getContextTransformConfig(versionId, contextKey) {
2429
- return this.resolveProfileForVersion(versionId)?.contextOverrides?.[contextKey] ?? DEFAULT_TRANSFORM;
2430
- }
2431
- exportResponse(response) {
2432
- if (!this.versions.get(response.versionId)) return null;
2433
- const columns = this.computeColumns(response.versionId, [response]);
2434
- const row = {};
2435
- for (const col of columns) {
2436
- if (col.source === ExportColumnSource.meta && col.metaField) {
2437
- row[col.header] = formatMetaValue(response, col.metaField);
2438
- continue;
2439
- }
2440
- if (col.source === ExportColumnSource.context && col.contextKey) {
2441
- const value = response.context?.[col.contextKey];
2442
- const transformConfig = this.getContextTransformConfig(response.versionId, col.contextKey);
2443
- const valueType = value?.type ?? ValueType.string;
2444
- row[col.header] = exportSingleResponseSlot({
2445
- value,
2446
- valueType,
2447
- transformConfig
1846
+ validateProfile(profile, batches, contexts, extraResponses = []) {
1847
+ if (!profile) return;
1848
+ if (profile.schemaVersion !== 1) throw new Error(`Unsupported exporter profile schema version: ${String(profile.schemaVersion)}`);
1849
+ const slots = new Map(batches.flatMap((batch) => [...contexts.get(batch).slots]));
1850
+ const contextKeys = new Set([...batches.flatMap((batch) => batch.responses), ...extraResponses].flatMap((response) => Object.keys(response.customValues ?? {})));
1851
+ const columns = /* @__PURE__ */ new Set([
1852
+ ...[...slots.values()].flatMap((slot) => projectSlot(slot, { mode: "default" }, true).map((column) => column.id)),
1853
+ ...META_COLUMN_ORDER.map((field) => encodeExportColumnId(["meta", field])),
1854
+ ...[...contextKeys].map((key) => encodeExportColumnId(["context", key]))
1855
+ ]);
1856
+ for (const id of Object.keys(profile.slotOverrides ?? {})) if (!slots.has(id)) throw new Error(`Profile references unknown response slot "${id}"`);
1857
+ for (const id of [...Object.keys(profile.columnHeaders ?? {}), ...profile.responseColumnOrder ?? []]) if (!columns.has(id)) throw new Error(`Profile references unknown export column "${id}"`);
1858
+ for (const id of profile.responseColumnOrder ?? []) if (![...slots.values()].some((slot) => projectSlot(slot, { mode: "default" }, true).some((column) => column.id === id))) throw new Error(`Response column order contains a non-response column: ${id}`);
1859
+ for (const key of [...Object.keys(profile.contextOverrides ?? {}), ...profile.contextColumnOrder ?? []]) if (!contextKeys.has(key)) throw new Error(`Profile references unknown context key "${key}"`);
1860
+ for (const slot of slots.values()) if ((slot.naming?.kind === "matrix" || slot.rowId || slot.columnId) && projectSlot(slot, { mode: "default" }, true).some((column) => Object.hasOwn(profile.columnHeaders ?? {}, column.id))) throw new Error(`Matrix variable "${slot.slotId}" cannot have an individual header override; edit its matrix coding components`);
1861
+ for (const [id, header] of Object.entries(profile.columnHeaders ?? {})) if (typeof header !== "string" || !header.trim()) throw new Error(`Export header for "${id}" cannot be empty`);
1862
+ if (new Set(profile.responseColumnOrder).size !== (profile.responseColumnOrder?.length ?? 0)) throw new Error("Response column order contains duplicate identities");
1863
+ }
1864
+ computeResponseColumns(batchIndex = 0, extraResponses = []) {
1865
+ const batch = this.batch(batchIndex);
1866
+ const contexts = this.compileContexts();
1867
+ this.validateProfile(batch.profile, [batch], contexts, extraResponses);
1868
+ this.validateProfile(this.options.profile, this.batches, contexts, extraResponses);
1869
+ return contexts.get(batch).columns;
1870
+ }
1871
+ computeColumns(extraResponses = []) {
1872
+ return this.columns(this.compileContexts(), extraResponses);
1873
+ }
1874
+ columns(contexts, extraResponses = []) {
1875
+ this.validateProfile(this.options.profile, this.batches, contexts, extraResponses);
1876
+ const columns = /* @__PURE__ */ new Map();
1877
+ const signatures = /* @__PURE__ */ new Map();
1878
+ const encodings = /* @__PURE__ */ new Map();
1879
+ this.batches.forEach((batch) => {
1880
+ const context = contexts.get(batch);
1881
+ this.validateProfile(batch.profile, [batch], contexts, extraResponses);
1882
+ for (const slot of context.slots.values()) {
1883
+ const signature = canonical({
1884
+ type: slot.valueType,
1885
+ allowedValues: slot.allowedValues?.map((value) => canonical(value)).sort(),
1886
+ codes: this.options.codingPolicy === "harmonized" ? ownValue(context.profile?.slotOverrides, slot.slotId)?.optionCodes ?? slot.referenceValueKeys : slot.referenceValueKeys,
1887
+ computed: slot.computed,
1888
+ scores: slot.referenceValueScores
2448
1889
  });
2449
- continue;
1890
+ const previous = signatures.get(slot.slotId);
1891
+ if (previous && previous !== signature) throw new Error(`Incompatible schema definitions for slot "${slot.slotId}"; supply a compatible explicit harmonization mapping`);
1892
+ signatures.set(slot.slotId, signature);
2450
1893
  }
2451
- if (col.source === ExportColumnSource.response && col.itemId != null && col.slotId != null && col.valueType != null) {
2452
- const value = response.responses.get(col.itemId)?.response?.get(col.slotId);
2453
- const transformConfig = this.getTransformConfig(response.versionId, col.itemId, col.slotId);
2454
- row[col.header] = exportSingleResponseSlot({
2455
- value,
2456
- valueType: col.valueType,
2457
- transformConfig
2458
- });
1894
+ for (const column of context.columns) {
1895
+ const signature = context.encodings.get(column.slotId);
1896
+ if (encodings.has(column.id) && encodings.get(column.id) !== signature) throw new Error(`Incompatible export encodings for column "${column.id}"; supply one harmonized profile`);
1897
+ encodings.set(column.id, signature);
1898
+ const existing = columns.get(column.id);
1899
+ if (existing && existing.header !== column.header) throw new Error(`Conflicting names for column "${column.id}"; select an explicit harmonized output header`);
1900
+ columns.set(column.id, column);
1901
+ }
1902
+ });
1903
+ const profile = this.options.profile ?? (this.batches.length === 1 ? this.batches[0].profile : void 0);
1904
+ const allResponses = [...this.batches.flatMap((batch) => batch.responses), ...extraResponses];
1905
+ const contextKeys = [...new Set(allResponses.flatMap((response) => Object.keys(response.customValues ?? {})))];
1906
+ const ordered = (values, order, id, name) => values.sort((a, b) => {
1907
+ const left = order.indexOf(id(a)), right = order.indexOf(id(b));
1908
+ if (left >= 0 || right >= 0) return (left < 0 ? Infinity : left) - (right < 0 ? Infinity : right);
1909
+ return name(a).localeCompare(name(b));
1910
+ });
1911
+ const result = [
1912
+ ...META_COLUMN_ORDER.map((field) => ({
1913
+ id: encodeExportColumnId(["meta", field]),
1914
+ header: profile?.columnHeaders?.[encodeExportColumnId(["meta", field])]?.trim() ?? field,
1915
+ source: ExportColumnSource.meta,
1916
+ metaField: field
1917
+ })),
1918
+ ...ordered(contextKeys, profile?.contextColumnOrder ?? [], (value) => value, (value) => value).map((key) => ({
1919
+ id: encodeExportColumnId(["context", key]),
1920
+ header: profile?.columnHeaders?.[encodeExportColumnId(["context", key])]?.trim() ?? `context.${key}`,
1921
+ source: ExportColumnSource.context,
1922
+ contextKey: key
1923
+ })),
1924
+ ...ordered([...columns.values()], profile?.responseColumnOrder ?? [], (value) => value.id, (value) => value.header)
1925
+ ];
1926
+ const headers = /* @__PURE__ */ new Set();
1927
+ for (const column of result) {
1928
+ if (headers.has(column.header)) throw new Error(`Duplicate export column header '${column.header}'`);
1929
+ headers.add(column.header);
1930
+ }
1931
+ return result;
1932
+ }
1933
+ row(response, batch, columns, context) {
1934
+ const { registry, slots, profile, included, transforms } = context;
1935
+ const diagnostics = registry.validateAnswers(response.answers);
1936
+ if (diagnostics.length) throw new Error(diagnostics.map((issue) => issue.message).join("\n"));
1937
+ const row = Object.create(null);
1938
+ const active = context.stateGraph.evaluate({
1939
+ answers: response.answers,
1940
+ context: {
1941
+ locale: response.evaluationContext?.locale ?? batch.schema.locales[0] ?? "en",
1942
+ currentDate: response.evaluationContext?.currentDate,
1943
+ participantFlags: response.evaluationContext?.participantFlags,
1944
+ customValues: response.customValues
2459
1945
  }
1946
+ });
1947
+ for (const column of columns) if (column.source === ExportColumnSource.meta) switch (column.metaField) {
1948
+ case "responseId":
1949
+ row[column.header] = response.responseId;
1950
+ break;
1951
+ case "participantId":
1952
+ row[column.header] = response.participantId ?? "";
1953
+ break;
1954
+ case "sourceId":
1955
+ row[column.header] = batch.sourceId ?? "";
1956
+ break;
1957
+ case "submittedAt": row[column.header] = response.submittedAt ? format(/* @__PURE__ */ new Date(response.submittedAt * 1e3), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx") : "";
1958
+ }
1959
+ else if (column.source === ExportColumnSource.context) {
1960
+ const value = ownValue(response.customValues, column.contextKey);
1961
+ row[column.header] = exportSingleResponseSlot({
1962
+ value,
1963
+ valueType: value?.type ?? ValueType.string,
1964
+ transformConfig: ownValue(profile?.contextOverrides, column.contextKey) ?? {
1965
+ ...DEFAULT_TRANSFORM,
1966
+ categorical: "ids"
1967
+ }
1968
+ });
1969
+ } else {
1970
+ const slot = slots.get(column.slotId);
1971
+ row[column.header] = slot && included.has(column.id) ? formatProjectedValue(slot, column, active.answer(slot.slotId), transforms.get(slot.slotId)) : "";
2460
1972
  }
2461
1973
  return row;
2462
1974
  }
2463
- /**
2464
- * Compute columns for a batch of responses (union of response columns across versions).
2465
- */
2466
- computeColumnsForBatch(responses) {
2467
- if (responses.length === 0) return [];
2468
- const versionIds = [...new Set(responses.map((r) => r.versionId))];
2469
- const primaryVersionId = versionIds[0];
2470
- return this.computeColumns(primaryVersionId, responses, versionIds);
1975
+ /** Explicitly associate one response with a supplied batch schema. */
1976
+ exportResponse(response, batchIndex = 0) {
1977
+ return new SurveyResponseExporter([{
1978
+ ...this.batch(batchIndex),
1979
+ responses: [response]
1980
+ }], { profile: this.options.profile }).exportRows()[0];
1981
+ }
1982
+ exportRows() {
1983
+ const contexts = this.compileContexts();
1984
+ return this.rows(this.columns(contexts), contexts);
1985
+ }
1986
+ rows(columns, contexts) {
1987
+ return this.batches.flatMap((batch) => batch.responses.map((response) => this.row(response, batch, columns, contexts.get(batch))));
2471
1988
  }
2472
- exportResponsesToCsv(responses, options) {
2473
- if (responses.length === 0) return "";
2474
- return serializeToCsv(this.computeColumnsForBatch(responses), responses.map((r) => this.exportResponse(r)).filter((r) => r !== null), options);
1989
+ exportResponsesToCsv(options) {
1990
+ const contexts = this.compileContexts();
1991
+ const columns = this.columns(contexts);
1992
+ return serializeToCsv(columns, this.rows(columns, contexts), options);
2475
1993
  }
2476
1994
  };
2477
1995
  //#endregion
@@ -2480,51 +1998,123 @@ var SurveyResponseExporter = class {
2480
1998
  * Generate a structured codebook from a survey definition.
2481
1999
  */
2482
2000
  function generateCodebook(survey, options) {
2001
+ const registry = survey.slots;
2002
+ validateExportConfiguration(survey, registry);
2003
+ const slots = localizedExportSlots(survey);
2483
2004
  const locales = options?.locales ?? survey.locales;
2484
- const keyIndex = survey.keyIndex;
2485
- const keyByItemId = new Map(keyIndex.map((r) => [r.itemId, r]));
2486
- const slotDefsByItemId = new Map(survey.getResponseSlotDefinitions().map(({ itemId, slots }) => [itemId, slots.map((slot) => ({
2487
- slotId: slot.slotId,
2488
- key: slot.key,
2489
- keyPath: [...slot.keyPath],
2490
- valueType: slot.valueType
2491
- }))]));
2005
+ const slotDefsByItemId = /* @__PURE__ */ new Map();
2006
+ for (const slot of slots) {
2007
+ const entries = slotDefsByItemId.get(slot.itemId) ?? [];
2008
+ entries.push({
2009
+ slotId: slot.slotId,
2010
+ elementId: slot.elementId,
2011
+ ...slot.rowId ? { rowId: slot.rowId } : {},
2012
+ ...slot.columnId ? { columnId: slot.columnId } : {},
2013
+ variableName: slot.variableName,
2014
+ valueType: slot.valueType,
2015
+ encoding: resolvedSlotEncoding(slot, {
2016
+ mode: "default",
2017
+ ...slot.exportSettings,
2018
+ ...ownValue(options?.profile?.slotOverrides, slot.slotId)
2019
+ }),
2020
+ optionLabels: slot.referenceValueLabels,
2021
+ ...slot.allowedValues ? { allowedValues: slot.allowedValues.map((value) => structuredClone(value)) } : {},
2022
+ ...slot.computed ? { computed: {
2023
+ ...slot.computed,
2024
+ sourceSlotIds: [...slot.computed.sourceSlotIds]
2025
+ } } : {},
2026
+ ...slot.referenceValueKeys ? { referenceValueKeys: { ...slot.referenceValueKeys } } : {},
2027
+ ...slot.referenceValueScores ? { referenceValueScores: { ...slot.referenceValueScores } } : {}
2028
+ });
2029
+ slotDefsByItemId.set(slot.itemId, entries);
2030
+ }
2492
2031
  const items = [];
2493
- for (const item of survey.surveyItems.values()) {
2494
- const keyInfo = keyByItemId.get(item.id);
2495
- const key = keyInfo?.key ?? item.id;
2496
- const keyPath = keyInfo?.keyPath ?? [];
2032
+ for (const item of survey.data.surveyItems) {
2497
2033
  const slotList = slotDefsByItemId.get(item.id) ?? [];
2498
2034
  slotList.sort((a, b) => a.slotId.localeCompare(b.slotId));
2499
2035
  let contentByLocale;
2500
2036
  if (locales.length > 0) {
2501
2037
  contentByLocale = {};
2502
- const itemTranslations = survey.getItemTranslations(item.id);
2503
- if (itemTranslations) for (const locale of locales) {
2504
- const all = itemTranslations.getAllForLocale(locale);
2505
- if (all) {
2506
- const flat = {};
2507
- for (const [contentKey, content] of Object.entries(all)) flat[contentKey] = getContentPlainText(content);
2508
- contentByLocale[locale] = flat;
2038
+ for (const locale of locales) {
2039
+ const values = {};
2040
+ for (const owner of survey.owners.values()) if (owner.itemId === item.id) {
2041
+ const roles = ownValue(ownValue(survey.data.translations, locale)?.content, owner.id);
2042
+ if (roles) setOwnValue(values, owner.id, Object.fromEntries(Object.entries(roles).map(([role, content]) => [role, getContentPlainText(content)])));
2509
2043
  }
2044
+ setOwnValue(contentByLocale, locale, values);
2510
2045
  }
2511
2046
  }
2512
2047
  items.push({
2513
2048
  id: item.id,
2514
- key,
2515
- keyPath,
2516
- type: item.type,
2517
- interactive: item.isInteractive(),
2049
+ editorName: survey.resolveItemEditorName(item.id, locales[0]),
2050
+ breadcrumb: survey.getItemDisplayPath(item.id, locales[0]).breadcrumb,
2051
+ type: item.itemType,
2052
+ interactive: slotList.some((slot) => !slot.computed),
2518
2053
  slots: slotList,
2519
2054
  contentByLocale
2520
2055
  });
2521
2056
  }
2522
2057
  return {
2523
- surveyKey: survey.surveyKey,
2524
- items: items.sort((a, b) => a.key.localeCompare(b.key))
2058
+ surveyKey: survey.data.metadata?.surveyKey,
2059
+ items,
2060
+ projection: new SurveyResponseExporter([{
2061
+ schema: survey,
2062
+ responses: [],
2063
+ profile: options?.profile
2064
+ }]).computeColumns(),
2065
+ profile: options?.profile && structuredClone(options.profile),
2066
+ missingValue: ""
2067
+ };
2068
+ }
2069
+ /** Describe precisely the combined projection and each host-selected source definition. */
2070
+ function generateExportCodebook(batches, options = {}) {
2071
+ const exporter = new SurveyResponseExporter(batches, options);
2072
+ return {
2073
+ schemaVersion: 1,
2074
+ codingPolicy: options.codingPolicy ?? "original",
2075
+ columns: exporter.computeColumns(),
2076
+ missingValue: "",
2077
+ profile: options.profile && structuredClone(options.profile),
2078
+ sources: batches.map((batch) => ({
2079
+ sourceId: batch.sourceId,
2080
+ surveySchemaVersion: batch.schema.serialize().schemaVersion,
2081
+ profile: batch.profile && structuredClone(batch.profile),
2082
+ variables: localizedExportSlots(batch.schema).map((slot) => ({
2083
+ ...slot,
2084
+ encoding: resolvedSlotEncoding(slot, {
2085
+ mode: "default",
2086
+ ...slot.exportSettings,
2087
+ ...ownValue(options.profile?.slotOverrides, slot.slotId) ?? ownValue(batch.profile?.slotOverrides, slot.slotId)
2088
+ })
2089
+ }))
2090
+ }))
2091
+ };
2092
+ }
2093
+ //#endregion
2094
+ //#region src/engine/submission.ts
2095
+ /** The host chooses the exact definition and trusted context before calling this boundary. */
2096
+ function validateSurveySubmission(survey, input, context, binding) {
2097
+ const response = SurveyResponse.deserialize(input);
2098
+ if (response.definitionId !== binding.definitionId) throw new Error(`Response was given against definition '${response.definitionId}', not '${binding.definitionId}'`);
2099
+ const issues = survey.slots.validateAnswers(response.answers);
2100
+ if (issues.length) throw new Error(issues.map((issue) => issue.message).join("\n"));
2101
+ const plan = new SurveyStateGraph(survey);
2102
+ const applicable = plan.evaluate({
2103
+ answers: response.answers,
2104
+ context
2105
+ }).activeAnswers();
2106
+ const validation = validateSurveyAnswers(plan, applicable, context, { phase: "submit" });
2107
+ if (validation.issues.length) throw new Error(validation.issues.map((issue) => `${issue.slotId}: ${issue.code}`).join("\n"));
2108
+ response.answers = structuredClone(applicable);
2109
+ response.evaluationContext = {
2110
+ locale: context.locale,
2111
+ currentDate: context.currentDate,
2112
+ participantFlags: context.participantFlags && { ...context.participantFlags }
2525
2113
  };
2114
+ response.customValues = context.customValues && structuredClone(context.customValues);
2115
+ return response;
2526
2116
  }
2527
2117
  //#endregion
2528
- export { AndExpressionEditor, CURRENT_SURVEY_SCHEMA, ConstBooleanEditor, ConstDateArrayEditor, ConstDateEditor, ConstExpression, ConstNumberArrayEditor, ConstNumberEditor, ConstStringArrayEditor, ConstStringEditor, ContentType, ContextVariableExpression, ContextVariableType, CtxCustomExpressionEditor, CtxCustomValueEditor, CtxLocaleEditor, CtxPFlagDateEditor, CtxPFlagIsDefinedEditor, CtxPFlagNumEditor, CtxPFlagStringEditor, DEFAULT_TRANSFORM, DurationUnits, EXPORT_COLUMN_SLOT_SEPARATOR, EqExpressionEditor, Expression, ExpressionEditor, ExpressionEvaluator, ExpressionType, FunctionExpression, FunctionExpressionNames, GroupItemCore, GtExpressionEditor, GteExpressionEditor, InRangeExpressionEditor, JsonPatchError, LtExpressionEditor, LteExpressionEditor, META_COLUMN_ORDER, MaxExpressionEditor, MinExpressionEditor, NumberPrecision, OrExpressionEditor, PageBreakItemCore, ReferenceUsageType, ReservedSurveyItemTypes, ResponseItem, ResponseVariableEditor, ResponseVariableExpression, SURVEY_EDITOR_ITEM_COLORS, SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY, SURVEY_RESPONSE_SCHEMA_VERSION, SlotTransformMode, StrEqExpressionEditor, StrListContainsExpressionEditor, SumExpressionEditor, Survey, SurveyEngineCore, SurveyEventTypes, SurveyItemCore, SurveyItemKey, SurveyItemPrefillApplyMode, SurveyItemPrefillTargetType, SurveyItemResponse, SurveyItemTranslations, SurveyResponse, SurveyResponseExporter, SurveyTranslations, TemplateDefTypes, ValueReference, ValueReferenceMethod, ValueType, analyzeSurveyExpressionReferences, and, applyJsonPatch, assertResponseValue, buildItemExpression, builtInItemCoreRegistry, collectSurveyExpressionLocations, const_boolean, const_date, const_date_array, const_number, const_number_array, const_string, const_string_array, createFullRegistry, createItemCore, createItemTypeDefinitionRegistry, createRichTextContent, createSeededRandom, ctx_custom_expression, ctx_custom_value, ctx_locale, ctx_pflag_date, ctx_pflag_is_defined, ctx_pflag_num, ctx_pflag_string, decodeJsonPointerSegment, deserializeSurveyItemPrefill, deserializeTemplateValue, deserializeTemplateValues, encodeJsonPointerSegment, eq, escapeCsvCell, exportSingleResponseSlot, expressionReferenceDiagnosticKey, findIntroducedBlockingExpressionReferenceDiagnostics, findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories, flattenTree, generateCodebook, generateCodingKey, generateId, getAssetUsagesFromContent, getContentPlainText, getItemExpressionDefinition, getPlainTextFromRichTextContent, getValueAtJsonPointer, gt, gte, hasRenderableRichTextBlock, hasRenderableRichTextContent, in_range, initValueForType, isBlockingSurveyExpressionReferenceDiagnostic, isBuiltInItemType, isContentEmpty, isLegacyItemGroupComponent, isLegacySurveyGroupItem, isResponseValue, lt, lte, max, min, or, parseJsonPointer, prefillTargetsEqual, replaceResponseVariableReferences, responseValueReferenceIds, response_boolean, response_date, response_date_array, response_number, response_number_array, response_string, response_string_array, serializeSurveyItemPrefill, serializeTemplateValue, serializeTemplateValues, serializeToCsv, shuffleArray, shuffleIndices, str_eq, str_list_contains, structuredCloneMethod, sum, toItemTypeDefinitionRegistry, validateLocale };
2118
+ export { AndExpressionEditor, BooleanExportValues, CALENDAR_DATE_UNITS, CURRENT_SURVEY_SCHEMA, CURRENT_SURVEY_SCHEMA_VERSION, ConstBooleanEditor, ConstDateArrayEditor, ConstDateEditor, ConstNumberArrayEditor, ConstNumberEditor, ConstStringArrayEditor, ConstStringEditor, ContentType, CtxCurrentDateEditor, CtxCustomValueEditor, CtxLocaleEditor, CtxPFlagDateEditor, CtxPFlagIsDefinedEditor, CtxPFlagNumEditor, CtxPFlagStringEditor, DEFAULT_TRANSFORM, DefinitionError, DisabledStateEditor, DurationUnits, EXPORTER_PROFILE_SCHEMA_VERSION, EqExpressionEditor, ExportColumnSource, ExpressionEditor, ExpressionEvaluationError, FUNCTION_SIGNATURES, GtExpressionEditor, GteExpressionEditor, InRangeExpressionEditor, JsonPatchError, LtExpressionEditor, LteExpressionEditor, META_COLUMN_ORDER, MaxExpressionEditor, MinExpressionEditor, NumberPrecision, OrExpressionEditor, RESPONSE_VARIABLE_NAME_MAX_LENGTH, ResponseSlotRegistry, ResponseVariableEditor, SURVEY_LOCALIZATION_SURFACES, SURVEY_RESPONSE_SCHEMA_VERSION, SlotTransformMode, StrEqExpressionEditor, StrListContainsExpressionEditor, SumExpressionEditor, Survey, SurveyEventTypes, SurveyResponse, SurveyResponseExporter, SurveySession, SurveyStateGraph, ValueType, addCalendarDate, allocateResponseVariableName, and, annotated, annotatedReferences, applyJsonPatch, assertResponseValue, boundField, calendarDateBounds, calendarDateFromLocalDate, calendarDatePrecision, calendarDateToLocalDate, calendarDaySchema, canWriteSelection, checkDocumentBudget, collectExpressionDependencies, compareCalendarDates, conditionsShape, const_boolean, const_date, const_date_array, const_number, const_number_array, const_string, const_string_array, contentOwner, contentOwnerIds, createCompositionSchema, createElementRegistry, createFlowItemSchema, createFunctionExpression, createRichTextContent, createSeededRandom, createSurveySchema, ctx_current_date, ctx_custom_value, ctx_locale, ctx_pflag_date, ctx_pflag_is_defined, ctx_pflag_num, ctx_pflag_string, date_add_days, date_add_months, date_add_years, date_diff, date_eq, date_gt, date_gte, date_lt, date_lte, date_max, date_min, decodeExportColumnId, decodeJsonPointerSegment, defineElement, differenceCalendarDates, elementShape, encodeExportColumnId, encodeJsonPointerSegment, eq, escapeCsvCell, evaluateExpression, exportSingleResponseSlot, expressionArguments, expressionEditorConfigSchema, expressionField, expressionIssues, expressionSchema, expressionTypeIssues, extendObject, followUpBody, formatCalendarDate, formatProjectedValue, functionNameSchema, functionTypeIssues, generateCodebook, generateCodingKey, generateExportCodebook, generateId, getAssetUsagesFromContent, getAuthoringWarnings, getBrokenReferences, getCompositionLabel, getContentPlainText, getDefaultProjectionDiagnostics, getDefaultProjectionIssues, getDefinitionDiagnostics, getElementSemanticContext, getLocalizationCellKey, getLocalizationCoverage, getLocalizationEntryKey, getLocalizationItems, getPlainTextFromRichTextContent, getSessionDiagnostics, getValueAtJsonPointer, gt, gte, hasLocalizationText, hasPermittedChange, hasRenderableRichTextBlock, hasRenderableRichTextContent, if_else, in_range, inferExpressionReturnType, inspectAnnotated, invalidResponseDomainValues, isAdmissibleInDraft, isCalendarDate, isCalendarDateUnit, isContentEmpty, isEmptySurveyLocalizationEntry, isResponseValue, isValidResponseVariableName, is_disabled, itemEditorMetadataSchema, itemResponseMetadataSchema, jsonValueSchema, layoutPolicySchema, listLocalizationEntries, list_count, localizedExportSlots, lt, lte, max, min, moveArrayElement, operand, optionIds, optionMapping, or, ownValue, ownedId, ownerAtPath, parseJsonPointer, projectSlot, readLocalizationContent, referenceId, remapAnnotated, remapExpression, resolveElementName, resolveParticipantContent, resolveUniqueItemKey, resolvedSlotEncoding, responseValueElements, responseValueSchema, responseValuesEqual, response_boolean, response_date, response_date_array, response_number, response_number_array, response_string, response_string_array, richTextImageDimensionSchema, richTextImageSourceSchema, schemaAnnotation, selectedAndEnabled, serializeToCsv, sessionSnapshotSchema, setOwnValue, shuffleArray, shuffleIndices, slotPrefillSchema, slotTransformSchema, str_eq, str_list_contains, strictKeyedObject, stringRecord, structuredCloneMethod, suggestResponseVariableName, sum, surveyContentSchema, surveyEventSchema, surveyLocales, surveyResponseSchema, swapArrayElements, templateValueSchema, transformAnnotated, transformExpression, unreachable, validateLocalizationRef, validateResponseSlotValue, validateSurveyAnswers, validateSurveySubmission, validationRuleSchema, valueTypeSchema, visitExpression };
2529
2119
 
2530
2120
  //# sourceMappingURL=index.mjs.map