@case-framework/survey-core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2344 @@
1
+ import { $ as inspectAnnotated, B as ValueType, Ct as calendarDaySchema, Dt as isCalendarDate, Et as formatCalendarDate, G as stringRecord, H as isResponseValue, L as surveyContentSchema, O as itemEditorMetadataSchema, Ot as isCalendarDateUnit, R as DurationUnits, Tt as differenceCalendarDates, U as ownValue, W as setOwnValue, _t as CALENDAR_DATE_UNITS, b as CURRENT_SURVEY_SCHEMA, ct as responseValueSchema, d as validateLocalizationRef, gt as visitExpression, h as getContentPlainText, ht as unreachable, l as readLocalizationContent, m as getAssetUsagesFromContent, mt as transformExpression, ot as expressionSchema, pt as operand, q as annotatedReferences, t as SURVEY_LOCALIZATION_SURFACES, ut as collectExpressionDependencies, vt as addCalendarDate, wt as compareCalendarDates, z as NumberPrecision } from "./localization-1lzNpyFX.mjs";
2
+ import { z } from "zod";
3
+ //#region src/utils.ts
4
+ function hashString(seed) {
5
+ let hash = 2166136261;
6
+ for (let i = 0; i < seed.length; i++) {
7
+ hash ^= seed.charCodeAt(i);
8
+ hash = Math.imul(hash, 16777619);
9
+ }
10
+ return hash >>> 0;
11
+ }
12
+ function createSeededRandom(seed) {
13
+ let state = hashString(seed);
14
+ return () => {
15
+ state = state + 1831565813 >>> 0;
16
+ let next = state;
17
+ next = Math.imul(next ^ next >>> 15, next | 1);
18
+ next ^= next + Math.imul(next ^ next >>> 7, next | 61);
19
+ return ((next ^ next >>> 14) >>> 0) / 4294967296;
20
+ };
21
+ }
22
+ function shuffleArray(values, random = Math.random) {
23
+ const shuffledValues = [...values];
24
+ for (let i = shuffledValues.length - 1; i > 0; i--) {
25
+ const j = Math.floor(random() * (i + 1));
26
+ [shuffledValues[i], shuffledValues[j]] = [shuffledValues[j], shuffledValues[i]];
27
+ }
28
+ return shuffledValues;
29
+ }
30
+ /**
31
+ * Shuffles an array of indices using the Fisher-Yates shuffle algorithm
32
+ * @param length - The length of the array to create indices for
33
+ * @returns A shuffled array of indices from 0 to length-1
34
+ */
35
+ function shuffleIndices(length, random = Math.random) {
36
+ return shuffleArray(Array.from({ length }, (_, i) => i), random);
37
+ }
38
+ function structuredCloneMethod(obj) {
39
+ if (obj === void 0) return obj;
40
+ if (typeof structuredClone !== "undefined") return structuredClone(obj);
41
+ return JSON.parse(JSON.stringify(obj));
42
+ }
43
+ const ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
44
+ function generateId() {
45
+ const length = 10;
46
+ const timestamp = Date.now();
47
+ let encodedTimestamp = "";
48
+ let t = timestamp;
49
+ const base = 62;
50
+ while (t > 0) {
51
+ encodedTimestamp = ID_ALPHABET.charAt(t % base) + encodedTimestamp;
52
+ t = Math.floor(t / base);
53
+ }
54
+ let randomPart = "";
55
+ for (let i = 0; i < length; i++) randomPart += ID_ALPHABET.charAt(Math.floor(Math.random() * base));
56
+ return randomPart + encodedTimestamp;
57
+ }
58
+ /** Alphanumeric chars without ambiguous ones (0/O, 1/I) for human-readable coding keys. */
59
+ const CODING_KEY_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
60
+ /**
61
+ * Generates a short random coding key suitable for item keys in exports/codebooks.
62
+ * @param length - Key length (default 4)
63
+ * @returns A random string of the specified length.
64
+ */
65
+ function generateCodingKey(length = 4) {
66
+ const base = 32;
67
+ let key = "";
68
+ for (let i = 0; i < length; i++) key += CODING_KEY_ALPHABET.charAt(Math.floor(Math.random() * base));
69
+ return key;
70
+ }
71
+ /** Resolve a sibling key using the editor's stable numeric suffix convention. */
72
+ function resolveUniqueItemKey(key, siblingKeys) {
73
+ let candidate = key;
74
+ let suffix = 1;
75
+ while (siblingKeys.has(candidate)) candidate = `${key}_${suffix++}`;
76
+ return candidate;
77
+ }
78
+ //#endregion
79
+ //#region src/expressions/function-signatures.ts
80
+ const comparison = {
81
+ min: 2,
82
+ max: 2,
83
+ required: [0, 1],
84
+ returns: "boolean"
85
+ };
86
+ const numericAggregate = {
87
+ min: 1,
88
+ required: [],
89
+ variadicGaps: true,
90
+ returns: "number"
91
+ };
92
+ const dateAdd = {
93
+ min: 2,
94
+ max: 2,
95
+ required: [0, 1],
96
+ returns: "date"
97
+ };
98
+ /** The exhaustive signature table for every serialized function name. */
99
+ const FUNCTION_SIGNATURES = {
100
+ and: {
101
+ min: 0,
102
+ required: [],
103
+ variadicGaps: true,
104
+ returns: "boolean"
105
+ },
106
+ or: {
107
+ min: 0,
108
+ required: [],
109
+ variadicGaps: true,
110
+ returns: "boolean"
111
+ },
112
+ not: {
113
+ min: 1,
114
+ max: 1,
115
+ required: [0],
116
+ returns: "boolean"
117
+ },
118
+ list_contains: comparison,
119
+ list_count: {
120
+ min: 1,
121
+ max: 2,
122
+ required: [0],
123
+ returns: "number"
124
+ },
125
+ if: {
126
+ min: 2,
127
+ max: 3,
128
+ required: [0, 1],
129
+ returns: { fromArguments: [1, 2] }
130
+ },
131
+ eq: comparison,
132
+ str_eq: comparison,
133
+ gt: comparison,
134
+ gte: comparison,
135
+ lt: comparison,
136
+ lte: comparison,
137
+ in_range: {
138
+ min: 4,
139
+ max: 4,
140
+ required: [
141
+ 0,
142
+ 1,
143
+ 2
144
+ ],
145
+ returns: "boolean"
146
+ },
147
+ sum: numericAggregate,
148
+ min: numericAggregate,
149
+ max: numericAggregate,
150
+ date_eq: comparison,
151
+ date_gt: comparison,
152
+ date_gte: comparison,
153
+ date_lt: comparison,
154
+ date_lte: comparison,
155
+ date_add_days: dateAdd,
156
+ date_add_months: dateAdd,
157
+ date_add_years: dateAdd,
158
+ date_diff: {
159
+ min: 3,
160
+ max: 3,
161
+ required: [
162
+ 0,
163
+ 1,
164
+ 2
165
+ ],
166
+ returns: "number"
167
+ },
168
+ date_min: {
169
+ min: 1,
170
+ required: [0],
171
+ returns: "date"
172
+ },
173
+ date_max: {
174
+ min: 1,
175
+ required: [0],
176
+ returns: "date"
177
+ }
178
+ };
179
+ /** Return a diagnostic message when a function has too few or too many arguments. */
180
+ function arityIssue(expression) {
181
+ const signature = FUNCTION_SIGNATURES[expression.functionName];
182
+ if (expression.arguments.length < signature.min || signature.max !== void 0 && expression.arguments.length > signature.max) return `${expression.functionName} expects ${signature.max === signature.min ? signature.min : signature.max === void 0 ? `at least ${signature.min}` : `${signature.min} to ${signature.max}`} arguments`;
183
+ }
184
+ //#endregion
185
+ //#region src/survey/responses/value-domain.ts
186
+ /** Finite domains describe individual selectable values, including for array slots. */
187
+ function responseValueElements(value) {
188
+ if (!Array.isArray(value.value)) return [value];
189
+ const type = value.type.slice(0, -2);
190
+ return value.value.map((entry) => ({
191
+ ...value,
192
+ type,
193
+ value: entry
194
+ }));
195
+ }
196
+ function responseValuesEqual(left, right) {
197
+ return left.type === right.type && JSON.stringify(left.value) === JSON.stringify(right.value) && (!("unit" in left) || "unit" in right && left.unit === right.unit);
198
+ }
199
+ /** No coercion: an export code or label is never a categorical option identity. */
200
+ function invalidResponseDomainValues(value, allowedValues) {
201
+ const domain = allowedValues.flatMap(responseValueElements);
202
+ return responseValueElements(value).filter((entry) => !domain.some((allowed) => responseValuesEqual(entry, allowed)));
203
+ }
204
+ function validateResponseSlotValue(slot, value) {
205
+ if (!isResponseValue(value)) return [`Slot "${slot.slotId}" has an invalid typed value.`];
206
+ if (value.type !== slot.valueType) return [`Slot "${slot.slotId}" requires ${slot.valueType}, received ${value.type}.`];
207
+ if (!slot.allowedValues) return [];
208
+ return invalidResponseDomainValues(value, slot.allowedValues).map((entry) => `Slot "${slot.slotId}" contains invalid value ${JSON.stringify(entry)}. Allowed values: ${JSON.stringify(slot.allowedValues)}.`);
209
+ }
210
+ //#endregion
211
+ //#region src/expressions/type-inference.ts
212
+ /** Infer expression result types from node shape and the survey's response slot types. */
213
+ /** Infer a node's result type without evaluating it; unknown slot or context types remain undefined. */
214
+ const inferExpressionReturnType = (expression, responseSlotTypes) => expression && visitExpression(expression, {
215
+ const: (node) => node.value?.type,
216
+ responseVariable: (node) => node.variableRef.method === "isDefined" ? "boolean" : ownValue(responseSlotTypes, node.variableRef.slotId),
217
+ ownerState: () => "boolean",
218
+ contextVariable: (node) => node.contextType === "locale" ? "string" : node.contextType === "currentDate" ? "date" : node.contextType === "participantFlag" ? node.asType ?? "string" : node.asType,
219
+ function: (node) => {
220
+ const { returns } = FUNCTION_SIGNATURES[node.functionName];
221
+ if (typeof returns === "string") return returns;
222
+ for (const index of returns.fromArguments) {
223
+ const type = inferExpressionReturnType(operand(node.arguments, index), responseSlotTypes);
224
+ if (type) return type;
225
+ }
226
+ }
227
+ });
228
+ //#endregion
229
+ //#region src/expressions/validation.ts
230
+ /** Report structural and type problems in authored expressions before graph execution. */
231
+ /** Describe operand type mismatches for one function call using known response slot types. */
232
+ function functionTypeIssues(node, types) {
233
+ const args = Array.from(node.arguments, (_, index) => inferExpressionReturnType(operand(node.arguments, index), types));
234
+ const messages = [];
235
+ const requireType = (index, expected) => {
236
+ const actual = args[index];
237
+ if (actual && actual !== expected) messages.push(`${node.functionName} argument ${index + 1} must be ${expected}, got ${actual}.`);
238
+ };
239
+ const all = (expected) => args.forEach((_, index) => requireType(index, expected));
240
+ const same = () => {
241
+ if (args[0] && args[1] && args[0] !== args[1]) messages.push(`${node.functionName} arguments must have the same type, got ${args[0]} and ${args[1]}.`);
242
+ };
243
+ const name = node.functionName;
244
+ switch (name) {
245
+ case "and":
246
+ case "or":
247
+ case "not":
248
+ all("boolean");
249
+ break;
250
+ case "eq":
251
+ case "gt":
252
+ case "gte":
253
+ case "lt":
254
+ case "lte":
255
+ same();
256
+ break;
257
+ case "str_eq":
258
+ all("string");
259
+ break;
260
+ case "sum":
261
+ case "min":
262
+ case "max":
263
+ all("number");
264
+ break;
265
+ case "in_range":
266
+ [
267
+ 0,
268
+ 1,
269
+ 2
270
+ ].forEach((index) => requireType(index, "number"));
271
+ requireType(3, "boolean");
272
+ break;
273
+ case "date_eq":
274
+ case "date_gt":
275
+ case "date_gte":
276
+ case "date_lt":
277
+ case "date_lte":
278
+ case "date_min":
279
+ case "date_max":
280
+ all("date");
281
+ break;
282
+ case "date_diff": {
283
+ requireType(0, "date");
284
+ requireType(1, "date");
285
+ requireType(2, "string");
286
+ const unit = operand(node.arguments, 2);
287
+ if (unit?.type === "const" && unit.value?.type === "string" && !isCalendarDateUnit(unit.value.value)) messages.push(`date_diff argument 3 must be one of ${CALENDAR_DATE_UNITS.join(", ")}, got ${JSON.stringify(unit.value.value)}.`);
288
+ break;
289
+ }
290
+ case "date_add_days":
291
+ case "date_add_months":
292
+ case "date_add_years":
293
+ requireType(0, "date");
294
+ requireType(1, "number");
295
+ break;
296
+ case "list_contains":
297
+ if (args[0] && args[0] !== "string[]" && args[0] !== "reference[]") messages.push(`list_contains argument 1 must be string[] or reference[], got ${args[0]}. Use eq for single-choice reference responses.`);
298
+ if (args[0] === "string[]") requireType(1, "string");
299
+ if (args[0] === "reference[]") requireType(1, "reference");
300
+ break;
301
+ case "list_count":
302
+ if (args[0] && !args[0].endsWith("[]")) messages.push(`list_count argument 1 must be a list, got ${args[0]}.`);
303
+ if (args[1] && !args[1].endsWith("[]")) messages.push(`list_count argument 2 must be a list, got ${args[1]}.`);
304
+ else if (args[0]?.endsWith("[]")) requireType(1, args[0]);
305
+ break;
306
+ case "if":
307
+ requireType(0, "boolean");
308
+ if (args[1] && args[2] && args[1] !== args[2]) messages.push(`if branches must have the same type, got ${args[1]} and ${args[2]}.`);
309
+ break;
310
+ default: unreachable(name);
311
+ }
312
+ return messages;
313
+ }
314
+ /** Locate malformed function calls and unfinished required operands in an expression tree. */
315
+ function expressionIssues(expression) {
316
+ const issues = [];
317
+ transformExpression(expression, {
318
+ const: (node) => node,
319
+ responseVariable: (node) => node,
320
+ ownerState: (node) => node,
321
+ contextVariable: (node) => node,
322
+ function: (node, path) => {
323
+ const signature = FUNCTION_SIGNATURES[node.functionName];
324
+ const arity = arityIssue(node);
325
+ if (arity) issues.push({
326
+ path,
327
+ message: arity
328
+ });
329
+ for (let index = 0; index < node.arguments.length; index++) if (!operand(node.arguments, index) && (signature.required.includes(index) || !signature.variadicGaps && signature.max === void 0)) issues.push({
330
+ path: [
331
+ ...path,
332
+ "arguments",
333
+ index
334
+ ],
335
+ message: `Operand ${index + 1} of ${node.functionName} is unfinished`
336
+ });
337
+ return node;
338
+ }
339
+ });
340
+ return issues;
341
+ }
342
+ /**
343
+ * Locate result, operand, and context key type errors. When option domains are supplied,
344
+ * also flag comparisons with option identities outside the response slot's domain.
345
+ */
346
+ function expressionTypeIssues(expression, types, expectedReturnType, domains) {
347
+ const issues = [];
348
+ transformExpression(expression, {
349
+ const: (node) => node,
350
+ responseVariable: (node) => node,
351
+ ownerState: (node) => node,
352
+ contextVariable: (node, path) => {
353
+ const keyType = inferExpressionReturnType(node.key, types);
354
+ if (keyType && keyType !== "string") issues.push({
355
+ path: [...path, "key"],
356
+ message: `Context keys must be string, got ${keyType}.`
357
+ });
358
+ return node;
359
+ },
360
+ function: (node, path) => {
361
+ for (const message of functionTypeIssues(node, types)) issues.push({
362
+ path,
363
+ message
364
+ });
365
+ if ([
366
+ "eq",
367
+ "list_contains",
368
+ "list_count"
369
+ ].includes(node.functionName)) for (const [sourceIndex, valueIndex] of [[0, 1], [1, 0]]) {
370
+ const source = operand(node.arguments, sourceIndex), value = operand(node.arguments, valueIndex);
371
+ if (source?.type !== "responseVariable" || source.variableRef.method !== "get" || value?.type !== "const" || !value.value) continue;
372
+ const allowed = domains?.get(source.variableRef.slotId);
373
+ for (const invalid of allowed ? invalidResponseDomainValues(value.value, allowed) : []) issues.push({
374
+ path: [
375
+ ...path,
376
+ "arguments",
377
+ valueIndex
378
+ ],
379
+ message: `Slot '${source.variableRef.slotId}' comparison contains an invalid value ${JSON.stringify(invalid.value)}; use a declared option identity, not an export code.`,
380
+ kind: "reference"
381
+ });
382
+ }
383
+ return node;
384
+ }
385
+ });
386
+ const actual = inferExpressionReturnType(expression, types);
387
+ if (expectedReturnType && actual && actual !== expectedReturnType) issues.push({
388
+ path: [],
389
+ message: `Must return ${expectedReturnType}, but this expression returns ${actual}.`
390
+ });
391
+ return issues;
392
+ }
393
+ //#endregion
394
+ //#region src/expressions/evaluator.ts
395
+ /** Evaluate canonical expressions against current answers, owner state, and session context. */
396
+ /** Raised when evaluation encounters an invalid function call or incompatible runtime value. */
397
+ var ExpressionEvaluationError = class extends Error {};
398
+ const boolean = (value) => ({
399
+ type: "boolean",
400
+ value
401
+ });
402
+ const number = (value) => ({
403
+ type: "number",
404
+ value
405
+ });
406
+ function evaluatedDates(values) {
407
+ const dates = [];
408
+ for (const value of values) {
409
+ if (value?.type !== "date" || !isCalendarDate(value.value)) return void 0;
410
+ dates.push(value.value);
411
+ }
412
+ return dates.length ? dates : void 0;
413
+ }
414
+ function comparableDates(values) {
415
+ const dates = evaluatedDates(values);
416
+ if (!dates || dates.some((date) => date.length !== dates[0].length)) return void 0;
417
+ return dates;
418
+ }
419
+ /** Evaluate the canonical tree; missing data returns undefined, while invalid calls can throw. */
420
+ function evaluateExpression(node, context) {
421
+ if (!node) return void 0;
422
+ const evaluate = (child) => evaluateExpression(child, context);
423
+ return visitExpression(node, {
424
+ const: (expression) => expression.value,
425
+ responseVariable: (expression) => {
426
+ const answer = context.answer(expression.variableRef.slotId);
427
+ return expression.variableRef.method === "get" ? answer : boolean(answer !== void 0);
428
+ },
429
+ ownerState: (expression) => boolean(context.disabled(expression.targetRef.ownerId)),
430
+ contextVariable: (expression) => {
431
+ if (expression.contextType === "locale") return context.locale === void 0 ? void 0 : {
432
+ type: "string",
433
+ value: context.locale
434
+ };
435
+ if (expression.contextType === "currentDate") return context.currentDate === void 0 ? void 0 : {
436
+ type: "date",
437
+ value: context.currentDate
438
+ };
439
+ const key = evaluate(expression.key);
440
+ if (key?.type !== "string") return void 0;
441
+ if (expression.contextType === "participantFlag") {
442
+ const value = ownValue(context.participantFlags, key.value);
443
+ if (expression.asType === "boolean") return boolean(value !== void 0);
444
+ if (value === void 0) return void 0;
445
+ if (expression.asType === "number") return Number.isNaN(parseFloat(value)) ? void 0 : number(parseFloat(value));
446
+ if (expression.asType === "date") return isCalendarDate(value) ? {
447
+ type: "date",
448
+ value
449
+ } : void 0;
450
+ return {
451
+ type: "string",
452
+ value
453
+ };
454
+ }
455
+ const value = ownValue(context.customValues, key.value);
456
+ if (value && expression.asType && value.type !== expression.asType) throw new ExpressionEvaluationError(`Context value '${key.value}' does not have expected type ${expression.asType}`);
457
+ return value;
458
+ },
459
+ function: (expression) => evaluateFunction(expression, evaluate)
460
+ });
461
+ }
462
+ function evaluateFunction(expression, evaluate) {
463
+ const arity = arityIssue(expression);
464
+ if (arity) throw new ExpressionEvaluationError(arity);
465
+ const arg = (index) => evaluate(operand(expression.arguments, index));
466
+ const all = () => Array.from(expression.arguments, (_, index) => arg(index));
467
+ const name = expression.functionName;
468
+ switch (name) {
469
+ case "and": return boolean(expression.arguments.every((_, index) => arg(index)?.value === true));
470
+ case "or": return boolean(expression.arguments.some((_, index) => arg(index)?.value === true));
471
+ case "not": {
472
+ const value = arg(0);
473
+ if (value === void 0) return void 0;
474
+ if (value.type !== "boolean") throw new ExpressionEvaluationError("not expects a boolean operand");
475
+ return boolean(!value.value);
476
+ }
477
+ case "list_contains": {
478
+ const list = arg(0), value = arg(1);
479
+ if (!list || !value) return boolean(false);
480
+ if (list.type !== "reference[]" && list.type !== "string[]" || value.type !== "reference" && value.type !== "string") throw new ExpressionEvaluationError("list_contains expects a string/reference array and a string/reference value");
481
+ return boolean(list.value.includes(value.value));
482
+ }
483
+ case "list_count": {
484
+ const list = arg(0);
485
+ if (!list) return number(0);
486
+ if (!Array.isArray(list.value)) throw new ExpressionEvaluationError("list_count expects a list operand");
487
+ if (!operand(expression.arguments, 1)) return number(list.value.length);
488
+ const filter = arg(1);
489
+ if (!filter) return number(0);
490
+ if (filter.type !== list.type) throw new ExpressionEvaluationError("list_count expects a filter of the list's type");
491
+ const allowed = responseValueElements(filter);
492
+ return number(responseValueElements(list).filter((entry) => allowed.some((value) => responseValuesEqual(entry, value))).length);
493
+ }
494
+ case "if": {
495
+ const condition = arg(0);
496
+ if (condition === void 0) return void 0;
497
+ if (condition.type !== "boolean") throw new ExpressionEvaluationError("if expects a boolean condition");
498
+ return arg(condition.value ? 1 : 2);
499
+ }
500
+ case "eq": {
501
+ const left = arg(0), right = arg(1);
502
+ if (!left || !right) return boolean(false);
503
+ if (left.type !== right.type) throw new ExpressionEvaluationError("eq expects matching operand types");
504
+ return boolean(left.value === right.value);
505
+ }
506
+ case "str_eq": {
507
+ const left = arg(0), right = arg(1);
508
+ if (!left || !right) return boolean(false);
509
+ if (left.type !== right.type) throw new ExpressionEvaluationError("str_eq expects matching operand types");
510
+ if (left.type !== "string") throw new ExpressionEvaluationError("str_eq expects string operands");
511
+ return boolean(left.value === right.value);
512
+ }
513
+ case "gt": {
514
+ const left = arg(0), right = arg(1);
515
+ if (!left || !right) return boolean(false);
516
+ if (left.type !== right.type) throw new ExpressionEvaluationError("gt expects matching operand types");
517
+ return boolean(left.value > right.value);
518
+ }
519
+ case "gte": {
520
+ const left = arg(0), right = arg(1);
521
+ if (!left || !right) return boolean(false);
522
+ if (left.type !== right.type) throw new ExpressionEvaluationError("gte expects matching operand types");
523
+ return boolean(left.value >= right.value);
524
+ }
525
+ case "lt": {
526
+ const left = arg(0), right = arg(1);
527
+ if (!left || !right) return boolean(false);
528
+ if (left.type !== right.type) throw new ExpressionEvaluationError("lt expects matching operand types");
529
+ return boolean(left.value < right.value);
530
+ }
531
+ case "lte": {
532
+ const left = arg(0), right = arg(1);
533
+ if (!left || !right) return boolean(false);
534
+ if (left.type !== right.type) throw new ExpressionEvaluationError("lte expects matching operand types");
535
+ return boolean(left.value <= right.value);
536
+ }
537
+ case "in_range": {
538
+ const value = arg(0), min = arg(1), max = arg(2), inclusive = arg(3)?.value === true;
539
+ if (!value || !min || !max) return boolean(false);
540
+ if (value.type !== "number" || min.type !== "number" || max.type !== "number") throw new ExpressionEvaluationError("in_range expects numeric bounds and value");
541
+ return boolean(inclusive ? value.value >= min.value && value.value <= max.value : value.value > min.value && value.value < max.value);
542
+ }
543
+ case "sum": {
544
+ let result = 0;
545
+ for (const value of all()) {
546
+ if (!value) continue;
547
+ if (value.type !== "number") throw new ExpressionEvaluationError("sum expects numeric operands");
548
+ result += value.value;
549
+ }
550
+ return number(result);
551
+ }
552
+ case "min": {
553
+ let result = Infinity;
554
+ for (const value of all()) {
555
+ if (!value) continue;
556
+ if (value.type !== "number") throw new ExpressionEvaluationError("min expects numeric operands");
557
+ result = Math.min(result, value.value);
558
+ }
559
+ return number(result);
560
+ }
561
+ case "max": {
562
+ let result = -Infinity;
563
+ for (const value of all()) {
564
+ if (!value) continue;
565
+ if (value.type !== "number") throw new ExpressionEvaluationError("max expects numeric operands");
566
+ result = Math.max(result, value.value);
567
+ }
568
+ return number(result);
569
+ }
570
+ case "date_add_days": {
571
+ const date = arg(0), amount = arg(1);
572
+ if (date?.type !== "date" || amount?.type !== "number") return void 0;
573
+ const value = addCalendarDate(date.value, amount.value, "days");
574
+ return value === void 0 ? void 0 : {
575
+ type: "date",
576
+ value
577
+ };
578
+ }
579
+ case "date_add_months": {
580
+ const date = arg(0), amount = arg(1);
581
+ if (date?.type !== "date" || amount?.type !== "number") return void 0;
582
+ const value = addCalendarDate(date.value, amount.value, "months");
583
+ return value === void 0 ? void 0 : {
584
+ type: "date",
585
+ value
586
+ };
587
+ }
588
+ case "date_add_years": {
589
+ const date = arg(0), amount = arg(1);
590
+ if (date?.type !== "date" || amount?.type !== "number") return void 0;
591
+ const value = addCalendarDate(date.value, amount.value, "years");
592
+ return value === void 0 ? void 0 : {
593
+ type: "date",
594
+ value
595
+ };
596
+ }
597
+ case "date_diff": {
598
+ const a = arg(0), b = arg(1), unit = arg(2);
599
+ if (unit === void 0) return void 0;
600
+ if (unit.type !== "string" || !isCalendarDateUnit(unit.value)) throw new ExpressionEvaluationError(`date_diff unit must be one of ${CALENDAR_DATE_UNITS.join(", ")}`);
601
+ if (a?.type !== "date" || b?.type !== "date") return void 0;
602
+ const value = differenceCalendarDates(a.value, b.value, unit.value);
603
+ return value === void 0 ? void 0 : number(value);
604
+ }
605
+ case "date_eq": {
606
+ const dates = evaluatedDates(all());
607
+ if (!dates) return void 0;
608
+ return boolean(dates[0] === dates[1]);
609
+ }
610
+ case "date_gt": {
611
+ const dates = comparableDates(all());
612
+ if (!dates) return void 0;
613
+ const comparison = compareCalendarDates(dates[0], dates[1]);
614
+ return comparison === void 0 ? void 0 : boolean(comparison > 0);
615
+ }
616
+ case "date_gte": {
617
+ const dates = comparableDates(all());
618
+ if (!dates) return void 0;
619
+ const comparison = compareCalendarDates(dates[0], dates[1]);
620
+ return comparison === void 0 ? void 0 : boolean(comparison >= 0);
621
+ }
622
+ case "date_lt": {
623
+ const dates = comparableDates(all());
624
+ if (!dates) return void 0;
625
+ const comparison = compareCalendarDates(dates[0], dates[1]);
626
+ return comparison === void 0 ? void 0 : boolean(comparison < 0);
627
+ }
628
+ case "date_lte": {
629
+ const dates = comparableDates(all());
630
+ if (!dates) return void 0;
631
+ const comparison = compareCalendarDates(dates[0], dates[1]);
632
+ return comparison === void 0 ? void 0 : boolean(comparison <= 0);
633
+ }
634
+ case "date_min": {
635
+ const dates = comparableDates(all());
636
+ if (!dates) return void 0;
637
+ return {
638
+ type: "date",
639
+ value: dates.reduce((a, b) => a < b ? a : b)
640
+ };
641
+ }
642
+ case "date_max": {
643
+ const dates = comparableDates(all());
644
+ if (!dates) return void 0;
645
+ return {
646
+ type: "date",
647
+ value: dates.reduce((a, b) => a > b ? a : b)
648
+ };
649
+ }
650
+ default: return unreachable(name);
651
+ }
652
+ }
653
+ //#endregion
654
+ //#region src/survey/responses/variable-naming.ts
655
+ const RESPONSE_VARIABLE_NAME_MAX_LENGTH = 32;
656
+ const isValidResponseVariableName = (name) => name.length <= 32 && /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(name);
657
+ /** Generate an initial suggestion; explicit rename operations validate instead of coercing. */
658
+ function suggestResponseVariableName(text, fallback = "response") {
659
+ const normalized = text.replace(/([a-z0-9])([A-Z])/g, "$1_$2").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
660
+ return ((/^[a-z]/.test(normalized) ? normalized : `${fallback}_${normalized}`) || fallback).slice(0, 32).replace(/_+$/g, "");
661
+ }
662
+ function allocateResponseVariableName(base, used, maxLength = 32) {
663
+ if (!isValidResponseVariableName(base)) throw new Error(`Invalid response variable name "${base}"; use lowercase snake_case, starting with a letter, at most 32 characters`);
664
+ if (base.length <= maxLength && !used.has(base)) return base;
665
+ const indexed = /^(.*)_([1-9][0-9]*)$/.exec(base);
666
+ const stemBase = indexed ? indexed[1] : base;
667
+ const start = indexed ? BigInt(indexed[2]) + 1n : 2n;
668
+ for (let n = start;; n++) {
669
+ const suffix = `_${n}`;
670
+ const stem = stemBase.slice(0, Math.max(0, maxLength - suffix.length)).replace(/_+$/g, "");
671
+ if (!stem) throw new Error("The variable naming suffix leaves no room for a name");
672
+ const name = `${stem}${suffix}`;
673
+ if (!used.has(name)) return name;
674
+ }
675
+ }
676
+ //#endregion
677
+ //#region src/survey/responses/slot-registry.ts
678
+ function deepFreeze(value) {
679
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
680
+ Object.freeze(value);
681
+ Object.values(value).forEach(deepFreeze);
682
+ }
683
+ return value;
684
+ }
685
+ /** A detached, derived index. Ambiguous identities never resolve to an arbitrary owner. */
686
+ var ResponseSlotRegistry = class {
687
+ slots = /* @__PURE__ */ new Map();
688
+ frozenDefinitions;
689
+ diagnostics = [];
690
+ constructor(declarations) {
691
+ const seen = /* @__PURE__ */ new Set();
692
+ for (const declaration of declarations) for (const definition of declaration.slots) {
693
+ const slot = deepFreeze(structuredClone({
694
+ ...definition,
695
+ itemId: declaration.itemId,
696
+ elementId: declaration.elementId
697
+ }));
698
+ if (!slot.slotId.trim() || !slot.elementId.trim() || !slot.itemId.trim() || !Object.values(ValueType).includes(slot.valueType)) {
699
+ this.diagnostics.push({
700
+ code: "invalid-slot",
701
+ slotId: slot.slotId,
702
+ itemId: slot.itemId,
703
+ message: "Response slots require nonempty identities and a supported value type."
704
+ });
705
+ continue;
706
+ }
707
+ if (seen.has(slot.slotId)) {
708
+ this.slots.delete(slot.slotId);
709
+ this.diagnostics.push({
710
+ code: "duplicate-slot-id",
711
+ slotId: slot.slotId,
712
+ itemId: slot.itemId,
713
+ message: `Slot "${slot.slotId}" is declared more than once. Allocate unique persistent identities before using this schema.`
714
+ });
715
+ continue;
716
+ }
717
+ seen.add(slot.slotId);
718
+ this.slots.set(slot.slotId, slot);
719
+ const elementType = slot.valueType.endsWith("[]") ? slot.valueType.slice(0, -2) : slot.valueType;
720
+ if (slot.allowedValues?.some((value) => !isResponseValue(value) || responseValueElements(value).some((entry) => entry.type !== elementType))) this.diagnostics.push({
721
+ code: "invalid-domain",
722
+ slotId: slot.slotId,
723
+ itemId: slot.itemId,
724
+ message: `The allowed values for slot "${slot.slotId}" must have type ${elementType}.`
725
+ });
726
+ if (slot.referenceValueScores && (slot.valueType !== ValueType.reference && slot.valueType !== ValueType.referenceArray || Object.entries(slot.referenceValueScores).some(([id, score]) => !Number.isFinite(score) || slot.allowedValues && !slot.allowedValues.some((value) => value.type === ValueType.reference && value.value === id)))) this.diagnostics.push({
727
+ code: "invalid-domain",
728
+ slotId: slot.slotId,
729
+ itemId: slot.itemId,
730
+ message: `Scores for slot "${slot.slotId}" must map allowed option IDs to finite numbers.`
731
+ });
732
+ }
733
+ const names = /* @__PURE__ */ new Map();
734
+ for (const slot of this.slots.values()) {
735
+ if (typeof slot.variableName !== "string" || !isValidResponseVariableName(slot.variableName)) this.diagnostics.push({
736
+ code: "invalid-variable-name",
737
+ slotId: slot.slotId,
738
+ itemId: slot.itemId,
739
+ message: `Variable "${slot.variableName}" must use lowercase snake_case, start with a letter and contain at most 32 characters.`
740
+ });
741
+ const previous = names.get(slot.variableName);
742
+ if (previous) this.diagnostics.push({
743
+ code: "duplicate-variable-name",
744
+ slotId: slot.slotId,
745
+ itemId: slot.itemId,
746
+ message: `Variable name "${slot.variableName}" is already used by slot "${previous}".`
747
+ });
748
+ names.set(slot.variableName, slot.slotId);
749
+ }
750
+ for (const slot of this.slots.values()) for (const dependency of slot.computed?.sourceSlotIds ?? []) if (!this.slots.has(dependency)) this.diagnostics.push({
751
+ code: "unknown-dependency",
752
+ slotId: slot.slotId,
753
+ itemId: slot.itemId,
754
+ message: `Computed slot "${slot.slotId}" depends on unknown or ambiguous slot "${dependency}".`
755
+ });
756
+ const active = /* @__PURE__ */ new Set();
757
+ const visited = /* @__PURE__ */ new Set();
758
+ const visit = (id) => {
759
+ if (active.has(id)) {
760
+ this.diagnostics.push({
761
+ code: "computed-cycle",
762
+ slotId: id,
763
+ message: `Computed slot "${id}" participates in a dependency cycle.`
764
+ });
765
+ return;
766
+ }
767
+ if (visited.has(id)) return;
768
+ active.add(id);
769
+ for (const dependency of this.slots.get(id)?.computed?.sourceSlotIds ?? []) visit(dependency);
770
+ active.delete(id);
771
+ visited.add(id);
772
+ };
773
+ for (const id of this.slots.keys()) visit(id);
774
+ }
775
+ /** Slots are deeply frozen and shared; copy one before deriving a changed definition. */
776
+ get(slotId) {
777
+ return this.slots.get(slotId);
778
+ }
779
+ get definitions() {
780
+ return this.frozenDefinitions ??= Object.freeze([...this.slots.values()]);
781
+ }
782
+ assertValid() {
783
+ if (this.diagnostics.length) throw new Error(this.diagnostics.map((entry) => entry.message).join("\n"));
784
+ }
785
+ validateAnswers(answers) {
786
+ const diagnostics = [...this.diagnostics];
787
+ for (const [slotId, value] of Object.entries(answers)) diagnostics.push(...this.validateAnswer(slotId, value));
788
+ return diagnostics;
789
+ }
790
+ /** Value diagnostics only; unrelated registry errors belong to the document. */
791
+ validateAnswer(slotId, value) {
792
+ const diagnostics = [];
793
+ const slot = this.slots.get(slotId);
794
+ if (!slot) {
795
+ diagnostics.push({
796
+ code: "unknown-slot",
797
+ slotId,
798
+ message: `Answer references unknown or ambiguous slot "${slotId}".`
799
+ });
800
+ return diagnostics;
801
+ }
802
+ const issues = slot.computed ? [`Computed slot "${slotId}" cannot contain a stored answer.`] : validateResponseSlotValue(slot, value);
803
+ if (isResponseValue(value) && value.type === "reference[]" && new Set(value.value).size !== value.value.length) issues.push("A selection cannot contain duplicate options");
804
+ for (const message of issues) diagnostics.push({
805
+ code: "invalid-answer",
806
+ slotId,
807
+ itemId: slot.itemId,
808
+ message
809
+ });
810
+ return diagnostics;
811
+ }
812
+ };
813
+ //#endregion
814
+ //#region src/survey/item-display-name.ts
815
+ function getCompositionLabel(types, locale, elementLabels = /* @__PURE__ */ new Map()) {
816
+ const language = locale.split("-")[0];
817
+ const labels = {
818
+ en: {
819
+ group: "Group",
820
+ "page-break": "Page break",
821
+ empty: "Empty item"
822
+ },
823
+ nl: {
824
+ group: "Groep",
825
+ "page-break": "Pagina-einde",
826
+ empty: "Leeg item"
827
+ },
828
+ de: {
829
+ group: "Gruppe",
830
+ "page-break": "Seitenumbruch",
831
+ empty: "Leeres Element"
832
+ }
833
+ };
834
+ const dictionary = labels[language] ?? labels.en;
835
+ return [...new Set(types)].map((type) => {
836
+ const labels = elementLabels.get(type);
837
+ return labels?.[locale] ?? labels?.[language] ?? labels?.en ?? dictionary[type] ?? type;
838
+ }).filter(Boolean).join(" + ") || dictionary.empty;
839
+ }
840
+ //#endregion
841
+ //#region src/survey/composition/document.ts
842
+ const isAdmissibleInDraft = (diagnostic) => diagnostic.severity !== "error" || diagnostic.kind === "reference";
843
+ const blockingFor = (diagnostics, admission) => diagnostics.filter((diagnostic) => admission === "draft" ? !isAdmissibleInDraft(diagnostic) : diagnostic.severity === "error");
844
+ /**
845
+ * The survey's languages with the default first, so fallbacks never depend on JSON key order.
846
+ * Hosts that hold unparsed survey JSON use this to pick the same language as the runtime.
847
+ */
848
+ function surveyLocales(survey) {
849
+ const locales = Object.keys(survey.translations ?? {});
850
+ const defaultLocale = survey.defaultLocale;
851
+ return defaultLocale && locales.includes(defaultLocale) ? [defaultLocale, ...locales.filter((locale) => locale !== defaultLocale)] : locales;
852
+ }
853
+ var DefinitionError = class extends Error {
854
+ diagnostics;
855
+ constructor(diagnostics) {
856
+ super(diagnostics.map((diagnostic) => `${JSON.stringify(diagnostic.path)}: ${diagnostic.message}`).join("\n"));
857
+ this.diagnostics = diagnostics;
858
+ }
859
+ };
860
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
861
+ const key = (path) => JSON.stringify(path);
862
+ const prefix = (parent, child) => parent.length <= child.length && parent.every((part, i) => part === child[i]);
863
+ /** Flow membership and assets stay structural; other references may dangle in a draft. */
864
+ const DRAFT_REFERENCE_TARGETS = /* @__PURE__ */ new Set([
865
+ "slot",
866
+ "state",
867
+ "option",
868
+ "owner",
869
+ "item"
870
+ ]);
871
+ const isFlowMembership = (path) => path[0] === "surveyItems" && path[2] === "config" && path[3] === "items";
872
+ function validateReferences(references, index, report) {
873
+ for (const reference of references) {
874
+ const identity = index.identities.get(reference.id);
875
+ if (!(reference.target === "slot" ? !!index.slots.get(reference.id) : reference.target === "state" ? [
876
+ "item",
877
+ "section",
878
+ "element",
879
+ "component"
880
+ ].includes(index.owners.get(reference.id)?.kind ?? "") : reference.target === "owner" ? index.owners.has(reference.id) : reference.target === "option" ? identity?.kind === "component" : identity?.kind === reference.target)) report("invalid-reference", reference.path, `Unknown ${reference.target} reference '${reference.id}'`, "error", DRAFT_REFERENCE_TARGETS.has(reference.target) && !isFlowMembership(reference.path) ? "reference" : void 0);
881
+ }
882
+ }
883
+ function validateLocalization(data, index, report) {
884
+ const defaultLocale = data.defaultLocale;
885
+ if (defaultLocale !== void 0 && !Object.hasOwn(data.translations ?? {}, defaultLocale)) report("default-locale", ["defaultLocale"], `Default locale '${defaultLocale}' has no translations`);
886
+ const references = [];
887
+ const collectAssets = (content, path) => {
888
+ for (const usage of getAssetUsagesFromContent(content)) references.push({
889
+ target: "asset",
890
+ id: usage.assetId,
891
+ path: [
892
+ ...path,
893
+ "doc",
894
+ "blocks",
895
+ usage.blockIndex,
896
+ "source",
897
+ "assetId"
898
+ ]
899
+ });
900
+ };
901
+ for (const [locale, translation] of Object.entries(data.translations ?? {})) {
902
+ const path = ["translations", locale];
903
+ for (const surface of SURVEY_LOCALIZATION_SURFACES) for (const [key, content] of Object.entries(translation[surface] ?? {})) collectAssets(content, [
904
+ ...path,
905
+ surface,
906
+ key
907
+ ]);
908
+ for (const [id, roles] of Object.entries(translation.content)) {
909
+ references.push({
910
+ target: "owner",
911
+ id,
912
+ path: [
913
+ ...path,
914
+ "content",
915
+ id
916
+ ]
917
+ });
918
+ for (const [role, content] of Object.entries(roles)) collectAssets(content, [
919
+ ...path,
920
+ "content",
921
+ id,
922
+ role
923
+ ]);
924
+ }
925
+ }
926
+ validateReferences(references, index, report);
927
+ for (const [locale, translation] of Object.entries(data.translations ?? {})) for (const [id, roles] of Object.entries(translation.content)) {
928
+ const owner = index.owners.get(id);
929
+ for (const role of Object.keys(roles)) if (!owner?.roles.some((entry) => entry.role === role)) report("content-role", [
930
+ "translations",
931
+ locale,
932
+ "content",
933
+ id,
934
+ role
935
+ ], `Owner '${id}' does not declare content role '${role}'`);
936
+ }
937
+ }
938
+ function sameJson(left, right) {
939
+ if (left === right) return true;
940
+ if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((entry, index) => sameJson(entry, right[index]));
941
+ if (!isRecord(left) || !isRecord(right)) return false;
942
+ if (![Object.prototype, null].includes(Object.getPrototypeOf(left)) || ![Object.prototype, null].includes(Object.getPrototypeOf(right))) return false;
943
+ const keys = Object.keys(left).filter((key) => left[key] !== void 0);
944
+ return keys.length === Object.keys(right).filter((key) => right[key] !== void 0).length && keys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
945
+ }
946
+ function canReuseStructureIndexes(left, right) {
947
+ const { translations: _leftTranslations, defaultLocale: _leftDefault, surveyItems: leftItems, ...leftFields } = left;
948
+ const { translations: _rightTranslations, defaultLocale: _rightDefault, surveyItems: rightItems, ...rightFields } = right;
949
+ return sameJson(leftFields, rightFields) && leftItems.length === rightItems.length && leftItems.every((item, index) => {
950
+ const { editorMetadata: _leftMetadata, ...leftItem } = item;
951
+ const { editorMetadata: _rightMetadata, ...rightItem } = rightItems[index];
952
+ return sameJson(leftItem, rightItem);
953
+ });
954
+ }
955
+ /** Fail before recursive parsers encounter cyclic input or exceed the common import budget. */
956
+ function checkDocumentBudget(input, maxNodes = 1e5, maxDepth = 128) {
957
+ const ancestors = /* @__PURE__ */ new Set();
958
+ const stack = [{
959
+ value: input,
960
+ depth: 0
961
+ }];
962
+ let nodes = 0;
963
+ while (stack.length) {
964
+ const entry = stack.pop();
965
+ if (typeof entry.value !== "object" || entry.value === null) continue;
966
+ if (entry.exit) {
967
+ ancestors.delete(entry.value);
968
+ continue;
969
+ }
970
+ if (++nodes > maxNodes || entry.depth > maxDepth) throw new Error("Survey exceeds the document node/depth budget");
971
+ if (ancestors.has(entry.value)) throw new Error("Survey input contains an object cycle");
972
+ ancestors.add(entry.value);
973
+ stack.push({
974
+ ...entry,
975
+ exit: true
976
+ });
977
+ for (const value of Object.values(entry.value)) stack.push({
978
+ value,
979
+ depth: entry.depth + 1
980
+ });
981
+ }
982
+ }
983
+ /** One parsed revision, one owner/identity index, and the existing shared response catalog. */
984
+ var SurveyStructure = class {
985
+ registry;
986
+ data;
987
+ owners = /* @__PURE__ */ new Map();
988
+ identities = /* @__PURE__ */ new Map();
989
+ elements = /* @__PURE__ */ new Map();
990
+ bodies = /* @__PURE__ */ new Map();
991
+ expressions = [];
992
+ diagnostics = [];
993
+ slots;
994
+ rootId;
995
+ flowItems;
996
+ ownerPaths = /* @__PURE__ */ new Map();
997
+ constructor(data, registry, admission) {
998
+ this.registry = registry;
999
+ this.data = data;
1000
+ const report = (code, path, message, severity = "error", kind) => this.diagnostics.push({
1001
+ code,
1002
+ path,
1003
+ message,
1004
+ severity,
1005
+ ...kind ? { kind } : {}
1006
+ });
1007
+ const nearestOwner = (path) => {
1008
+ for (let length = path.length; length >= 0; length--) {
1009
+ const owner = this.ownerPaths.get(key(path.slice(0, length)));
1010
+ if (owner) return owner;
1011
+ }
1012
+ };
1013
+ const visits = inspectAnnotated(registry.surveySchema, this.data);
1014
+ const bodies = [];
1015
+ for (const visit of visits) {
1016
+ const { annotation, value, path } = visit;
1017
+ if (annotation.kind === "expression") this.expressions.push({
1018
+ expression: expressionSchema.parse(value),
1019
+ path,
1020
+ ...annotation.expectedReturnType ? { expectedReturnType: annotation.expectedReturnType } : {}
1021
+ });
1022
+ if (annotation.kind === "owner") {
1023
+ if (!isRecord(value) || typeof value.id !== "string") throw new Error("Content owners require an annotated id");
1024
+ const parent = nearestOwner(path.slice(0, -1));
1025
+ const owner = {
1026
+ id: value.id,
1027
+ kind: "component",
1028
+ path,
1029
+ roles: annotation.roles,
1030
+ parentId: parent?.id,
1031
+ itemId: parent?.itemId ?? value.id,
1032
+ elementId: value.kind === "element" ? value.id : parent?.elementId,
1033
+ visibility: value.visibility === void 0 ? void 0 : expressionSchema.parse(value.visibility),
1034
+ disabled: value.disabled === void 0 ? void 0 : expressionSchema.parse(value.disabled)
1035
+ };
1036
+ if (!this.owners.has(owner.id)) this.owners.set(owner.id, owner);
1037
+ this.ownerPaths.set(key(path), owner);
1038
+ }
1039
+ if (annotation.kind === "identity") {
1040
+ if (typeof value !== "string") throw new Error("Identity annotations require string values");
1041
+ const owner = nearestOwner(path);
1042
+ if (owner && path.length === owner.path.length + 1 && path.at(-1) === "id") owner.kind = annotation.identity;
1043
+ const identity = {
1044
+ id: value,
1045
+ kind: annotation.identity,
1046
+ ownerId: owner?.id,
1047
+ path,
1048
+ slotAlias: annotation.slotAlias
1049
+ };
1050
+ if (this.identities.has(value)) report("duplicate-identity", path, `Identity '${value}' is declared more than once`);
1051
+ else this.identities.set(value, identity);
1052
+ }
1053
+ if (annotation.kind === "body") {
1054
+ const owner = nearestOwner(path);
1055
+ if (!owner) report("body-owner", path, "A body requires a declared content owner");
1056
+ else if (this.bodies.has(owner.id)) {
1057
+ if (key(this.bodies.get(owner.id).path) !== key(path)) report("body-owner", path, "A content owner can own only one body");
1058
+ else if (annotation.followUp) this.bodies.set(owner.id, {
1059
+ ownerId: owner.id,
1060
+ path,
1061
+ followUp: true
1062
+ });
1063
+ } else this.bodies.set(owner.id, {
1064
+ ownerId: owner.id,
1065
+ path,
1066
+ followUp: annotation.followUp
1067
+ });
1068
+ bodies.push({
1069
+ path,
1070
+ followUp: annotation.followUp,
1071
+ triggerId: annotation.followUp ? owner?.id : void 0
1072
+ });
1073
+ }
1074
+ }
1075
+ for (const owner of this.owners.values()) {
1076
+ let sections = owner.kind === "section" ? 1 : 0;
1077
+ let parent = owner.parentId && this.owners.get(owner.parentId);
1078
+ const seen = /* @__PURE__ */ new Set([owner.id]);
1079
+ while (parent) {
1080
+ if (seen.has(parent.id)) break;
1081
+ seen.add(parent.id);
1082
+ if (parent.kind === "section") sections++;
1083
+ parent = parent.parentId && this.owners.get(parent.parentId);
1084
+ }
1085
+ if (sections > 1) report("section-depth", owner.path, "Only one semantic section is allowed along an ownership path");
1086
+ owner.followUpTriggerId = bodies.filter((body) => body.followUp && prefix(body.path, owner.path)).at(-1)?.triggerId;
1087
+ }
1088
+ for (const body of bodies.filter((candidate) => candidate.followUp)) {
1089
+ if (bodies.some((parent) => parent.followUp && parent !== body && prefix(parent.path, body.path))) report("follow-up-depth", body.path, "Follow-ups cannot contain another follow-up, including an empty body");
1090
+ const trigger = body.triggerId && this.owners.get(body.triggerId);
1091
+ if (!trigger || trigger.kind !== "component") report("follow-up-trigger", body.path, "A follow-up must belong to a declared trigger component");
1092
+ if (bodies.some((other) => other !== body && other.followUp && other.triggerId === body.triggerId)) report("follow-up-trigger", body.path, "A trigger can own only one follow-up body");
1093
+ }
1094
+ const declarations = [];
1095
+ for (const item of this.data.surveyItems) {
1096
+ if (item.itemType !== "content") continue;
1097
+ for (const owner of this.owners.values()) {
1098
+ if (owner.itemId !== item.id || owner.kind !== "element") continue;
1099
+ let raw = this.data;
1100
+ for (const segment of owner.path) if (Array.isArray(raw) && typeof segment === "number") raw = raw[segment];
1101
+ else if (isRecord(raw)) raw = ownValue(raw, String(segment));
1102
+ else throw new Error("Invalid derived element path");
1103
+ const node = registry.elementSchema.parse(raw);
1104
+ const facts = registry.get(node.elementType).inspect(node);
1105
+ this.elements.set(node.id, {
1106
+ node,
1107
+ facts,
1108
+ itemId: item.id,
1109
+ path: owner.path
1110
+ });
1111
+ declarations.push({
1112
+ itemId: item.id,
1113
+ elementId: node.id,
1114
+ slots: facts.slots.map((slot) => ({
1115
+ ...slot,
1116
+ exportSettings: ownValue(node.exportSettings, slot.slotId)
1117
+ }))
1118
+ });
1119
+ for (const slot of facts.slots) {
1120
+ const identity = this.identities.get(slot.slotId);
1121
+ const slotOwner = identity?.ownerId ? this.owners.get(identity.ownerId) : void 0;
1122
+ if (!identity || identity.kind !== "slot" && !(identity.kind === "component" && identity.slotAlias) || slotOwner?.elementId !== node.id) report("slot-identity", owner.path, `Slot '${slot.slotId}' must be declared by its element schema (or explicitly alias its own component)`);
1123
+ }
1124
+ const ownComponent = (id) => this.owners.get(id)?.elementId === node.id;
1125
+ const storedSlots = facts.slots.filter((slot) => !slot.computed);
1126
+ const boundSlots = /* @__PURE__ */ new Set();
1127
+ for (const binding of facts.bindings) {
1128
+ if (!storedSlots.some((slot) => slot.slotId === binding.slotId) || boundSlots.has(binding.slotId)) report("slot-binding", owner.path, `Invalid or duplicate input binding '${binding.slotId}'`);
1129
+ boundSlots.add(binding.slotId);
1130
+ for (const id of [
1131
+ binding.controlId,
1132
+ ...binding.visibilityIds ?? [],
1133
+ ...binding.selection?.options ?? []
1134
+ ]) if (!ownComponent(id)) report("component-binding", owner.path, `Component '${id}' does not belong to element '${node.id}'`);
1135
+ if (binding.labelRefs) {
1136
+ if (!binding.labelRefs.length) report("control-label", owner.path, "Declared control labels must not be empty");
1137
+ for (const ref of binding.labelRefs) if (!ownComponent(ref.ownerId) || !this.owners.get(ref.ownerId)?.roles.some(({ role }) => role === ref.role)) report("control-label", owner.path, `Control label ${ref.ownerId}.${ref.role} is not an owned content role`);
1138
+ }
1139
+ if (binding.selection && new Set(binding.selection.options).size !== binding.selection.options.length) report("component-binding", owner.path, "Selection options must be unique");
1140
+ }
1141
+ for (const slot of storedSlots) if (!boundSlots.has(slot.slotId)) report("slot-binding", owner.path, `Stored slot '${slot.slotId}' requires a control binding`);
1142
+ for (const [child, parents] of Object.entries(facts.componentParents)) if (!ownComponent(child) || parents.some((id) => !ownComponent(id))) report("component-binding", owner.path, "Component inheritance must stay within its declaring element");
1143
+ for (const binding of facts.followUps) {
1144
+ if (!ownComponent(binding.triggerId)) report("follow-up-activation", owner.path, "Follow-up trigger must belong to its declaring element");
1145
+ if ("selectionSlotId" in binding && !facts.bindings.some((input) => input.slotId === binding.selectionSlotId && input.selection?.options.includes(binding.triggerId))) report("follow-up-activation", owner.path, "Selection activation requires an option of its declared input");
1146
+ }
1147
+ for (const id of Object.keys(node.exportSettings ?? {})) if (!facts.slots.some((slot) => slot.slotId === id)) report("slot-settings-owner", owner.path, `Settings target '${id}' is not owned by this element`);
1148
+ for (const id of Object.keys(node.prefills ?? {})) if (!facts.slots.some((slot) => slot.slotId === id && !slot.computed)) report("prefill-owner", owner.path, `Prefill target '${id}' is not a writable slot owned by this element`);
1149
+ for (const body of bodies.filter((body) => body.followUp && body.triggerId && this.owners.get(body.triggerId)?.elementId === node.id)) {
1150
+ if (!facts.fullWidth) report("follow-up-width", body.path, "Follow-up presentations require a full row");
1151
+ if (!facts.followUps.some((binding) => binding.triggerId === body.triggerId)) report("follow-up-activation", body.path, "Follow-up activation must be declared by its element domain");
1152
+ }
1153
+ for (const binding of facts.followUps) if (!bodies.some((body) => body.followUp && body.triggerId === binding.triggerId)) report("follow-up-activation", owner.path, "Activation references a trigger without a follow-up body");
1154
+ }
1155
+ }
1156
+ this.slots = new ResponseSlotRegistry(declarations);
1157
+ for (const diagnostic of this.slots.diagnostics) report(diagnostic.code, [], diagnostic.message);
1158
+ for (const element of this.elements.values()) for (const [slotId, candidates] of Object.entries(element.node.prefills ?? {})) candidates.forEach((candidate, index) => {
1159
+ const path = [
1160
+ ...element.path,
1161
+ "prefills",
1162
+ slotId,
1163
+ index,
1164
+ "source"
1165
+ ];
1166
+ if (candidate.source.type === "static") for (const issue of this.slots.validateAnswer(slotId, candidate.source.value)) report("prefill-domain", [...path, "value"], issue.message);
1167
+ if (candidate.source.type === "templateValue" && !ownValue(this.data.templateValues, candidate.source.key)) report("prefill-template", [...path, "key"], `Unknown prefill template '${candidate.source.key}'`);
1168
+ });
1169
+ validateReferences(annotatedReferences(visits).filter((reference) => reference.path[0] !== "translations"), this, report);
1170
+ validateLocalization(this.data, this, report);
1171
+ const items = new Map(this.data.surveyItems.map((item) => [item.id, item]));
1172
+ this.flowItems = items;
1173
+ const roots = this.data.surveyItems.filter((item) => item.itemType === "group" && item.config.isRoot);
1174
+ this.rootId = roots[0]?.id ?? "";
1175
+ if (roots.length !== 1) report("flow-root", ["surveyItems"], "The survey must contain exactly one root group");
1176
+ const parents = /* @__PURE__ */ new Map();
1177
+ for (const item of items.values()) if (item.itemType === "group") for (const child of item.config.items) {
1178
+ if (parents.has(child)) report("flow-parent", [], `Flow item '${child}' has more than one parent`);
1179
+ parents.set(child, item.id);
1180
+ const owner = this.owners.get(child);
1181
+ if (owner) owner.parentId = item.id;
1182
+ }
1183
+ for (const item of items.values()) {
1184
+ const chain = /* @__PURE__ */ new Set();
1185
+ let id = item.id;
1186
+ while (id) {
1187
+ if (chain.has(id)) {
1188
+ report("flow-cycle", [], `Flow cycle through '${id}'`);
1189
+ break;
1190
+ }
1191
+ chain.add(id);
1192
+ id = parents.get(id);
1193
+ }
1194
+ if (item.id === this.rootId && parents.has(item.id)) report("flow-root", [], "The root group cannot have a parent");
1195
+ if (item.id !== this.rootId && !parents.has(item.id)) report("flow-orphan", [], `Flow item '${item.id}' has no parent`);
1196
+ if (item.itemType === "content") for (const rule of item.config.validationRules ?? []) for (const slotId of rule.targets) {
1197
+ const target = this.slots.get(slotId);
1198
+ if (target && target.itemId !== item.id) report("rule-scope", [], `Rule '${rule.id}' targets a slot outside its content item`);
1199
+ }
1200
+ }
1201
+ if (blockingFor(this.diagnostics, admission).length) throw new DefinitionError(this.diagnostics);
1202
+ for (const value of this.owners.values()) freezeDocument(value);
1203
+ for (const value of this.identities.values()) freezeDocument(value);
1204
+ for (const value of this.elements.values()) freezeDocument(value);
1205
+ for (const value of this.bodies.values()) freezeDocument(value);
1206
+ freezeDocument(this.expressions);
1207
+ }
1208
+ };
1209
+ function freezeDocument(value) {
1210
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
1211
+ Object.freeze(value);
1212
+ Object.values(value).forEach(freezeDocument);
1213
+ }
1214
+ return value;
1215
+ }
1216
+ /** A detached revision. Translation publication shares immutable structural indexes. */
1217
+ var Survey = class Survey {
1218
+ data;
1219
+ registry;
1220
+ structure;
1221
+ itemIndex;
1222
+ definitionDiagnostics;
1223
+ constructor(data, registry, structure, itemIndex = structure.flowItems, definitionDiagnostics = structure.diagnostics) {
1224
+ this.data = data;
1225
+ this.registry = registry;
1226
+ this.structure = structure;
1227
+ this.itemIndex = itemIndex;
1228
+ this.definitionDiagnostics = definitionDiagnostics;
1229
+ freezeDocument(data);
1230
+ }
1231
+ /**
1232
+ * Parse and index one revision. `previous` lets an unchanged structure share its indexes.
1233
+ * Editors, history, editor files and assistant drafts use `admission: "draft"`; everything that
1234
+ * executes, submits or releases a survey keeps the strict default.
1235
+ */
1236
+ static parse(input, registry, options = {}) {
1237
+ const { previous, admission = "strict" } = options;
1238
+ checkDocumentBudget(input);
1239
+ const data = registry.surveySchema.parse(input);
1240
+ checkDocumentBudget(data);
1241
+ if (previous?.registry === registry && canReuseStructureIndexes(data, previous.data)) {
1242
+ const diagnostics = previous.structure.diagnostics.filter((diagnostic) => !diagnostic.path.length || diagnostic.path[0] !== "translations" && diagnostic.path[0] !== "defaultLocale");
1243
+ const report = (code, path, message, severity = "error", kind) => diagnostics.push({
1244
+ code,
1245
+ path,
1246
+ message,
1247
+ severity,
1248
+ ...kind ? { kind } : {}
1249
+ });
1250
+ validateLocalization(data, previous.structure, report);
1251
+ if (blockingFor(diagnostics, admission).length) throw new DefinitionError(diagnostics);
1252
+ return new Survey(data, registry, previous.structure, new Map(data.surveyItems.map((item) => [item.id, item])), diagnostics);
1253
+ }
1254
+ const structure = new SurveyStructure(data, registry, admission);
1255
+ return new Survey(structure.data, registry, structure);
1256
+ }
1257
+ static blank(registry) {
1258
+ return Survey.parse({
1259
+ $schema: CURRENT_SURVEY_SCHEMA,
1260
+ schemaVersion: 3,
1261
+ surveyItems: [{
1262
+ id: generateId(),
1263
+ itemType: "group",
1264
+ config: {
1265
+ isRoot: true,
1266
+ items: []
1267
+ }
1268
+ }]
1269
+ }, registry);
1270
+ }
1271
+ get rootId() {
1272
+ return this.structure.rootId;
1273
+ }
1274
+ get locales() {
1275
+ return surveyLocales(this.data);
1276
+ }
1277
+ get defaultLocale() {
1278
+ return this.locales[0];
1279
+ }
1280
+ get surveyKey() {
1281
+ return this.data.metadata?.surveyKey;
1282
+ }
1283
+ get metadata() {
1284
+ return this.data.metadata;
1285
+ }
1286
+ get maxItemsPerPage() {
1287
+ return this.data.maxItemsPerPage;
1288
+ }
1289
+ get rootItem() {
1290
+ const root = this.getItem(this.rootId);
1291
+ if (root?.itemType !== "group") throw new Error("Validated survey is missing its root group");
1292
+ return root;
1293
+ }
1294
+ getItemPath(itemId) {
1295
+ if (!this.getItem(itemId)) throw new Error(`Item ${itemId} not found`);
1296
+ const path = [];
1297
+ let parent = this.getParentItem(itemId);
1298
+ while (parent) {
1299
+ path.unshift(parent.id);
1300
+ parent = this.getParentItem(parent.id);
1301
+ }
1302
+ return path;
1303
+ }
1304
+ getChildrenItems(parentId) {
1305
+ const parent = this.getItem(parentId);
1306
+ return parent?.itemType === "group" ? parent.config.items.map((id) => this.getItem(id)) : [];
1307
+ }
1308
+ getSiblings(itemId) {
1309
+ const parent = this.getParentItem(itemId);
1310
+ return parent ? this.getChildrenItems(parent.id) : [];
1311
+ }
1312
+ resolveItemEditorName(itemId, locale = this.locales[0] ?? "en") {
1313
+ const item = this.getItem(itemId);
1314
+ if (!item) return getCompositionLabel([], locale);
1315
+ const explicit = item.metadata?.itemLabel?.trim();
1316
+ if (explicit) return explicit;
1317
+ const content = ownValue(this.data.translations, locale)?.content;
1318
+ const summary = (value) => getContentPlainText(value).replace(/\s+/g, " ").trim();
1319
+ const title = summary(ownValue(content, itemId)?.title);
1320
+ if (title) return title;
1321
+ const elements = [...this.elements.values()].filter((element) => element.itemId === itemId);
1322
+ for (const element of elements) {
1323
+ const roles = ownValue(content, element.node.id);
1324
+ const wording = summary(roles?.label) || summary(roles?.body);
1325
+ if (wording) return wording;
1326
+ }
1327
+ return getCompositionLabel(item.itemType === "content" ? elements.map(({ node }) => node.elementType) : [item.itemType], locale, this.registry.labels);
1328
+ }
1329
+ getItemDisplayPath(itemId, locale = this.locales[0] ?? "en") {
1330
+ if (!this.getItem(itemId)) return void 0;
1331
+ const path = this.getItemPath(itemId), name = this.resolveItemEditorName(itemId, locale);
1332
+ const namePath = path.map((id) => this.resolveItemEditorName(id, locale));
1333
+ return {
1334
+ itemId,
1335
+ name,
1336
+ path,
1337
+ namePath,
1338
+ breadcrumb: [...namePath, name].join(" / "),
1339
+ parentBreadcrumb: namePath.join(" / ")
1340
+ };
1341
+ }
1342
+ getItemEditorBreadcrumb(itemId, locale = this.locales[0] ?? "en") {
1343
+ const path = this.getItemDisplayPath(itemId, locale);
1344
+ if (!path) return void 0;
1345
+ return [...path.namePath.slice(path.path[0] === this.rootId ? 1 : 0), path.name].join(" / ");
1346
+ }
1347
+ get itemOutline() {
1348
+ const result = [];
1349
+ const visit = (item) => {
1350
+ result.push(this.getItemDisplayPath(item.id));
1351
+ this.getChildrenItems(item.id).forEach(visit);
1352
+ };
1353
+ visit(this.rootItem);
1354
+ return result;
1355
+ }
1356
+ getTemplateValue(key) {
1357
+ return ownValue(this.data.templateValues, key);
1358
+ }
1359
+ getTemplateValueKeys() {
1360
+ return Object.keys(this.data.templateValues ?? {});
1361
+ }
1362
+ getAsset(assetId) {
1363
+ return ownValue(this.data.assets, assetId);
1364
+ }
1365
+ getAssets() {
1366
+ return new Map(Object.entries(this.data.assets ?? {}));
1367
+ }
1368
+ getAssetIds() {
1369
+ return Object.keys(this.data.assets ?? {});
1370
+ }
1371
+ hasAsset(assetId) {
1372
+ return Object.hasOwn(this.data.assets ?? {}, assetId);
1373
+ }
1374
+ getAssetUsages(filterAssetId) {
1375
+ const usages = [];
1376
+ const collect = (content, ref, locale) => {
1377
+ for (const usage of getAssetUsagesFromContent(content)) {
1378
+ if (filterAssetId && usage.assetId !== filterAssetId) continue;
1379
+ const itemId = ref.scope === "content" ? this.owners.get(ref.ownerId).itemId : this.rootId;
1380
+ usages.push({
1381
+ ...usage,
1382
+ ref,
1383
+ locale,
1384
+ itemId
1385
+ });
1386
+ }
1387
+ };
1388
+ for (const [locale, translation] of Object.entries(this.data.translations ?? {})) {
1389
+ for (const surface of SURVEY_LOCALIZATION_SURFACES) for (const [contentKey, content] of Object.entries(translation[surface] ?? {})) collect(content, {
1390
+ scope: "survey",
1391
+ surface,
1392
+ contentKey
1393
+ }, locale);
1394
+ for (const [ownerId, roles] of Object.entries(translation.content)) for (const [role, content] of Object.entries(roles)) collect(content, {
1395
+ scope: "content",
1396
+ ownerId,
1397
+ role
1398
+ }, locale);
1399
+ }
1400
+ return usages;
1401
+ }
1402
+ get flowItems() {
1403
+ return this.itemIndex;
1404
+ }
1405
+ get owners() {
1406
+ return this.structure.owners;
1407
+ }
1408
+ get identities() {
1409
+ return this.structure.identities;
1410
+ }
1411
+ get elements() {
1412
+ return this.structure.elements;
1413
+ }
1414
+ get expressions() {
1415
+ return this.structure.expressions;
1416
+ }
1417
+ get bodies() {
1418
+ return this.structure.bodies;
1419
+ }
1420
+ get slots() {
1421
+ return this.structure.slots;
1422
+ }
1423
+ get diagnostics() {
1424
+ return this.definitionDiagnostics;
1425
+ }
1426
+ /**
1427
+ * Document-index errors that prevent running, submitting or releasing this revision. Graph
1428
+ * compilation can add more; use getBrokenReferences for the complete set of broken references.
1429
+ */
1430
+ get blockingDiagnostics() {
1431
+ return this.definitionDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
1432
+ }
1433
+ getItem(itemId) {
1434
+ return this.itemIndex.get(itemId);
1435
+ }
1436
+ getParentItem(itemId) {
1437
+ if (!this.getItem(itemId)) return void 0;
1438
+ const parentId = this.owners.get(itemId)?.parentId;
1439
+ const parent = parentId ? this.getItem(parentId) : void 0;
1440
+ return parent?.itemType === "group" ? parent : void 0;
1441
+ }
1442
+ /** Inclusive ancestry, as used by the outline's subtree/drop guards. */
1443
+ isDescendantOf(itemId, ancestorId) {
1444
+ let item = this.getItem(itemId);
1445
+ while (item) {
1446
+ if (item.id === ancestorId) return true;
1447
+ item = this.getParentItem(item.id);
1448
+ }
1449
+ return false;
1450
+ }
1451
+ serialize() {
1452
+ return structuredClone(this.data);
1453
+ }
1454
+ /** Editor timestamps cannot affect identity, conditions or responses; share their indexes. */
1455
+ withItemEditorMetadata(updates) {
1456
+ if (!updates.size) return this;
1457
+ const items = new Map(this.itemIndex);
1458
+ for (const [id, metadata] of updates) {
1459
+ const item = items.get(id);
1460
+ if (!item) throw new Error(`Unknown item '${id}'`);
1461
+ items.set(id, {
1462
+ ...item,
1463
+ editorMetadata: itemEditorMetadataSchema.parse(metadata)
1464
+ });
1465
+ }
1466
+ const data = {
1467
+ ...this.data,
1468
+ surveyItems: this.data.surveyItems.map((item) => items.get(item.id))
1469
+ };
1470
+ checkDocumentBudget(data);
1471
+ return new Survey(data, this.registry, this.structure, items, this.definitionDiagnostics);
1472
+ }
1473
+ /** Sets the default language without rebuilding structural indexes. */
1474
+ withDefaultLocale(locale) {
1475
+ if (!Object.hasOwn(this.data.translations ?? {}, locale)) throw new Error(`Default locale '${locale}' has no translations`);
1476
+ return new Survey({
1477
+ ...this.data,
1478
+ defaultLocale: locale
1479
+ }, this.registry, this.structure, this.itemIndex, this.definitionDiagnostics);
1480
+ }
1481
+ withLocalizationContent(ref, locale, content) {
1482
+ validateLocalizationRef(this, ref);
1483
+ if (!locale.trim()) throw new Error("A locale is required");
1484
+ if (content === void 0 && readLocalizationContent(this, ref, locale) === void 0) return this;
1485
+ const translations = { ...this.data.translations };
1486
+ const old = ownValue(translations, locale) ?? { content: {} };
1487
+ const translation = {
1488
+ ...old,
1489
+ content: { ...old.content }
1490
+ };
1491
+ let fields;
1492
+ if (ref.scope === "content") {
1493
+ fields = { ...ownValue(translation.content, ref.ownerId) };
1494
+ if (content === void 0) delete fields[ref.role];
1495
+ else setOwnValue(fields, ref.role, content);
1496
+ if (Object.keys(fields).length) setOwnValue(translation.content, ref.ownerId, fields);
1497
+ else delete translation.content[ref.ownerId];
1498
+ } else {
1499
+ fields = { ...translation[ref.surface] };
1500
+ if (content === void 0) delete fields[ref.contentKey];
1501
+ else setOwnValue(fields, ref.contentKey, content);
1502
+ translation[ref.surface] = fields;
1503
+ }
1504
+ setOwnValue(translations, locale, translation);
1505
+ const data = {
1506
+ ...this.data,
1507
+ translations
1508
+ };
1509
+ checkDocumentBudget(data);
1510
+ if (content !== void 0) {
1511
+ const parsed = surveyContentSchema.parse(content);
1512
+ for (const asset of getAssetUsagesFromContent(parsed)) if (!ownValue(this.data.assets, asset.assetId)) throw new Error(`Unknown content asset '${asset.assetId}'`);
1513
+ setOwnValue(fields, ref.scope === "content" ? ref.role : ref.contentKey, parsed);
1514
+ }
1515
+ return new Survey(data, this.registry, this.structure, this.itemIndex, this.definitionDiagnostics);
1516
+ }
1517
+ };
1518
+ //#endregion
1519
+ //#region src/engine/survey-state-graph.ts
1520
+ const stateNodeKey = (type, id) => JSON.stringify([type, id]);
1521
+ /** Compiled dependencies for one survey definition; evaluate against current answers and context. */
1522
+ var SurveyStateGraph = class {
1523
+ document;
1524
+ nodes = /* @__PURE__ */ new Map();
1525
+ bindings = /* @__PURE__ */ new Map();
1526
+ initializationOrder;
1527
+ /**
1528
+ * Draft-only problems that did not prevent compilation: dangling references (which evaluate as
1529
+ * missing) and unfinished expressions. Always empty for a strict graph.
1530
+ */
1531
+ admittedDiagnostics;
1532
+ /**
1533
+ * `strict` (default) compiles only executable definitions. `draft` serves editors and editor
1534
+ * previews: unfinished expressions and dangling references are admitted, and a missing answer or
1535
+ * state target reads as missing (unanswered, not disabled, not visible).
1536
+ */
1537
+ constructor(document, options = {}) {
1538
+ this.document = document;
1539
+ const draft = options.admission === "draft";
1540
+ if (!draft && document.blockingDiagnostics.length) throw new DefinitionError(document.blockingDiagnostics);
1541
+ const diagnostics = [];
1542
+ const admitted = [];
1543
+ const types = Object.fromEntries(document.slots.definitions.map((slot) => [slot.slotId, slot.valueType]));
1544
+ const domains = new Map(document.slots.definitions.flatMap((slot) => slot.allowedValues ? [[slot.slotId, slot.allowedValues]] : []));
1545
+ for (const { expression, path, expectedReturnType } of document.expressions) for (const issue of expressionTypeIssues(expression, types, expectedReturnType, domains)) diagnostics.push({
1546
+ ...issue,
1547
+ path: [...path, ...issue.path],
1548
+ code: "expression-type",
1549
+ severity: "error"
1550
+ });
1551
+ for (const element of document.elements.values()) for (const [slotId, candidates] of Object.entries(element.node.prefills ?? {})) candidates.forEach((candidate, index) => {
1552
+ const target = document.slots.get(slotId);
1553
+ const path = [
1554
+ ...element.path,
1555
+ "prefills",
1556
+ slotId,
1557
+ index
1558
+ ];
1559
+ const check = (expression, expected, at) => {
1560
+ for (const issue of expressionTypeIssues(expression, types, expected, domains)) diagnostics.push({
1561
+ ...issue,
1562
+ path: [...at, ...issue.path],
1563
+ code: "prefill-type",
1564
+ severity: "error"
1565
+ });
1566
+ };
1567
+ if (candidate.when) check(candidate.when, "boolean", [...path, "when"]);
1568
+ if (candidate.source.type === "expression") check(candidate.source.expression, target.valueType, [
1569
+ ...path,
1570
+ "source",
1571
+ "expression"
1572
+ ]);
1573
+ if (candidate.source.type === "templateValue") {
1574
+ const template = ownValue(document.data.templateValues, candidate.source.key);
1575
+ if (template.returnType !== target.valueType) diagnostics.push({
1576
+ code: "prefill-type",
1577
+ severity: "error",
1578
+ path: [...path, "source"],
1579
+ message: `Prefill template produces ${template.returnType}, but '${slotId}' requires ${target.valueType}`
1580
+ });
1581
+ if (template.expression) check(template.expression, template.type === "formatDate" ? "date" : template.returnType, [
1582
+ "templateValues",
1583
+ candidate.source.key,
1584
+ "expression"
1585
+ ]);
1586
+ }
1587
+ });
1588
+ for (const { expression, path } of document.expressions) for (const issue of expressionIssues(expression)) (draft ? admitted : diagnostics).push({
1589
+ ...issue,
1590
+ path: [...path, ...issue.path],
1591
+ code: "invalid-expression",
1592
+ severity: "error"
1593
+ });
1594
+ const nodePaths = /* @__PURE__ */ new Map();
1595
+ const slotOwners = new Map(document.slots.definitions.map((slot) => [slot.slotId, slot.elementId]));
1596
+ const add = (type, id, dependencies, evaluate) => {
1597
+ const key = stateNodeKey(type, id);
1598
+ if (this.nodes.has(key)) throw new Error(`Duplicate graph node ${key}`);
1599
+ this.nodes.set(key, {
1600
+ key,
1601
+ dependencies,
1602
+ evaluate
1603
+ });
1604
+ const owner = document.owners.get(id) ?? document.owners.get(slotOwners.get(id) ?? "");
1605
+ if (owner) nodePaths.set(key, owner.path);
1606
+ };
1607
+ const edge = (type, id, reason) => ({
1608
+ key: stateNodeKey(type, id),
1609
+ reason
1610
+ });
1611
+ const dependencies = (expression) => {
1612
+ if (!expression) return [];
1613
+ return collectExpressionDependencies(expression).map((dependency) => dependency.kind === "answer" ? edge("answer", dependency.slotId, "authored answer reference") : edge("disabled", dependency.targetRef.ownerId, "authored state reference"));
1614
+ };
1615
+ const evaluate = (expression, read, input, issues, path) => {
1616
+ try {
1617
+ return evaluateExpression(expression, {
1618
+ ...input.context,
1619
+ answer: (id) => {
1620
+ const value = read(stateNodeKey("answer", id));
1621
+ return typeof value === "boolean" ? void 0 : value;
1622
+ },
1623
+ disabled: (id) => read(stateNodeKey("disabled", id)) === true
1624
+ });
1625
+ } catch (error) {
1626
+ if (!(error instanceof ExpressionEvaluationError)) throw error;
1627
+ issues.push({
1628
+ code: "condition-evaluation",
1629
+ path,
1630
+ message: error.message,
1631
+ severity: "warning"
1632
+ });
1633
+ return;
1634
+ }
1635
+ };
1636
+ for (const owner of document.owners.values()) {
1637
+ const domainParents = owner.elementId ? ownValue(document.elements.get(owner.elementId)?.facts.componentParents, owner.id) ?? [] : [];
1638
+ const parents = [...owner.parentId ? [owner.parentId] : [], ...domainParents];
1639
+ const inheritedVisibility = parents.map((id) => edge("visibility", id, "visibility inheritance"));
1640
+ const followUp = owner.followUpTriggerId ? [edge("followUpActive", owner.followUpTriggerId, "follow-up activation")] : [];
1641
+ add("visibility", owner.id, [
1642
+ ...inheritedVisibility,
1643
+ ...followUp,
1644
+ ...dependencies(owner.visibility)
1645
+ ], (read, input, issues) => {
1646
+ if ([...inheritedVisibility, ...followUp].some((dependency) => read(dependency.key) !== true)) return false;
1647
+ if (!owner.visibility) return true;
1648
+ const result = evaluate(owner.visibility, read, input, issues, [...owner.path, "visibility"]);
1649
+ if (result && result.type !== "boolean") issues.push({
1650
+ code: "condition-type",
1651
+ path: [...owner.path, "visibility"],
1652
+ message: "Visibility must return a boolean",
1653
+ severity: "warning"
1654
+ });
1655
+ return result?.type === "boolean" && result.value;
1656
+ });
1657
+ const inheritedDisabled = parents.map((id) => edge("disabled", id, "disabled inheritance"));
1658
+ add("disabled", owner.id, [...inheritedDisabled, ...dependencies(owner.disabled)], (read, input, issues) => {
1659
+ const inherited = inheritedDisabled.some((dependency) => read(dependency.key) === true);
1660
+ if (!owner.disabled) return inherited;
1661
+ const result = evaluate(owner.disabled, read, input, issues, [...owner.path, "disabled"]);
1662
+ if (!result || result.type !== "boolean") issues.push({
1663
+ code: "condition-type",
1664
+ path: [...owner.path, "disabled"],
1665
+ message: "Disabled condition did not return a boolean; local disabling is false",
1666
+ severity: "warning"
1667
+ });
1668
+ return inherited || result?.type === "boolean" && result.value;
1669
+ });
1670
+ }
1671
+ for (const element of document.elements.values()) {
1672
+ for (const binding of element.facts.bindings) {
1673
+ if (this.bindings.has(binding.slotId)) throw new Error(`Duplicate control binding for ${binding.slotId}`);
1674
+ this.bindings.set(binding.slotId, binding);
1675
+ }
1676
+ for (const slot of element.facts.slots) {
1677
+ const storedKey = stateNodeKey("storedAnswer", slot.slotId);
1678
+ if (!slot.computed) add("storedAnswer", slot.slotId, [], (_read, input, issues) => {
1679
+ const answer = ownValue(input.answers, slot.slotId);
1680
+ if (answer === void 0) return void 0;
1681
+ const errors = validateResponseSlotValue(slot, answer);
1682
+ if (errors.length) {
1683
+ issues.push({
1684
+ code: "invalid-answer",
1685
+ path: ["answers", slot.slotId],
1686
+ message: errors.join("; "),
1687
+ severity: "error"
1688
+ });
1689
+ return;
1690
+ }
1691
+ return answer;
1692
+ });
1693
+ const binding = this.bindings.get(slot.slotId);
1694
+ const visibility = [binding?.controlId ?? element.node.id, ...binding?.visibilityIds ?? []];
1695
+ const options = binding?.selection?.options ?? [];
1696
+ const sources = slot.computed?.sourceSlotIds ?? [];
1697
+ add("answer", slot.slotId, [
1698
+ ...visibility.map((id) => edge("visibility", id, "control applicability")),
1699
+ ...options.map((id) => edge("visibility", id, "option projection")),
1700
+ ...slot.computed ? sources.map((id) => edge("answer", id, "computed source")) : [{
1701
+ key: storedKey,
1702
+ reason: "retained source"
1703
+ }]
1704
+ ], (read) => {
1705
+ if (visibility.some((id) => read(stateNodeKey("visibility", id)) !== true)) return void 0;
1706
+ if (slot.computed) return document.registry.get(element.node.elementType).compute(element.node, slot.slotId, (id) => {
1707
+ const value = read(stateNodeKey("answer", id));
1708
+ return typeof value === "boolean" ? void 0 : value;
1709
+ });
1710
+ const value = read(storedKey);
1711
+ if (typeof value === "boolean" || !value) return void 0;
1712
+ if (!binding?.selection) return value;
1713
+ if (value.type === "reference") return read(stateNodeKey("visibility", value.value)) === true ? value : void 0;
1714
+ if (value.type === "reference[]") {
1715
+ const active = value.value.filter((id) => read(stateNodeKey("visibility", id)) === true);
1716
+ return active.length || value.value.length === 0 ? {
1717
+ ...value,
1718
+ value: active
1719
+ } : void 0;
1720
+ }
1721
+ });
1722
+ }
1723
+ for (const followUp of element.facts.followUps) {
1724
+ const trigger = edge("visibility", followUp.triggerId, "trigger visibility");
1725
+ if ("selectionSlotId" in followUp) {
1726
+ const selected = edge("storedAnswer", followUp.selectionSlotId, "per-option checked stored selection");
1727
+ add("followUpActive", followUp.triggerId, [trigger, selected], (read) => {
1728
+ if (read(trigger.key) !== true) return false;
1729
+ const value = read(selected.key);
1730
+ return typeof value !== "boolean" && (value?.type === "reference" ? value.value === followUp.triggerId : value?.type === "reference[]" && value.value.includes(followUp.triggerId));
1731
+ });
1732
+ } else add("followUpActive", followUp.triggerId, [trigger, ...dependencies(followUp.expression)], (read, input, issues) => {
1733
+ if (read(trigger.key) !== true) return false;
1734
+ const result = evaluate(followUp.expression, read, input, issues, element.path);
1735
+ if (result && result.type !== "boolean") issues.push({
1736
+ code: "condition-type",
1737
+ path: element.path,
1738
+ message: "Follow-up activation must return a boolean",
1739
+ severity: "warning"
1740
+ });
1741
+ return result?.type === "boolean" && result.value;
1742
+ });
1743
+ }
1744
+ }
1745
+ if (draft) {
1746
+ const missing = /* @__PURE__ */ new Set();
1747
+ for (const node of this.nodes.values()) for (const dependency of node.dependencies) if (!this.nodes.has(dependency.key)) missing.add(dependency.key);
1748
+ for (const { expression } of document.expressions) for (const dependency of dependencies(expression)) if (!this.nodes.has(dependency.key)) missing.add(dependency.key);
1749
+ for (const key of missing) {
1750
+ const [type] = JSON.parse(key);
1751
+ const value = type === "answer" || type === "storedAnswer" ? void 0 : false;
1752
+ this.nodes.set(key, {
1753
+ key,
1754
+ dependencies: [],
1755
+ evaluate: () => value
1756
+ });
1757
+ }
1758
+ }
1759
+ const visited = /* @__PURE__ */ new Set();
1760
+ const visiting = [];
1761
+ const visit = (key, reason) => {
1762
+ if (visited.has(key)) return;
1763
+ const cycle = visiting.findIndex((node) => node.key === key);
1764
+ if (cycle >= 0) {
1765
+ diagnostics.push({
1766
+ code: "dependency-cycle",
1767
+ path: nodePaths.get(key) ?? [],
1768
+ message: "Dependency cycle: " + [...visiting.slice(cycle), {
1769
+ key,
1770
+ reason
1771
+ }].map((node) => `${node.key} (${node.reason})`).join(" → "),
1772
+ severity: "error"
1773
+ });
1774
+ return;
1775
+ }
1776
+ const node = this.nodes.get(key);
1777
+ if (!node) {
1778
+ diagnostics.push({
1779
+ code: "unknown-dependency",
1780
+ path: [],
1781
+ message: `Unknown dependency ${key} (${reason})`,
1782
+ severity: "error"
1783
+ });
1784
+ return;
1785
+ }
1786
+ visiting.push({
1787
+ key,
1788
+ reason
1789
+ });
1790
+ for (const dependency of node.dependencies) visit(dependency.key, dependency.reason);
1791
+ visiting.pop();
1792
+ visited.add(key);
1793
+ };
1794
+ for (const key of this.nodes.keys()) visit(key, "root");
1795
+ if (draft) for (const diagnostic of diagnostics.splice(0)) (diagnostic.kind === "reference" ? admitted : diagnostics).push(diagnostic);
1796
+ if (diagnostics.length) throw new DefinitionError([...diagnostics, ...admitted]);
1797
+ this.admittedDiagnostics = Object.freeze(admitted);
1798
+ const prefills = new Map([...document.elements.values()].flatMap((element) => Object.entries(element.node.prefills ?? {})));
1799
+ const initialization = [], initialized = /* @__PURE__ */ new Set(), initializing = /* @__PURE__ */ new Set();
1800
+ const initialize = (id) => {
1801
+ if (initialized.has(id)) return;
1802
+ if (initializing.has(id)) throw new DefinitionError([{
1803
+ code: "prefill-cycle",
1804
+ severity: "error",
1805
+ path: [],
1806
+ message: `Prefill dependency cycle through '${id}'`
1807
+ }]);
1808
+ initializing.add(id);
1809
+ for (const candidate of prefills.get(id) ?? []) {
1810
+ const expressions = [candidate.when, candidate.source.type === "expression" ? candidate.source.expression : candidate.source.type === "templateValue" ? ownValue(document.data.templateValues, candidate.source.key)?.expression : void 0];
1811
+ for (const expression of expressions) if (expression) {
1812
+ for (const dependency of this.storedDependencies(expression)) if (prefills.has(dependency)) initialize(dependency);
1813
+ }
1814
+ }
1815
+ initializing.delete(id);
1816
+ initialized.add(id);
1817
+ initialization.push(id);
1818
+ };
1819
+ prefills.forEach((_, id) => initialize(id));
1820
+ this.initializationOrder = Object.freeze(initialization);
1821
+ }
1822
+ evaluate(input) {
1823
+ const cache = /* @__PURE__ */ new Map();
1824
+ const diagnostics = [];
1825
+ const read = (key) => {
1826
+ if (cache.has(key)) return cache.get(key);
1827
+ const node = this.nodes.get(key);
1828
+ if (!node) throw new Error(`Unknown response graph node ${key}`);
1829
+ const result = node.evaluate((dependency) => {
1830
+ if (!node.dependencies.some((edge) => edge.key === dependency)) throw new Error(`Undeclared domain dependency ${key} → ${dependency}`);
1831
+ return read(dependency);
1832
+ }, input, diagnostics);
1833
+ cache.set(key, result);
1834
+ return result;
1835
+ };
1836
+ const answer = (id) => {
1837
+ const value = read(stateNodeKey("answer", id));
1838
+ return typeof value === "boolean" ? void 0 : value;
1839
+ };
1840
+ return {
1841
+ answer,
1842
+ stored: (id) => {
1843
+ const value = read(stateNodeKey("storedAnswer", id));
1844
+ return typeof value === "boolean" ? void 0 : value;
1845
+ },
1846
+ visible: (id) => read(stateNodeKey("visibility", id)) === true,
1847
+ disabled: (id) => read(stateNodeKey("disabled", id)) === true,
1848
+ followUpActive: (id) => read(stateNodeKey("followUpActive", id)) === true,
1849
+ activeAnswers: () => Object.fromEntries(this.document.slots.definitions.flatMap((slot) => {
1850
+ const value = answer(slot.slotId);
1851
+ return value === void 0 || slot.computed ? [] : [[slot.slotId, value]];
1852
+ })),
1853
+ get diagnostics() {
1854
+ return diagnostics.map((issue) => ({
1855
+ ...issue,
1856
+ path: [...issue.path]
1857
+ }));
1858
+ }
1859
+ };
1860
+ }
1861
+ storedDependencies(expression) {
1862
+ const result = /* @__PURE__ */ new Set();
1863
+ const visited = /* @__PURE__ */ new Set();
1864
+ const visit = (key) => {
1865
+ if (visited.has(key)) return;
1866
+ visited.add(key);
1867
+ const [type, id] = JSON.parse(key);
1868
+ if (type === "storedAnswer") result.add(id);
1869
+ const node = this.nodes.get(key);
1870
+ if (!node) throw new Error(`Unknown dependency ${key}`);
1871
+ node.dependencies.forEach((edge) => visit(edge.key));
1872
+ };
1873
+ collectExpressionDependencies(expression).forEach((ref) => visit(ref.kind === "answer" ? stateNodeKey("answer", ref.slotId) : stateNodeKey("disabled", ref.targetRef.ownerId)));
1874
+ return result;
1875
+ }
1876
+ };
1877
+ /** The same mechanical permissions power controls, batch writes, and validation eligibility. */
1878
+ function canWriteSelection(binding, before, after, snapshot) {
1879
+ if (!snapshot.visible(binding.controlId) || snapshot.disabled(binding.controlId) || binding.visibilityIds?.some((id) => !snapshot.visible(id) || snapshot.disabled(id))) return false;
1880
+ const selection = binding.selection;
1881
+ if (!selection) return true;
1882
+ const optionEnabled = (id) => selection.options.includes(id) && snapshot.visible(id) && !snapshot.disabled(id);
1883
+ if (!selection.multiple) {
1884
+ if (after?.type === "reference") return after.value === (before?.type === "reference" ? before.value : void 0) || optionEnabled(after.value);
1885
+ return after === void 0 && selection.allowClear !== false && (before?.type !== "reference" || optionEnabled(before.value));
1886
+ }
1887
+ if (after !== void 0 && after.type !== "reference[]") return false;
1888
+ const oldValues = before?.type === "reference[]" ? before.value : [];
1889
+ const values = after?.type === "reference[]" ? after.value : [];
1890
+ const activeCount = values.filter((id) => snapshot.visible(id)).length;
1891
+ const previousActiveCount = oldValues.filter((id) => snapshot.visible(id)).length;
1892
+ if (selection.maxSelection != null && activeCount > selection.maxSelection && activeCount >= previousActiveCount) return false;
1893
+ return [...oldValues.filter((id) => !values.includes(id)), ...values.filter((id) => !oldValues.includes(id))].every(optionEnabled);
1894
+ }
1895
+ function hasPermittedChange(binding, snapshot) {
1896
+ if (!snapshot.visible(binding.controlId) || snapshot.disabled(binding.controlId) || binding.visibilityIds?.some((id) => !snapshot.visible(id) || snapshot.disabled(id))) return false;
1897
+ if (!binding.selection) return true;
1898
+ const current = snapshot.stored(binding.slotId);
1899
+ const selection = binding.selection;
1900
+ if (!selection.multiple) {
1901
+ if (current !== void 0 && canWriteSelection(binding, current, void 0, snapshot)) return true;
1902
+ return selection.options.some((id) => (current?.type !== "reference" || current.value !== id) && canWriteSelection(binding, current, {
1903
+ type: "reference",
1904
+ value: id
1905
+ }, snapshot));
1906
+ }
1907
+ const values = current?.type === "reference[]" ? current.value : [];
1908
+ return selection.options.some((id) => canWriteSelection(binding, current, {
1909
+ type: "reference[]",
1910
+ value: values.includes(id) ? values.filter((value) => value !== id) : [...values, id]
1911
+ }, snapshot));
1912
+ }
1913
+ //#endregion
1914
+ //#region src/survey/responses/response-meta.ts
1915
+ const SurveyEventTypes = {
1916
+ openSurvey: "openSurvey",
1917
+ pageChanged: "pageChanged",
1918
+ responseChanged: "responseChanged",
1919
+ languageChanged: "languageChanged"
1920
+ };
1921
+ const surveyEventSchema = z.discriminatedUnion("type", [
1922
+ z.strictObject({
1923
+ type: z.literal("openSurvey"),
1924
+ timestamp: z.number().finite(),
1925
+ value: z.string()
1926
+ }),
1927
+ z.strictObject({
1928
+ type: z.literal("pageChanged"),
1929
+ timestamp: z.number().finite(),
1930
+ value: z.number().int().nonnegative()
1931
+ }),
1932
+ z.strictObject({
1933
+ type: z.literal("languageChanged"),
1934
+ timestamp: z.number().finite(),
1935
+ value: z.string()
1936
+ }),
1937
+ z.strictObject({
1938
+ type: z.literal("responseChanged"),
1939
+ timestamp: z.number().finite(),
1940
+ slotId: z.string().min(1),
1941
+ operation: z.enum(["set", "clear"])
1942
+ })
1943
+ ]);
1944
+ const itemResponseMetadataSchema = stringRecord(z.string().min(1), z.strictObject({ position: z.number().int().nonnegative() }));
1945
+ //#endregion
1946
+ //#region src/survey/responses/survey-response.ts
1947
+ const SURVEY_RESPONSE_SCHEMA_VERSION = 2;
1948
+ const surveyResponseSchema = z.strictObject({
1949
+ schemaVersion: z.literal(2),
1950
+ responseId: z.string().min(1),
1951
+ /** The exact definition the answers were given against; slot and option IDs are only meaningful with it. */
1952
+ definitionId: z.string().min(1),
1953
+ participantId: z.string().optional(),
1954
+ submittedAt: z.number().finite().optional(),
1955
+ answers: stringRecord(z.string().min(1), responseValueSchema),
1956
+ itemMetadata: itemResponseMetadataSchema.optional(),
1957
+ events: z.array(surveyEventSchema),
1958
+ /** Host-provided values stored and exported with the response, e.g. a tracking ID. */
1959
+ customValues: stringRecord(z.string().min(1), responseValueSchema).optional(),
1960
+ evaluationContext: z.strictObject({
1961
+ locale: z.string().min(1),
1962
+ currentDate: calendarDaySchema.optional(),
1963
+ participantFlags: stringRecord(z.string(), z.string()).optional()
1964
+ }).optional(),
1965
+ /** Reproduces item and option shuffles. */
1966
+ randomization: z.strictObject({ seed: z.string().min(1) }).optional()
1967
+ });
1968
+ /** Publication and schema association belong to the host, never to an answer address. */
1969
+ var SurveyResponse = class SurveyResponse {
1970
+ responseId;
1971
+ definitionId;
1972
+ participantId;
1973
+ submittedAt;
1974
+ answers = Object.create(null);
1975
+ itemMetadata = Object.create(null);
1976
+ events = [];
1977
+ customValues;
1978
+ evaluationContext;
1979
+ randomization;
1980
+ constructor(responseId, definitionId) {
1981
+ this.responseId = responseId;
1982
+ this.definitionId = definitionId;
1983
+ }
1984
+ serialize() {
1985
+ return structuredClone({
1986
+ schemaVersion: 2,
1987
+ responseId: this.responseId,
1988
+ definitionId: this.definitionId,
1989
+ participantId: this.participantId,
1990
+ submittedAt: this.submittedAt,
1991
+ answers: this.answers,
1992
+ itemMetadata: this.itemMetadata,
1993
+ events: this.events,
1994
+ customValues: this.customValues,
1995
+ evaluationContext: this.evaluationContext,
1996
+ randomization: this.randomization
1997
+ });
1998
+ }
1999
+ static deserialize(input) {
2000
+ const json = surveyResponseSchema.parse(input);
2001
+ const response = new SurveyResponse(json.responseId, json.definitionId);
2002
+ response.answers = json.answers;
2003
+ response.participantId = json.participantId;
2004
+ response.submittedAt = json.submittedAt;
2005
+ response.itemMetadata = json.itemMetadata ?? {};
2006
+ response.events = json.events;
2007
+ response.customValues = json.customValues;
2008
+ response.evaluationContext = json.evaluationContext;
2009
+ response.randomization = json.randomization;
2010
+ return response;
2011
+ }
2012
+ };
2013
+ //#endregion
2014
+ //#region src/response-exporter/types.ts
2015
+ /** Meta column keys in fixed order (common attributes first). */
2016
+ const META_COLUMN_ORDER = [
2017
+ "submittedAt",
2018
+ "participantId",
2019
+ "responseId",
2020
+ "sourceId"
2021
+ ];
2022
+ /** Built-in transform modes for slot export. */
2023
+ const SlotTransformMode = {
2024
+ default: "default",
2025
+ mask: "mask",
2026
+ dateFormat: "dateFormat"
2027
+ };
2028
+ const EXPORTER_PROFILE_SCHEMA_VERSION = 1;
2029
+ /** Source of an export column. */
2030
+ const ExportColumnSource = {
2031
+ meta: "meta",
2032
+ context: "context",
2033
+ response: "response"
2034
+ };
2035
+ //#endregion
2036
+ //#region src/response-exporter/column-id.ts
2037
+ function encodeExportColumnId(identity) {
2038
+ return JSON.stringify(identity);
2039
+ }
2040
+ function decodeExportColumnId(value) {
2041
+ const tuple = JSON.parse(value);
2042
+ if (!Array.isArray(tuple) || tuple.some((part) => typeof part !== "string") || !(tuple.length === 2 && (tuple[0] === "meta" || tuple[0] === "context") || tuple[0] === "response" && (tuple.length === 3 && tuple[2] === "value" || tuple.length === 4 && tuple[2] === "option"))) throw new Error(`Invalid export column ID: ${value}`);
2043
+ if (JSON.stringify(tuple) !== value) throw new Error("Export column IDs must use the canonical JSON tuple encoding");
2044
+ return tuple;
2045
+ }
2046
+ //#endregion
2047
+ //#region src/response-exporter/slot-formatter.ts
2048
+ /** Default transform config when no profile override. */
2049
+ const DEFAULT_TRANSFORM = { mode: SlotTransformMode.default };
2050
+ /**
2051
+ * Export a single response slot to a string. Pure function for unit testing.
2052
+ * Uses default formatting by ValueType unless overridden by transformConfig.
2053
+ */
2054
+ function exportSingleResponseSlot(params) {
2055
+ const { value, valueType, transformConfig } = params;
2056
+ if (value === void 0 || value === null) return "";
2057
+ const mode = transformConfig.mode;
2058
+ if (mode === SlotTransformMode.mask) return maskValue(value, transformConfig.maskText ?? "***");
2059
+ if (mode === SlotTransformMode.dateFormat && (valueType === ValueType.date || valueType === ValueType.dateArray)) return formatDateValue(value, transformConfig.dateFormat?.pattern ?? "yyyy-MM-dd");
2060
+ if (value.type === ValueType.reference || value.type === ValueType.referenceArray) {
2061
+ const keys = transformConfig.categorical === "labels" ? transformConfig.optionLabels : transformConfig.optionCodes ?? params.referenceValueKeys;
2062
+ const exportReference = (id) => {
2063
+ if (transformConfig.categorical === "ids") return id;
2064
+ const key = keys && Object.hasOwn(keys, id) ? keys[id] : void 0;
2065
+ if (typeof key !== "string" || !key.trim()) throw new Error(`Missing categorical export mapping for option "${id}"`);
2066
+ return key.trim();
2067
+ };
2068
+ return value.type === ValueType.reference ? exportReference(value.value) : JSON.stringify(value.value.map(exportReference));
2069
+ }
2070
+ return formatDefault(value);
2071
+ }
2072
+ const BooleanExportValues = {
2073
+ TRUE: "TRUE",
2074
+ FALSE: "FALSE"
2075
+ };
2076
+ function formatDefault(value) {
2077
+ switch (value.type) {
2078
+ case ValueType.string: return String(value.value);
2079
+ case ValueType.reference: return String(value.value);
2080
+ case ValueType.boolean: return value.value ? BooleanExportValues.TRUE : BooleanExportValues.FALSE;
2081
+ case ValueType.number: return value.precision === NumberPrecision.int ? String(Math.floor(value.value)) : String(value.value);
2082
+ case ValueType.date: return value.value;
2083
+ case ValueType.duration: {
2084
+ const d = value;
2085
+ return `${d.precision === NumberPrecision.int ? Math.floor(d.value) : d.value} ${d.unit}`;
2086
+ }
2087
+ case ValueType.stringArray: return value.value.join("; ");
2088
+ case ValueType.referenceArray: return value.value.join("; ");
2089
+ case ValueType.numberArray: {
2090
+ const arr = value.value;
2091
+ const prec = value.precision;
2092
+ const fmt = (n) => prec === NumberPrecision.int ? Math.floor(n) : n;
2093
+ return arr.map(fmt).join("; ");
2094
+ }
2095
+ case ValueType.dateArray: return value.value.join("; ");
2096
+ case ValueType.durationArray: {
2097
+ const d = value;
2098
+ const prec = d.precision === NumberPrecision.int;
2099
+ const fmt = (n) => prec ? Math.floor(n) : n;
2100
+ return d.value.map((v) => `${fmt(v)} ${d.unit}`).join("; ");
2101
+ }
2102
+ default: return String(value.value ?? "");
2103
+ }
2104
+ }
2105
+ function formatDateValue(value, pattern) {
2106
+ if (value.type === ValueType.date) return formatCalendarDate(value.value, pattern);
2107
+ if (value.type === ValueType.dateArray) return value.value.map((date) => formatCalendarDate(date, pattern)).join("; ");
2108
+ return formatDefault(value);
2109
+ }
2110
+ function maskValue(value, maskText) {
2111
+ return valueToString(value) ? maskText : "";
2112
+ }
2113
+ function valueToString(value) {
2114
+ switch (value.type) {
2115
+ case ValueType.string:
2116
+ case ValueType.reference: return String(value.value);
2117
+ case ValueType.stringArray:
2118
+ case ValueType.referenceArray: return value.value.join(" ");
2119
+ default: return String(value.value ?? "");
2120
+ }
2121
+ }
2122
+ //#endregion
2123
+ //#region src/response-exporter/projection.ts
2124
+ function optionIds(slot) {
2125
+ return [...new Set((slot.allowedValues ?? []).flatMap((value) => value.type === ValueType.reference ? [value.value] : value.type === ValueType.referenceArray ? value.value : []))];
2126
+ }
2127
+ function optionMapping(slot, config) {
2128
+ const ids = optionIds(slot);
2129
+ const encoding = config.categorical ?? "codes";
2130
+ const mapping = encoding === "ids" ? Object.fromEntries(ids.map((id) => [id, id])) : encoding === "labels" ? config.optionLabels ?? ownValue(slot.referenceValueLabels, config.locale ?? "en") ?? {} : config.optionCodes ?? slot.referenceValueKeys ?? {};
2131
+ const used = /* @__PURE__ */ new Set();
2132
+ for (const id of ids) {
2133
+ if (typeof mapping[id] !== "string" || !mapping[id].trim()) throw new Error(`Variable "${slot.variableName}" has no ${encoding} mapping for option "${id}"`);
2134
+ if (used.has(mapping[id])) throw new Error(`Variable "${slot.variableName}" has ambiguous ${encoding} mapping "${mapping[id]}"`);
2135
+ used.add(mapping[id]);
2136
+ }
2137
+ for (const id of [...Object.keys(config.optionCodes ?? {}), ...Object.keys(config.optionLabels ?? {})]) if (!ids.includes(id)) throw new Error(`Mapping for "${slot.variableName}" contains unknown option "${id}"`);
2138
+ return mapping;
2139
+ }
2140
+ /** Compile every declared role before applying inclusion settings. */
2141
+ function projectSlot(slot, config, allRoles = false) {
2142
+ if (![
2143
+ "default",
2144
+ "mask",
2145
+ "dateFormat"
2146
+ ].includes(config.mode)) throw new Error(`Unsupported export transform "${config.mode}"`);
2147
+ if (config.categorical && ![
2148
+ "codes",
2149
+ "labels",
2150
+ "ids"
2151
+ ].includes(config.categorical)) throw new Error("Unsupported categorical encoding");
2152
+ if (config.multiple && ![
2153
+ "json",
2154
+ "delimited",
2155
+ "indicators"
2156
+ ].includes(config.multiple)) throw new Error("Unsupported multiple-selection encoding");
2157
+ if (config.mode === "dateFormat" && slot.valueType !== "date" && slot.valueType !== "date[]") throw new Error("Date formatting requires a date variable");
2158
+ if (config.mode === "dateFormat") {
2159
+ const pattern = config.dateFormat?.pattern ?? "yyyy-MM-dd";
2160
+ if (!pattern.trim()) throw new Error("Date export pattern cannot be empty");
2161
+ formatCalendarDate("2026-01-15", pattern);
2162
+ }
2163
+ if (config.durationUnit && (!Object.values(DurationUnits).includes(config.durationUnit) || !slot.valueType.startsWith("duration"))) throw new Error("Duration units require a duration variable and a supported unit");
2164
+ if (config.booleanValues && (!config.booleanValues.true || !config.booleanValues.false || config.booleanValues.true === config.booleanValues.false)) throw new Error("Boolean export values must be distinct and nonempty");
2165
+ const categorical = slot.valueType === ValueType.reference || slot.valueType === ValueType.referenceArray;
2166
+ if (config.multiple && slot.valueType !== ValueType.referenceArray) throw new Error(`Multiple-selection encoding requires a reference[] variable: ${slot.variableName}`);
2167
+ if ((config.categorical || config.optionCodes || config.optionLabels) && !categorical) throw new Error(`Categorical encoding requires a categorical variable: ${slot.variableName}`);
2168
+ if (config.booleanValues && slot.valueType !== ValueType.boolean) throw new Error(`Boolean encoding requires a boolean variable: ${slot.variableName}`);
2169
+ if (config.precision !== void 0 && ![
2170
+ "number",
2171
+ "number[]",
2172
+ "duration",
2173
+ "duration[]"
2174
+ ].includes(slot.valueType)) throw new Error("Precision requires a numeric or duration variable");
2175
+ if (config.precision !== void 0 && (!Number.isInteger(config.precision) || config.precision < 0 || config.precision > 15)) throw new Error("Precision must be an integer between 0 and 15");
2176
+ if (config.multiple === "delimited" && (!config.delimiter || config.delimiter.includes("\"") || /[\r\n]/.test(config.delimiter))) throw new Error("Delimited selection export requires an explicit delimiter without quotes or line breaks");
2177
+ if (!allRoles && (config.include === false || slot.computed && config.include !== true)) return [];
2178
+ const mapping = categorical && !allRoles ? optionMapping(slot, config) : {};
2179
+ const base = {
2180
+ source: "response",
2181
+ itemId: slot.itemId,
2182
+ slotId: slot.slotId,
2183
+ valueType: slot.valueType
2184
+ };
2185
+ const scalar = {
2186
+ ...base,
2187
+ id: encodeExportColumnId([
2188
+ "response",
2189
+ slot.slotId,
2190
+ "value"
2191
+ ]),
2192
+ header: slot.variableName
2193
+ };
2194
+ const indicators = () => optionIds(slot).map((optionId) => ({
2195
+ ...base,
2196
+ id: encodeExportColumnId([
2197
+ "response",
2198
+ slot.slotId,
2199
+ "option",
2200
+ optionId
2201
+ ]),
2202
+ header: `${slot.variableName}_${mapping[optionId] ?? optionId}`,
2203
+ optionId
2204
+ }));
2205
+ return allRoles ? [scalar, ...slot.valueType === ValueType.referenceArray ? indicators() : []] : config.multiple === "indicators" ? indicators() : [scalar];
2206
+ }
2207
+ function formatProjectedValue(slot, column, value, config) {
2208
+ if (value === void 0) return "";
2209
+ if (config.mode === "mask") return config.maskText ?? "***";
2210
+ if (value.type === ValueType.reference || value.type === ValueType.referenceArray) {
2211
+ const mapping = optionMapping(slot, config);
2212
+ const values = value.type === ValueType.reference ? [value.value] : value.value;
2213
+ if (values.some((id) => !Object.hasOwn(mapping, id))) throw new Error(`Unmapped option in "${slot.variableName}"`);
2214
+ if (column.optionId) return values.includes(column.optionId) ? "1" : "0";
2215
+ const encoded = values.map((id) => mapping[id]);
2216
+ if (value.type === ValueType.reference) return encoded[0];
2217
+ if (config.multiple === "delimited") return encoded.map((entry) => `"${entry.replaceAll("\"", "\"\"")}"`).join(config.delimiter);
2218
+ return JSON.stringify(encoded);
2219
+ }
2220
+ if (value.type === ValueType.boolean) {
2221
+ const values = config.booleanValues ?? {
2222
+ true: "TRUE",
2223
+ false: "FALSE"
2224
+ };
2225
+ if (!values.true || !values.false || values.true === values.false) throw new Error("Boolean export values must be distinct and nonempty");
2226
+ return value.value ? values.true : values.false;
2227
+ }
2228
+ if (value.type === ValueType.duration || value.type === ValueType.durationArray) {
2229
+ const unit = config.durationUnit ?? "seconds";
2230
+ const seconds = {
2231
+ seconds: 1,
2232
+ minutes: 60,
2233
+ hours: 3600,
2234
+ days: 86400,
2235
+ weeks: 604800
2236
+ };
2237
+ if (unit !== value.unit && (!seconds[unit] || !seconds[value.unit])) throw new Error("Calendar months and years cannot be converted to fixed durations");
2238
+ const convert = (number) => {
2239
+ const n = number * (unit === value.unit ? 1 : seconds[value.unit] / seconds[unit]);
2240
+ return config.precision === void 0 ? String(n) : n.toFixed(config.precision);
2241
+ };
2242
+ return Array.isArray(value.value) ? JSON.stringify(value.value.map(convert)) : convert(value.value);
2243
+ }
2244
+ if (value.type === ValueType.number && config.precision !== void 0) return value.value.toFixed(config.precision);
2245
+ if (value.type === ValueType.numberArray && config.precision !== void 0) return `[${value.value.map((number) => number.toFixed(config.precision)).join(",")}]`;
2246
+ if (Array.isArray(value.value) && config.mode !== "dateFormat") return JSON.stringify(value.value);
2247
+ return exportSingleResponseSlot({
2248
+ value,
2249
+ valueType: slot.valueType,
2250
+ transformConfig: config
2251
+ });
2252
+ }
2253
+ /** Internal typed diagnostics let authoring distinguish conflicts from unfinished mappings. */
2254
+ function getDefaultProjectionDiagnostics(survey) {
2255
+ const issues = [];
2256
+ const headers = /* @__PURE__ */ new Set([
2257
+ "submittedAt",
2258
+ "participantId",
2259
+ "responseId",
2260
+ "sourceId"
2261
+ ]);
2262
+ const definitions = survey.slots.definitions;
2263
+ const slots = definitions.some((slot) => slot.exportSettings?.categorical === "labels") ? localizeExportSlots(survey, definitions) : definitions;
2264
+ for (const slot of slots) try {
2265
+ for (const column of projectSlot(slot, {
2266
+ mode: "default",
2267
+ ...slot.exportSettings
2268
+ })) {
2269
+ if (headers.has(column.header)) issues.push({
2270
+ code: "duplicate-header",
2271
+ header: column.header,
2272
+ message: `Duplicate export column header '${column.header}'`
2273
+ });
2274
+ headers.add(column.header);
2275
+ }
2276
+ } catch (error) {
2277
+ issues.push({
2278
+ code: "invalid-projection",
2279
+ slotId: slot.slotId,
2280
+ message: error instanceof Error ? error.message : String(error)
2281
+ });
2282
+ }
2283
+ return issues;
2284
+ }
2285
+ /** Collect draft diagnostics without stopping at the first unfinished variable. */
2286
+ function getDefaultProjectionIssues(survey) {
2287
+ return getDefaultProjectionDiagnostics(survey).map((issue) => issue.message);
2288
+ }
2289
+ /** Fully resolved output semantics, shared by compatibility checks and codebooks. */
2290
+ function resolvedSlotEncoding(slot, config) {
2291
+ const encoding = {
2292
+ mode: config.mode,
2293
+ include: config.include ?? !slot.computed
2294
+ };
2295
+ if (config.mode === "mask") return {
2296
+ ...encoding,
2297
+ maskText: config.maskText ?? "***"
2298
+ };
2299
+ if (config.mode === "dateFormat") encoding.dateFormat = { pattern: config.dateFormat?.pattern ?? "yyyy-MM-dd" };
2300
+ if (slot.valueType === "reference" || slot.valueType === "reference[]") {
2301
+ encoding.categorical = config.categorical ?? "codes";
2302
+ if (encoding.categorical === "labels") {
2303
+ encoding.locale = config.locale ?? "en";
2304
+ encoding.optionLabels = optionMapping(slot, config);
2305
+ } else if (encoding.categorical === "codes") encoding.optionCodes = optionMapping(slot, config);
2306
+ if (slot.valueType === "reference[]") {
2307
+ encoding.multiple = config.multiple ?? "json";
2308
+ if (encoding.multiple === "delimited") encoding.delimiter = config.delimiter;
2309
+ }
2310
+ }
2311
+ if (slot.valueType === "boolean") encoding.booleanValues = config.booleanValues ?? {
2312
+ true: "TRUE",
2313
+ false: "FALSE"
2314
+ };
2315
+ if (slot.valueType.startsWith("duration")) encoding.durationUnit = config.durationUnit ?? "seconds";
2316
+ if (config.precision !== void 0) encoding.precision = config.precision;
2317
+ return structuredClone(encoding);
2318
+ }
2319
+ /** Localization is projected from this exact revision, never cached in structural slot identities. */
2320
+ function localizedExportSlots(survey) {
2321
+ return localizeExportSlots(survey, survey.slots.definitions);
2322
+ }
2323
+ function localizeExportSlots(survey, definitions) {
2324
+ const fallbackLocale = survey.locales[0];
2325
+ return definitions.map((slot) => ({
2326
+ ...slot,
2327
+ referenceValueLabels: Object.fromEntries([.../* @__PURE__ */ new Set([...survey.locales, ...Object.keys(slot.referenceValueLabels ?? {})])].map((locale) => [locale, Object.fromEntries(optionIds(slot).flatMap((id) => {
2328
+ const label = survey.owners.get(id)?.roles.some((role) => role.role === "label") ? getContentPlainText(readLocalizationContent(survey, {
2329
+ scope: "content",
2330
+ ownerId: id,
2331
+ role: "label"
2332
+ }, locale) ?? (fallbackLocale ? readLocalizationContent(survey, {
2333
+ scope: "content",
2334
+ ownerId: id,
2335
+ role: "label"
2336
+ }, fallbackLocale) : void 0)).trim() : ownValue(ownValue(slot.referenceValueLabels, locale), id);
2337
+ return label ? [[id, label]] : [];
2338
+ }))]))
2339
+ }));
2340
+ }
2341
+ //#endregion
2342
+ export { shuffleArray as $, isAdmissibleInDraft as A, expressionIssues as B, surveyEventSchema as C, DefinitionError as D, hasPermittedChange as E, allocateResponseVariableName as F, responseValueElements as G, functionTypeIssues as H, isValidResponseVariableName as I, FUNCTION_SIGNATURES as J, responseValuesEqual as K, suggestResponseVariableName as L, getCompositionLabel as M, ResponseSlotRegistry as N, Survey as O, RESPONSE_VARIABLE_NAME_MAX_LENGTH as P, resolveUniqueItemKey as Q, ExpressionEvaluationError as R, itemResponseMetadataSchema as S, canWriteSelection as T, inferExpressionReturnType as U, expressionTypeIssues as V, invalidResponseDomainValues as W, generateCodingKey as X, createSeededRandom as Y, generateId as Z, SlotTransformMode as _, optionIds as a, surveyResponseSchema as b, resolvedSlotEncoding as c, exportSingleResponseSlot as d, shuffleIndices as et, decodeExportColumnId as f, META_COLUMN_ORDER as g, ExportColumnSource as h, localizedExportSlots as i, surveyLocales as j, checkDocumentBudget as k, BooleanExportValues as l, EXPORTER_PROFILE_SCHEMA_VERSION as m, getDefaultProjectionDiagnostics as n, optionMapping as o, encodeExportColumnId as p, validateResponseSlotValue as q, getDefaultProjectionIssues as r, projectSlot as s, formatProjectedValue as t, structuredCloneMethod as tt, DEFAULT_TRANSFORM as u, SURVEY_RESPONSE_SCHEMA_VERSION as v, SurveyStateGraph as w, SurveyEventTypes as x, SurveyResponse as y, evaluateExpression as z };
2343
+
2344
+ //# sourceMappingURL=projection-DrLPFroE.mjs.map