@case-framework/survey-core 0.7.0 → 0.8.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,3930 @@
1
+ import { format } from "date-fns/format";
2
+ //#region src/calendar-date.ts
3
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
4
+ const daysInMonth = (year, month) => month === 2 ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28 : [
5
+ 4,
6
+ 6,
7
+ 9,
8
+ 11
9
+ ].includes(month) ? 30 : 31;
10
+ function isCalendarDate(value) {
11
+ if (typeof value !== "string" || !/^\d{4}(?:-\d{2}(?:-\d{2})?)?$/.test(value)) return false;
12
+ const [year, month, day] = value.split("-").map(Number);
13
+ return year >= 1 && year <= 9999 && (month === void 0 || month >= 1 && month <= 12 && (day === void 0 || day >= 1 && day <= daysInMonth(year, month)));
14
+ }
15
+ function calendarDatePrecision(value) {
16
+ if (!isCalendarDate(value)) throw new Error(`Invalid calendar date: ${value}`);
17
+ return value.length === 4 ? "year" : value.length === 7 ? "year-month" : "year-month-day";
18
+ }
19
+ function calendarDateFromLocalDate(date, precision = "year-month-day") {
20
+ const value = [
21
+ pad(date.getFullYear(), 4),
22
+ pad(date.getMonth() + 1),
23
+ pad(date.getDate())
24
+ ].slice(0, precision === "year" ? 1 : precision === "year-month" ? 2 : 3).join("-");
25
+ if (!isCalendarDate(value)) throw new Error("Invalid calendar date");
26
+ return value;
27
+ }
28
+ /** Adapter for calendar widgets only. Never serialize this Date as an instant. */
29
+ function calendarDateToLocalDate(value) {
30
+ calendarDatePrecision(value);
31
+ const [year, month = 1, day = 1] = value.split("-").map(Number);
32
+ const date = /* @__PURE__ */ new Date(0);
33
+ date.setFullYear(year, month - 1, day);
34
+ date.setHours(12, 0, 0, 0);
35
+ return date;
36
+ }
37
+ function calendarDateBounds(value) {
38
+ calendarDatePrecision(value);
39
+ const [year, month] = value.split("-").map(Number);
40
+ if (value.length === 4) return [`${value}-01-01`, `${value}-12-31`];
41
+ if (value.length === 7) return [`${value}-01`, `${value}-${pad(daysInMonth(year, month))}`];
42
+ return [value, value];
43
+ }
44
+ /** Ordering requires equal precision; callers must explicitly choose interval semantics otherwise. */
45
+ function compareCalendarDates(a, b) {
46
+ if (!isCalendarDate(a) || !isCalendarDate(b) || a.length !== b.length) return void 0;
47
+ return a === b ? 0 : a < b ? -1 : 1;
48
+ }
49
+ function utcDate(value) {
50
+ const [year, month = 1, day = 1] = value.split("-").map(Number);
51
+ const date = /* @__PURE__ */ new Date(0);
52
+ date.setUTCFullYear(year, month - 1, day);
53
+ return date;
54
+ }
55
+ function addCalendarDate(value, amount, unit) {
56
+ if (!isCalendarDate(value) || !Number.isSafeInteger(amount)) return void 0;
57
+ if (amount === 0) return value;
58
+ if (unit === "days" && value.length !== 10 || unit === "months" && value.length < 7) return void 0;
59
+ const [year, month = 1, day = 1] = value.split("-").map(Number);
60
+ let result;
61
+ if (unit === "days") {
62
+ const date = utcDate(value);
63
+ date.setUTCDate(date.getUTCDate() + amount);
64
+ result = `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
65
+ } else {
66
+ const index = (year - 1) * 12 + month - 1 + amount * (unit === "years" ? 12 : 1);
67
+ const nextYear = Math.floor(index / 12) + 1;
68
+ const nextMonth = (index % 12 + 12) % 12 + 1;
69
+ result = `${pad(nextYear, 4)}-${pad(nextMonth)}-${pad(Math.min(day, daysInMonth(nextYear, nextMonth)))}`.slice(0, value.length);
70
+ }
71
+ return isCalendarDate(result) ? result : void 0;
72
+ }
73
+ /** Signed difference a - b in the value's precision unit (days, months, or years). */
74
+ function differenceCalendarDates(a, b) {
75
+ if (compareCalendarDates(a, b) === void 0) return void 0;
76
+ if (a.length === 10) return (utcDate(a).getTime() - utcDate(b).getTime()) / 864e5;
77
+ const [ay, am = 1] = a.split("-").map(Number);
78
+ const [by, bm = 1] = b.split("-").map(Number);
79
+ return a.length === 4 ? ay - by : (ay - by) * 12 + am - bm;
80
+ }
81
+ var CalendarFormattingDate = class extends Date {
82
+ getTimezoneOffset() {
83
+ return 0;
84
+ }
85
+ getFullYear() {
86
+ return this.getUTCFullYear();
87
+ }
88
+ getMonth() {
89
+ return this.getUTCMonth();
90
+ }
91
+ getDate() {
92
+ return this.getUTCDate();
93
+ }
94
+ getDay() {
95
+ return this.getUTCDay();
96
+ }
97
+ getHours() {
98
+ return this.getUTCHours();
99
+ }
100
+ getMinutes() {
101
+ return this.getUTCMinutes();
102
+ }
103
+ getSeconds() {
104
+ return this.getUTCSeconds();
105
+ }
106
+ getMilliseconds() {
107
+ return this.getUTCMilliseconds();
108
+ }
109
+ setFullYear(...args) {
110
+ return this.setUTCFullYear(...args);
111
+ }
112
+ setMonth(...args) {
113
+ return this.setUTCMonth(...args);
114
+ }
115
+ setDate(...args) {
116
+ return this.setUTCDate(...args);
117
+ }
118
+ setHours(...args) {
119
+ return this.setUTCHours(...args);
120
+ }
121
+ setMinutes(...args) {
122
+ return this.setUTCMinutes(...args);
123
+ }
124
+ setSeconds(...args) {
125
+ return this.setUTCSeconds(...args);
126
+ }
127
+ setMilliseconds(...args) {
128
+ return this.setUTCMilliseconds(...args);
129
+ }
130
+ };
131
+ /** Partial dates retain their precision even if a display pattern requests absent components. */
132
+ function formatCalendarDate(value, pattern) {
133
+ calendarDatePrecision(value);
134
+ if (value.length !== 10) return value;
135
+ return format(new CalendarFormattingDate(utcDate(value).getTime()), pattern);
136
+ }
137
+ //#endregion
138
+ //#region src/survey/responses/value-types.ts
139
+ const ValueType = {
140
+ string: "string",
141
+ duration: "duration",
142
+ reference: "reference",
143
+ number: "number",
144
+ boolean: "boolean",
145
+ date: "date",
146
+ stringArray: "string[]",
147
+ durationArray: "duration[]",
148
+ referenceArray: "reference[]",
149
+ numberArray: "number[]",
150
+ dateArray: "date[]"
151
+ };
152
+ const DurationUnits = {
153
+ seconds: "seconds",
154
+ minutes: "minutes",
155
+ hours: "hours",
156
+ days: "days",
157
+ weeks: "weeks",
158
+ months: "months",
159
+ years: "years"
160
+ };
161
+ const NumberPrecision = {
162
+ int: "int",
163
+ float: "float"
164
+ };
165
+ function isPlainObject(value) {
166
+ return value !== null && typeof value === "object" && !Array.isArray(value);
167
+ }
168
+ function isFiniteNumber(value) {
169
+ return typeof value === "number" && Number.isFinite(value);
170
+ }
171
+ function isStringArray(value) {
172
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
173
+ }
174
+ function isNumberArray(value) {
175
+ return Array.isArray(value) && value.every((item) => isFiniteNumber(item));
176
+ }
177
+ function isOptionalNumberPrecision(value) {
178
+ return value === void 0 || value === NumberPrecision.int || value === NumberPrecision.float;
179
+ }
180
+ function isDurationUnit(value) {
181
+ return typeof value === "string" && Object.values(DurationUnits).includes(value);
182
+ }
183
+ function isResponseValue(value) {
184
+ if (!isPlainObject(value) || typeof value.type !== "string") return false;
185
+ switch (value.type) {
186
+ case ValueType.string:
187
+ case ValueType.reference: return typeof value.value === "string";
188
+ case ValueType.number: return isFiniteNumber(value.value) && isOptionalNumberPrecision(value.precision);
189
+ case ValueType.boolean: return typeof value.value === "boolean";
190
+ case ValueType.date: return isCalendarDate(value.value);
191
+ case ValueType.duration: return isFiniteNumber(value.value) && isDurationUnit(value.unit) && isOptionalNumberPrecision(value.precision);
192
+ case ValueType.stringArray:
193
+ case ValueType.referenceArray: return isStringArray(value.value);
194
+ case ValueType.dateArray: return Array.isArray(value.value) && value.value.every(isCalendarDate);
195
+ case ValueType.numberArray: return isNumberArray(value.value) && (value.type !== ValueType.numberArray || isOptionalNumberPrecision(value.precision));
196
+ case ValueType.durationArray: return isNumberArray(value.value) && isDurationUnit(value.unit) && isOptionalNumberPrecision(value.precision);
197
+ default: return false;
198
+ }
199
+ }
200
+ function assertResponseValue(value, path) {
201
+ if (!isResponseValue(value)) throw new Error(`Invalid response value at '${path}'`);
202
+ }
203
+ //#endregion
204
+ //#region src/survey/responses/item-response.ts
205
+ /**
206
+ * SurveyItemResponse to store the response of a survey item.
207
+ */
208
+ var SurveyItemResponse = class SurveyItemResponse {
209
+ itemId;
210
+ meta;
211
+ response;
212
+ itemType;
213
+ constructor(itemDef, response) {
214
+ this.itemId = itemDef.itemId;
215
+ this.response = response;
216
+ this.itemType = itemDef.itemType;
217
+ }
218
+ serialize() {
219
+ return {
220
+ itemId: this.itemId,
221
+ itemType: this.itemType,
222
+ meta: this.meta,
223
+ response: this.response?.serialize()
224
+ };
225
+ }
226
+ static deserialize(json) {
227
+ if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey item response payload");
228
+ if (typeof json.itemId !== "string" || json.itemId.trim().length === 0) throw new Error("Survey item response is missing a valid itemId");
229
+ if (typeof json.itemType !== "string" || json.itemType.trim().length === 0) throw new Error("Survey item response is missing a valid itemType");
230
+ const itemDef = {
231
+ itemId: json.itemId,
232
+ itemType: json.itemType
233
+ };
234
+ const response = json.response ? ResponseItem.deserialize(json.response) : void 0;
235
+ const newResponse = new SurveyItemResponse(itemDef, response);
236
+ newResponse.meta = json.meta;
237
+ return newResponse;
238
+ }
239
+ };
240
+ var ResponseItem = class ResponseItem {
241
+ _slots;
242
+ constructor(initialValues) {
243
+ this._slots = new Map(initialValues ?? []);
244
+ }
245
+ get(slotId) {
246
+ return this._slots.get(slotId);
247
+ }
248
+ setSlotValue(slotId, slotResponse) {
249
+ if (slotResponse) this._slots.set(slotId, slotResponse);
250
+ else this._slots.delete(slotId);
251
+ }
252
+ serialize() {
253
+ if (this._slots.size === 0) return;
254
+ return { slots: Object.fromEntries(this._slots.entries()) };
255
+ }
256
+ isEmpty() {
257
+ return this._slots.size === 0;
258
+ }
259
+ clone() {
260
+ return new ResponseItem(Array.from(this._slots.entries()));
261
+ }
262
+ static deserialize(json) {
263
+ if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid response item payload");
264
+ const { slots } = json;
265
+ if (!slots || typeof slots !== "object" || Array.isArray(slots)) throw new Error("Invalid response slots payload");
266
+ const slotValues = Object.entries(slots).map(([slotId, slotResponse]) => {
267
+ if (slotId.trim().length === 0) throw new Error("Response slot id cannot be empty");
268
+ assertResponseValue(slotResponse, `response.slots.${slotId}`);
269
+ return [slotId, slotResponse];
270
+ });
271
+ return new ResponseItem(slotValues);
272
+ }
273
+ };
274
+ //#endregion
275
+ //#region src/survey/responses/survey-response.ts
276
+ const SURVEY_RESPONSE_SCHEMA_VERSION = 2;
277
+ /** Publication and schema association belong to the host, never to an answer address. */
278
+ var SurveyResponse = class SurveyResponse {
279
+ key;
280
+ participantId;
281
+ submittedAt;
282
+ answers = Object.create(null);
283
+ itemMetadata = Object.create(null);
284
+ events = [];
285
+ context;
286
+ evaluationContext;
287
+ constructor(key) {
288
+ this.key = key;
289
+ }
290
+ /** Collect an engine item view into a flat submission, rejecting ambiguous ownership. */
291
+ addItemResponse(item) {
292
+ const slots = item.response?.serialize()?.slots ?? {};
293
+ for (const slotId of Object.keys(slots)) if (Object.hasOwn(this.answers, slotId)) throw new Error(`Duplicate submission slot: ${slotId}`);
294
+ Object.assign(this.answers, structuredClone(slots));
295
+ if (item.meta) this.itemMetadata[item.itemId] = structuredClone(item.meta);
296
+ }
297
+ serialize() {
298
+ return structuredClone({
299
+ schemaVersion: 2,
300
+ key: this.key,
301
+ participantId: this.participantId,
302
+ submittedAt: this.submittedAt,
303
+ answers: this.answers,
304
+ itemMetadata: this.itemMetadata,
305
+ events: this.events,
306
+ context: this.context,
307
+ evaluationContext: this.evaluationContext
308
+ });
309
+ }
310
+ static deserialize(json) {
311
+ if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey response payload");
312
+ if (json.schemaVersion !== 2) throw new Error(`Unsupported survey response schema version: ${String(json.schemaVersion)}`);
313
+ if (typeof json.key !== "string" || !json.key.trim()) throw new Error("Survey response is missing a valid key");
314
+ const readValues = (value, name) => {
315
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid ${name} object`);
316
+ const values = Object.create(null);
317
+ for (const [key, entry] of Object.entries(value)) {
318
+ if (!key.trim()) throw new Error(`Empty ${name} key`);
319
+ assertResponseValue(entry, `${name}.${key}`);
320
+ values[key] = structuredClone(entry);
321
+ }
322
+ return values;
323
+ };
324
+ const response = new SurveyResponse(json.key);
325
+ response.answers = readValues(json.answers, "answers");
326
+ response.participantId = json.participantId;
327
+ response.submittedAt = json.submittedAt;
328
+ if (json.itemMetadata !== void 0 && (!json.itemMetadata || typeof json.itemMetadata !== "object" || Array.isArray(json.itemMetadata))) throw new Error("Invalid itemMetadata object");
329
+ response.itemMetadata = structuredClone(json.itemMetadata ?? {});
330
+ if (!Array.isArray(json.events)) throw new Error("Invalid response events");
331
+ response.events = structuredClone(json.events);
332
+ if (json.context !== void 0) response.context = readValues(json.context, "context");
333
+ if (json.evaluationContext !== void 0) {
334
+ const context = json.evaluationContext;
335
+ if (!context || typeof context !== "object" || typeof context.locale !== "string" || !context.locale || context.participantFlags !== void 0 && (!context.participantFlags || typeof context.participantFlags !== "object" || Array.isArray(context.participantFlags) || Object.values(context.participantFlags).some((value) => typeof value !== "string"))) throw new Error("Invalid response evaluation context");
336
+ response.evaluationContext = structuredClone(context);
337
+ }
338
+ return response;
339
+ }
340
+ };
341
+ //#endregion
342
+ //#region src/survey/responses/utils.ts
343
+ const initValueForType = (returnType) => {
344
+ switch (returnType) {
345
+ case ValueType.string: return {
346
+ type: ValueType.string,
347
+ value: ""
348
+ };
349
+ case ValueType.number: return {
350
+ type: ValueType.number,
351
+ value: 0
352
+ };
353
+ case ValueType.boolean: return {
354
+ type: ValueType.boolean,
355
+ value: false
356
+ };
357
+ case ValueType.date: return {
358
+ type: ValueType.date,
359
+ value: calendarDateFromLocalDate(/* @__PURE__ */ new Date())
360
+ };
361
+ case ValueType.stringArray: return {
362
+ type: ValueType.stringArray,
363
+ value: []
364
+ };
365
+ case ValueType.numberArray: return {
366
+ type: ValueType.numberArray,
367
+ value: []
368
+ };
369
+ case ValueType.dateArray: return {
370
+ type: ValueType.dateArray,
371
+ value: []
372
+ };
373
+ case ValueType.duration: return {
374
+ type: ValueType.duration,
375
+ value: 0,
376
+ unit: DurationUnits.seconds
377
+ };
378
+ case ValueType.reference: return {
379
+ type: ValueType.reference,
380
+ value: ""
381
+ };
382
+ case ValueType.durationArray: return {
383
+ type: ValueType.durationArray,
384
+ value: [],
385
+ unit: DurationUnits.seconds
386
+ };
387
+ case ValueType.referenceArray: return {
388
+ type: ValueType.referenceArray,
389
+ value: []
390
+ };
391
+ default: throw new Error("Invalid return type: " + returnType);
392
+ }
393
+ };
394
+ //#endregion
395
+ //#region src/survey/responses/value-domain.ts
396
+ /** Finite domains describe individual selectable values, including for array slots. */
397
+ function responseValueElements(value) {
398
+ if (!Array.isArray(value.value)) return [value];
399
+ const type = value.type.slice(0, -2);
400
+ return value.value.map((entry) => ({
401
+ ...value,
402
+ type,
403
+ value: entry
404
+ }));
405
+ }
406
+ function responseValuesEqual(left, right) {
407
+ return left.type === right.type && JSON.stringify(left.value) === JSON.stringify(right.value) && (!("unit" in left) || "unit" in right && left.unit === right.unit);
408
+ }
409
+ /** No coercion: an export code or label is never a categorical option identity. */
410
+ function invalidResponseDomainValues(value, allowedValues) {
411
+ const domain = allowedValues.flatMap(responseValueElements);
412
+ return responseValueElements(value).filter((entry) => !domain.some((allowed) => responseValuesEqual(entry, allowed)));
413
+ }
414
+ function validateResponseSlotValue(slot, value) {
415
+ if (!isResponseValue(value)) return [`Slot "${slot.slotId}" has an invalid typed value.`];
416
+ if (value.type !== slot.valueType) return [`Slot "${slot.slotId}" requires ${slot.valueType}, received ${value.type}.`];
417
+ if (!slot.allowedValues) return [];
418
+ return invalidResponseDomainValues(value, slot.allowedValues).map((entry) => `Slot "${slot.slotId}" contains invalid value ${JSON.stringify(entry)}. Allowed values: ${JSON.stringify(slot.allowedValues)}.`);
419
+ }
420
+ //#endregion
421
+ //#region src/survey/responses/variable-naming.ts
422
+ const RESPONSE_VARIABLE_NAME_MAX_LENGTH = 32;
423
+ const isValidResponseVariableName = (name) => name.length <= 32 && /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(name);
424
+ /** Generate an initial suggestion; explicit rename operations validate instead of coercing. */
425
+ function suggestResponseVariableName(text, fallback = "response") {
426
+ 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, "");
427
+ return ((/^[a-z]/.test(normalized) ? normalized : `${fallback}_${normalized}`) || fallback).slice(0, 32).replace(/_+$/g, "");
428
+ }
429
+ function allocateResponseVariableName(base, used, maxLength = 32) {
430
+ if (!isValidResponseVariableName(base)) throw new Error(`Invalid response variable name "${base}"; use lowercase snake_case, starting with a letter, at most 32 characters`);
431
+ if (base.length <= maxLength && !used.has(base)) return base;
432
+ const indexed = /^(.*)_([1-9][0-9]*)$/.exec(base);
433
+ const stemBase = indexed ? indexed[1] : base;
434
+ const start = indexed ? BigInt(indexed[2]) + 1n : 2n;
435
+ for (let n = start;; n++) {
436
+ const suffix = `_${n}`;
437
+ const stem = stemBase.slice(0, Math.max(0, maxLength - suffix.length)).replace(/_+$/g, "");
438
+ if (!stem) throw new Error("The variable naming suffix leaves no room for a name");
439
+ const name = `${stem}${suffix}`;
440
+ if (!used.has(name)) return name;
441
+ }
442
+ }
443
+ //#endregion
444
+ //#region src/survey/responses/slot-registry.ts
445
+ /** A detached, derived index. Ambiguous identities never resolve to an arbitrary owner. */
446
+ var ResponseSlotRegistry = class {
447
+ slots = /* @__PURE__ */ new Map();
448
+ diagnostics = [];
449
+ constructor(declarations) {
450
+ const seen = /* @__PURE__ */ new Set();
451
+ for (const declaration of declarations) for (const definition of declaration.slots) {
452
+ const slot = structuredClone({
453
+ ...definition,
454
+ itemId: declaration.itemId,
455
+ primitiveId: definition.primitiveId ?? declaration.itemId
456
+ });
457
+ if (!slot.slotId.trim() || !slot.primitiveId.trim() || !slot.itemId.trim() || !Object.values(ValueType).includes(slot.valueType)) {
458
+ this.diagnostics.push({
459
+ code: "invalid-slot",
460
+ slotId: slot.slotId,
461
+ itemId: slot.itemId,
462
+ message: "Response slots require nonempty identities and a supported value type."
463
+ });
464
+ continue;
465
+ }
466
+ if (seen.has(slot.slotId)) {
467
+ this.slots.delete(slot.slotId);
468
+ this.diagnostics.push({
469
+ code: "duplicate-slot-id",
470
+ slotId: slot.slotId,
471
+ itemId: slot.itemId,
472
+ message: `Slot "${slot.slotId}" is declared more than once. Allocate unique persistent identities before using this schema.`
473
+ });
474
+ continue;
475
+ }
476
+ seen.add(slot.slotId);
477
+ this.slots.set(slot.slotId, slot);
478
+ const elementType = slot.valueType.endsWith("[]") ? slot.valueType.slice(0, -2) : slot.valueType;
479
+ if (slot.allowedValues?.some((value) => !isResponseValue(value) || responseValueElements(value).some((entry) => entry.type !== elementType))) this.diagnostics.push({
480
+ code: "invalid-domain",
481
+ slotId: slot.slotId,
482
+ itemId: slot.itemId,
483
+ message: `The allowed values for slot "${slot.slotId}" must have type ${elementType}.`
484
+ });
485
+ 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({
486
+ code: "invalid-domain",
487
+ slotId: slot.slotId,
488
+ itemId: slot.itemId,
489
+ message: `Scores for slot "${slot.slotId}" must map allowed option IDs to finite numbers.`
490
+ });
491
+ }
492
+ const names = /* @__PURE__ */ new Map();
493
+ for (const slot of this.slots.values()) {
494
+ if (typeof slot.variableName !== "string" || !isValidResponseVariableName(slot.variableName)) this.diagnostics.push({
495
+ code: "invalid-variable-name",
496
+ slotId: slot.slotId,
497
+ itemId: slot.itemId,
498
+ message: `Variable "${slot.variableName}" must use lowercase snake_case, start with a letter and contain at most 32 characters.`
499
+ });
500
+ const previous = names.get(slot.variableName);
501
+ if (previous) this.diagnostics.push({
502
+ code: "duplicate-variable-name",
503
+ slotId: slot.slotId,
504
+ itemId: slot.itemId,
505
+ message: `Variable name "${slot.variableName}" is already used by slot "${previous}".`
506
+ });
507
+ names.set(slot.variableName, slot.slotId);
508
+ }
509
+ for (const slot of this.slots.values()) for (const dependency of slot.computed?.sourceSlotIds ?? []) if (!this.slots.has(dependency)) this.diagnostics.push({
510
+ code: "unknown-dependency",
511
+ slotId: slot.slotId,
512
+ itemId: slot.itemId,
513
+ message: `Computed slot "${slot.slotId}" depends on unknown or ambiguous slot "${dependency}".`
514
+ });
515
+ const active = /* @__PURE__ */ new Set();
516
+ const visited = /* @__PURE__ */ new Set();
517
+ const visit = (id) => {
518
+ if (active.has(id)) {
519
+ this.diagnostics.push({
520
+ code: "computed-cycle",
521
+ slotId: id,
522
+ message: `Computed slot "${id}" participates in a dependency cycle.`
523
+ });
524
+ return;
525
+ }
526
+ if (visited.has(id)) return;
527
+ active.add(id);
528
+ for (const dependency of this.slots.get(id)?.computed?.sourceSlotIds ?? []) visit(dependency);
529
+ active.delete(id);
530
+ visited.add(id);
531
+ };
532
+ for (const id of this.slots.keys()) visit(id);
533
+ }
534
+ get(slotId) {
535
+ const slot = this.slots.get(slotId);
536
+ return slot && structuredClone(slot);
537
+ }
538
+ get definitions() {
539
+ return structuredClone([...this.slots.values()]);
540
+ }
541
+ assertValid() {
542
+ if (this.diagnostics.length) throw new Error(this.diagnostics.map((entry) => entry.message).join("\n"));
543
+ }
544
+ validateAnswers(answers) {
545
+ const diagnostics = [...this.diagnostics];
546
+ for (const [slotId, value] of Object.entries(answers)) {
547
+ const slot = this.slots.get(slotId);
548
+ if (!slot) {
549
+ diagnostics.push({
550
+ code: "unknown-slot",
551
+ slotId,
552
+ message: `Answer references unknown or ambiguous slot "${slotId}".`
553
+ });
554
+ continue;
555
+ }
556
+ const issues = slot.computed ? [`Computed slot "${slotId}" cannot contain a stored answer.`] : validateResponseSlotValue(slot, value);
557
+ for (const message of issues) diagnostics.push({
558
+ code: "invalid-answer",
559
+ slotId,
560
+ itemId: slot.itemId,
561
+ message
562
+ });
563
+ }
564
+ return diagnostics;
565
+ }
566
+ };
567
+ //#endregion
568
+ //#region src/survey/utils/value-reference.ts
569
+ let ValueReferenceMethod = /* @__PURE__ */ function(ValueReferenceMethod) {
570
+ ValueReferenceMethod["get"] = "get";
571
+ ValueReferenceMethod["isDefined"] = "isDefined";
572
+ return ValueReferenceMethod;
573
+ }({});
574
+ /** Slot identity is opaque. String keys are an internal JSON-tuple lookup codec. */
575
+ var ValueReference = class ValueReference {
576
+ slotId;
577
+ name;
578
+ constructor(reference) {
579
+ let value = reference;
580
+ if (typeof reference === "string") {
581
+ const tuple = JSON.parse(reference);
582
+ if (!Array.isArray(tuple) || tuple.length !== 2) throw new Error("Invalid response-reference lookup key");
583
+ value = {
584
+ slotId: tuple[0],
585
+ method: tuple[1]
586
+ };
587
+ }
588
+ const parsed = value;
589
+ if (!parsed || typeof parsed.slotId !== "string" || !parsed.slotId.trim() || parsed.method !== "get" && parsed.method !== "isDefined") throw new Error("Response references require a slotId and get or isDefined method");
590
+ this.slotId = parsed.slotId;
591
+ this.name = parsed.method;
592
+ }
593
+ serialize() {
594
+ return {
595
+ slotId: this.slotId,
596
+ method: this.name
597
+ };
598
+ }
599
+ toString() {
600
+ return JSON.stringify([this.slotId, this.name]);
601
+ }
602
+ static fromParts(slotId, method) {
603
+ return new ValueReference({
604
+ slotId,
605
+ method
606
+ });
607
+ }
608
+ };
609
+ let ReferenceUsageType = /* @__PURE__ */ function(ReferenceUsageType) {
610
+ ReferenceUsageType["displayConditions"] = "displayConditions";
611
+ ReferenceUsageType["templateValues"] = "templateValues";
612
+ ReferenceUsageType["validations"] = "validations";
613
+ ReferenceUsageType["disabledConditions"] = "disabledConditions";
614
+ ReferenceUsageType["prefills"] = "prefills";
615
+ return ReferenceUsageType;
616
+ }({});
617
+ //#endregion
618
+ //#region src/expressions/expression.ts
619
+ const ExpressionType = {
620
+ Const: "const",
621
+ ResponseVariable: "responseVariable",
622
+ ContextVariable: "contextVariable",
623
+ Function: "function"
624
+ };
625
+ const ContextVariableType = {
626
+ Locale: "locale",
627
+ ParticipantFlag: "participantFlag",
628
+ CustomValue: "customValue",
629
+ CustomExpression: "customExpression"
630
+ };
631
+ /**
632
+ * Base class for all expressions.
633
+ */
634
+ var Expression = class Expression {
635
+ type;
636
+ editorConfig;
637
+ constructor(type, editorConfig) {
638
+ this.type = type;
639
+ this.editorConfig = editorConfig;
640
+ }
641
+ static deserialize(json) {
642
+ if (!json) return;
643
+ switch (json.type) {
644
+ case ExpressionType.Const: return ConstExpression.deserialize(json);
645
+ case ExpressionType.ResponseVariable: return ResponseVariableExpression.deserialize(json);
646
+ case ExpressionType.ContextVariable: return ContextVariableExpression.deserialize(json);
647
+ case ExpressionType.Function: return FunctionExpression.deserialize(json);
648
+ }
649
+ }
650
+ clone() {
651
+ return Expression.deserialize(this.serialize()) ?? (() => {
652
+ throw new Error("Failed to clone expression");
653
+ })();
654
+ }
655
+ };
656
+ var ConstExpression = class ConstExpression extends Expression {
657
+ value;
658
+ constructor(value, editorConfig) {
659
+ super(ExpressionType.Const, editorConfig);
660
+ if (value?.type === "date" || value?.type === "date[]") assertResponseValue(value, "expression.value");
661
+ this.value = value;
662
+ this.type = ExpressionType.Const;
663
+ }
664
+ static deserialize(json) {
665
+ if (json.type !== ExpressionType.Const) throw new Error("Invalid expression type: " + json.type);
666
+ return new ConstExpression(json.value, json.editorConfig);
667
+ }
668
+ get responseVariableRefs() {
669
+ return [];
670
+ }
671
+ serialize() {
672
+ return {
673
+ type: this.type,
674
+ value: this.value,
675
+ editorConfig: this.editorConfig
676
+ };
677
+ }
678
+ updateItemKeyReferences(_oldItemKey, _newItemKey) {
679
+ return false;
680
+ }
681
+ };
682
+ var ResponseVariableExpression = class ResponseVariableExpression extends Expression {
683
+ variableRef;
684
+ constructor(variableRef, editorConfig) {
685
+ super(ExpressionType.ResponseVariable, editorConfig);
686
+ this.variableRef = new ValueReference(variableRef).toString();
687
+ this.type = ExpressionType.ResponseVariable;
688
+ }
689
+ static deserialize(json) {
690
+ if (json.type !== ExpressionType.ResponseVariable) throw new Error("Invalid expression type: " + json.type);
691
+ if (typeof json.variableRef !== "object") throw new Error("Response expressions require a structured slot reference");
692
+ return new ResponseVariableExpression(json.variableRef, json.editorConfig);
693
+ }
694
+ get responseVariableRefs() {
695
+ return [new ValueReference(this.variableRef)];
696
+ }
697
+ get responseVariableRef() {
698
+ return new ValueReference(this.variableRef);
699
+ }
700
+ serialize() {
701
+ return {
702
+ type: this.type,
703
+ variableRef: this.responseVariableRef.serialize(),
704
+ editorConfig: this.editorConfig
705
+ };
706
+ }
707
+ };
708
+ var ContextVariableExpression = class ContextVariableExpression extends Expression {
709
+ contextType;
710
+ key;
711
+ arguments;
712
+ asType;
713
+ constructor(contextType, key, args, asType, editorConfig) {
714
+ super(ExpressionType.ContextVariable, editorConfig);
715
+ this.type = ExpressionType.ContextVariable;
716
+ this.contextType = contextType;
717
+ this.key = key;
718
+ this.arguments = args;
719
+ this.asType = asType;
720
+ }
721
+ static deserialize(json) {
722
+ if (json.type !== ExpressionType.ContextVariable) throw new Error("Invalid expression type: " + json.type);
723
+ return new ContextVariableExpression(json.contextType, Expression.deserialize(json.key), json.arguments?.map((arg) => Expression.deserialize(arg)), json.asType, json.editorConfig);
724
+ }
725
+ get responseVariableRefs() {
726
+ return [this.key, ...this.arguments ?? []].flatMap((arg) => arg?.responseVariableRefs ?? []);
727
+ }
728
+ serialize() {
729
+ return {
730
+ type: this.type,
731
+ contextType: this.contextType,
732
+ key: this.key?.serialize(),
733
+ arguments: this.arguments?.map((arg) => arg?.serialize()),
734
+ asType: this.asType,
735
+ editorConfig: this.editorConfig
736
+ };
737
+ }
738
+ };
739
+ let FunctionExpressionNames = /* @__PURE__ */ function(FunctionExpressionNames) {
740
+ FunctionExpressionNames["and"] = "and";
741
+ FunctionExpressionNames["or"] = "or";
742
+ FunctionExpressionNames["not"] = "not";
743
+ FunctionExpressionNames["list_contains"] = "list_contains";
744
+ FunctionExpressionNames["eq"] = "eq";
745
+ FunctionExpressionNames["gt"] = "gt";
746
+ FunctionExpressionNames["gte"] = "gte";
747
+ FunctionExpressionNames["lt"] = "lt";
748
+ FunctionExpressionNames["lte"] = "lte";
749
+ FunctionExpressionNames["in_range"] = "in_range";
750
+ FunctionExpressionNames["sum"] = "sum";
751
+ FunctionExpressionNames["min"] = "min";
752
+ FunctionExpressionNames["max"] = "max";
753
+ FunctionExpressionNames["str_eq"] = "str_eq";
754
+ FunctionExpressionNames["date_eq"] = "date_eq";
755
+ FunctionExpressionNames["date_gt"] = "date_gt";
756
+ FunctionExpressionNames["date_gte"] = "date_gte";
757
+ FunctionExpressionNames["date_lt"] = "date_lt";
758
+ FunctionExpressionNames["date_lte"] = "date_lte";
759
+ FunctionExpressionNames["date_add_days"] = "date_add_days";
760
+ FunctionExpressionNames["date_add_months"] = "date_add_months";
761
+ FunctionExpressionNames["date_add_years"] = "date_add_years";
762
+ FunctionExpressionNames["date_diff"] = "date_diff";
763
+ FunctionExpressionNames["date_min"] = "date_min";
764
+ FunctionExpressionNames["date_max"] = "date_max";
765
+ return FunctionExpressionNames;
766
+ }({});
767
+ var FunctionExpression = class FunctionExpression extends Expression {
768
+ functionName;
769
+ arguments;
770
+ constructor(functionName, args, editorConfig) {
771
+ super(ExpressionType.Function);
772
+ this.type = ExpressionType.Function;
773
+ this.functionName = functionName;
774
+ this.arguments = args;
775
+ this.editorConfig = editorConfig;
776
+ }
777
+ static deserialize(json) {
778
+ if (json.type !== ExpressionType.Function) throw new Error("Invalid expression type: " + json.type);
779
+ const functionName = json.functionName;
780
+ if (!Object.values(FunctionExpressionNames).includes(functionName)) throw new Error("Invalid function name: " + functionName);
781
+ const expr = new FunctionExpression(functionName, json.arguments.map((arg) => Expression.deserialize(arg)));
782
+ expr.editorConfig = json.editorConfig;
783
+ return expr;
784
+ }
785
+ get responseVariableRefs() {
786
+ const refStrings = this.arguments.flatMap((arg) => arg?.responseVariableRefs).filter((ref) => ref !== void 0).map((ref) => ref.toString());
787
+ return [...new Set(refStrings)].map((ref) => new ValueReference(ref));
788
+ }
789
+ serialize() {
790
+ return {
791
+ type: this.type,
792
+ functionName: this.functionName,
793
+ arguments: this.arguments.map((arg) => arg?.serialize()),
794
+ editorConfig: this.editorConfig
795
+ };
796
+ }
797
+ };
798
+ //#endregion
799
+ //#region src/expressions/expression-evaluator.ts
800
+ var ExpressionEvaluator = class {
801
+ context;
802
+ constructor(context) {
803
+ this.context = context;
804
+ }
805
+ eval(expression) {
806
+ if (expression === void 0) return;
807
+ switch (expression.type) {
808
+ case ExpressionType.Const: return this.evaluateConst(expression);
809
+ case ExpressionType.Function: return this.evaluateFunction(expression);
810
+ case ExpressionType.ResponseVariable: return this.evaluateResponseVariable(expression);
811
+ case ExpressionType.ContextVariable: return this.evaluateContextVariable(expression);
812
+ default: throw new Error(`Unsupported expression type: ${expression.type}`);
813
+ }
814
+ }
815
+ setContext(context) {
816
+ this.context = context;
817
+ }
818
+ evaluateConst(expression) {
819
+ return expression.value;
820
+ }
821
+ resolveStoredValue(slotId) {
822
+ if (this.context?.resolveResponseValue) return this.context.resolveResponseValue(slotId);
823
+ const values = Object.values(this.context?.responses ?? {}).map((item) => item.response?.get(slotId)).filter((value) => value !== void 0);
824
+ if (values.length > 1) throw new Error(`Ambiguous response slot "${slotId}"`);
825
+ return values[0];
826
+ }
827
+ evaluateResponseVariable(expression) {
828
+ const varRef = expression.responseVariableRef;
829
+ switch (varRef.name) {
830
+ case "get": return this.context?.resolveComputedValue?.(varRef.slotId) ?? this.resolveStoredValue(varRef.slotId);
831
+ case "isDefined": return {
832
+ type: ValueType.boolean,
833
+ value: (this.context?.resolveComputedValue?.(varRef.slotId) ?? this.resolveStoredValue(varRef.slotId)) !== void 0
834
+ };
835
+ default: throw new Error(`Unsupported value reference method: ${varRef.name}`);
836
+ }
837
+ }
838
+ evaluateFunction(expression) {
839
+ switch (expression.functionName) {
840
+ case "and": return this.evaluateAnd(expression);
841
+ case "or": return this.evaluateOr(expression);
842
+ case "not": return this.evaluateNot(expression);
843
+ case "date_eq":
844
+ case "date_gt":
845
+ case "date_gte":
846
+ case "date_lt":
847
+ case "date_lte":
848
+ case "date_add_days":
849
+ case "date_add_months":
850
+ case "date_add_years":
851
+ case "date_diff":
852
+ case "date_min":
853
+ case "date_max": return this.evaluateDateFunction(expression);
854
+ case "str_eq": return this.evaluateStrEq(expression);
855
+ case "eq": return this.evaluateEq(expression);
856
+ case "gt": return this.evaluateGt(expression);
857
+ case "gte": return this.evaluateGte(expression);
858
+ case "lt": return this.evaluateLt(expression);
859
+ case "lte": return this.evaluateLte(expression);
860
+ case "in_range": return this.evaluateInRange(expression);
861
+ case "sum": return this.evaluateSum(expression);
862
+ case "min": return this.evaluateMin(expression);
863
+ case "max": return this.evaluateMax(expression);
864
+ case "list_contains": return this.evaluateListContains(expression);
865
+ default: throw new Error(`Unsupported function: ${expression.functionName}`);
866
+ }
867
+ }
868
+ evaluateContextVariable(expression) {
869
+ if (!this.context?.surveyContext) return;
870
+ const surveyContext = this.context.surveyContext;
871
+ switch (expression.contextType) {
872
+ case ContextVariableType.Locale: return {
873
+ type: ValueType.string,
874
+ value: surveyContext.locale
875
+ };
876
+ case ContextVariableType.ParticipantFlag: {
877
+ if (!expression.key) return;
878
+ const flagKey = this.eval(expression.key);
879
+ if (flagKey?.type !== ValueType.string || flagKey.value === void 0) return;
880
+ const flagValue = surveyContext.participantFlags?.[flagKey.value];
881
+ switch (expression.asType) {
882
+ case ValueType.boolean: return {
883
+ type: ValueType.boolean,
884
+ value: flagValue !== void 0
885
+ };
886
+ case ValueType.string:
887
+ if (flagValue === void 0) return;
888
+ return {
889
+ type: ValueType.string,
890
+ value: flagValue
891
+ };
892
+ case ValueType.number: {
893
+ if (flagValue === void 0) return;
894
+ const numValue = parseFloat(flagValue);
895
+ return isNaN(numValue) ? void 0 : {
896
+ type: ValueType.number,
897
+ value: numValue
898
+ };
899
+ }
900
+ case ValueType.date:
901
+ if (flagValue === void 0) return;
902
+ return isCalendarDate(flagValue) ? {
903
+ type: ValueType.date,
904
+ value: flagValue
905
+ } : void 0;
906
+ default:
907
+ if (flagValue === void 0) return;
908
+ return {
909
+ type: ValueType.string,
910
+ value: flagValue
911
+ };
912
+ }
913
+ }
914
+ case ContextVariableType.CustomValue: {
915
+ if (!expression.key) return;
916
+ const customKey = this.eval(expression.key);
917
+ if (customKey?.type !== ValueType.string || customKey.value === void 0) return;
918
+ const customValue = surveyContext.customValues?.[customKey.value];
919
+ if (customValue === void 0) return;
920
+ if (expression.asType !== void 0) {
921
+ if (customValue?.type !== expression.asType) throw new Error(`Custom value type does not match expected type, got ${customValue?.type} and expected ${expression.asType}`);
922
+ }
923
+ return customValue;
924
+ }
925
+ case ContextVariableType.CustomExpression: {
926
+ if (!expression.key) return;
927
+ const expressionKey = this.eval(expression.key);
928
+ if (expressionKey?.type !== ValueType.string || expressionKey.value === void 0) return;
929
+ const customExpression = surveyContext.customExpressions?.[expressionKey.value];
930
+ if (typeof customExpression !== ExpressionType.Function || customExpression === void 0) return;
931
+ try {
932
+ const args = expression.arguments;
933
+ const result = customExpression(args);
934
+ if (expression.asType !== void 0) {
935
+ if (result?.type !== expression.asType) throw new Error(`Custom expression result type does not match expected type, got ${result?.type} and expected ${expression.asType}`);
936
+ }
937
+ return result;
938
+ } catch (error) {
939
+ throw new Error(`Error evaluating custom expression: ${error}`);
940
+ }
941
+ }
942
+ }
943
+ }
944
+ evaluateDateFunction(expression) {
945
+ const name = expression.functionName;
946
+ const aggregate = name === "date_min" || name === "date_max";
947
+ if (aggregate ? expression.arguments.length < 1 : expression.arguments.length !== 2) throw new Error(`${name} expects ${aggregate ? "at least 1 argument" : "2 arguments"}`);
948
+ const args = expression.arguments.map((arg) => this.eval(arg));
949
+ if (args.some((arg) => arg === void 0)) return void 0;
950
+ const first = args[0];
951
+ if (first?.type !== ValueType.date || !isCalendarDate(first.value)) return void 0;
952
+ if (name.startsWith("date_add_")) {
953
+ const amount = args[1];
954
+ if (amount?.type !== ValueType.number) return void 0;
955
+ const unit = name === "date_add_days" ? "days" : name === "date_add_months" ? "months" : "years";
956
+ const value = addCalendarDate(first.value, amount.value, unit);
957
+ return value === void 0 ? void 0 : {
958
+ type: ValueType.date,
959
+ value
960
+ };
961
+ }
962
+ const dates = [];
963
+ for (const arg of args) {
964
+ if (arg?.type !== ValueType.date || !isCalendarDate(arg.value)) return void 0;
965
+ dates.push(arg.value);
966
+ }
967
+ if (name === "date_eq") return {
968
+ type: ValueType.boolean,
969
+ value: dates[0] === dates[1]
970
+ };
971
+ if (dates.some((date) => date.length !== first.value.length)) return void 0;
972
+ if (aggregate) return {
973
+ type: ValueType.date,
974
+ value: dates.reduce((a, b) => name === "date_min" ? a < b ? a : b : a > b ? a : b)
975
+ };
976
+ if (name === "date_diff") return {
977
+ type: ValueType.number,
978
+ value: differenceCalendarDates(dates[0], dates[1])
979
+ };
980
+ const comparison = compareCalendarDates(dates[0], dates[1]);
981
+ return {
982
+ type: ValueType.boolean,
983
+ value: name === "date_gt" ? comparison > 0 : name === "date_gte" ? comparison >= 0 : name === "date_lt" ? comparison < 0 : comparison <= 0
984
+ };
985
+ }
986
+ evaluateAnd(expression) {
987
+ return {
988
+ type: ValueType.boolean,
989
+ value: expression.arguments.every((arg) => this.eval(arg)?.value === true)
990
+ };
991
+ }
992
+ evaluateOr(expression) {
993
+ return {
994
+ type: ValueType.boolean,
995
+ value: expression.arguments.some((arg) => this.eval(arg)?.value === true)
996
+ };
997
+ }
998
+ evaluateNot(expression) {
999
+ if (expression.arguments.length !== 1 || expression.arguments[0] === void 0) throw new Error(`Not function expects 1 argument, got ${expression.arguments.length}`);
1000
+ const resolvedValue = this.eval(expression.arguments[0]);
1001
+ if (resolvedValue?.type !== ValueType.boolean) throw new Error(`Not function expects a boolean argument, got ${resolvedValue?.type}`);
1002
+ if (resolvedValue === void 0) return {
1003
+ type: ValueType.boolean,
1004
+ value: false
1005
+ };
1006
+ return {
1007
+ type: ValueType.boolean,
1008
+ value: !resolvedValue.value
1009
+ };
1010
+ }
1011
+ evaluateListContains(expression) {
1012
+ if (expression.arguments.length !== 2) throw new Error(`List contains function expects 2 arguments, got ${expression.arguments.length}`);
1013
+ const resolvedList = this.eval(expression.arguments[0]);
1014
+ const resolvedItem = this.eval(expression.arguments[1]);
1015
+ if (resolvedList === void 0 || resolvedItem === void 0) return {
1016
+ type: ValueType.boolean,
1017
+ value: false
1018
+ };
1019
+ const allowedListTypes = [ValueType.stringArray, ValueType.referenceArray];
1020
+ const allowedItemTypes = [ValueType.string, ValueType.reference];
1021
+ const resolvedListType = resolvedList.type;
1022
+ const resolvedItemType = resolvedItem.type;
1023
+ if (!allowedListTypes.includes(resolvedListType) || !allowedItemTypes.includes(resolvedItemType)) throw new Error(`List contains function expects a string array list and a string item, got ${resolvedList?.type} and ${resolvedItem?.type}`);
1024
+ const list = resolvedList.value;
1025
+ const item = resolvedItem.value;
1026
+ return {
1027
+ type: ValueType.boolean,
1028
+ value: list.includes(item)
1029
+ };
1030
+ }
1031
+ evaluateStrEq(expression) {
1032
+ if (expression.arguments.length !== 2) throw new Error(`String equals function expects 2 arguments, got ${expression.arguments.length}`);
1033
+ const resolvedA = this.eval(expression.arguments[0]);
1034
+ const resolvedB = this.eval(expression.arguments[1]);
1035
+ if (resolvedA === void 0 || resolvedB === void 0) return {
1036
+ type: ValueType.boolean,
1037
+ value: false
1038
+ };
1039
+ if (resolvedA?.type !== ValueType.string || resolvedB?.type !== ValueType.string) throw new Error(`String equals function expects strings for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
1040
+ return {
1041
+ type: ValueType.boolean,
1042
+ value: resolvedA.value === resolvedB.value
1043
+ };
1044
+ }
1045
+ evaluateEq(expression) {
1046
+ if (expression.arguments.length !== 2) throw new Error(`Equals function expects 2 arguments, got ${expression.arguments.length}`);
1047
+ const resolvedA = this.eval(expression.arguments[0]);
1048
+ const resolvedB = this.eval(expression.arguments[1]);
1049
+ if (resolvedA === void 0 || resolvedB === void 0) return {
1050
+ type: ValueType.boolean,
1051
+ value: false
1052
+ };
1053
+ if (resolvedA?.type !== resolvedB?.type) throw new Error(`Equals function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
1054
+ return {
1055
+ type: ValueType.boolean,
1056
+ value: resolvedA.value === resolvedB.value
1057
+ };
1058
+ }
1059
+ evaluateGt(expression) {
1060
+ if (expression.arguments.length !== 2) throw new Error(`Greater than function expects 2 arguments, got ${expression.arguments.length}`);
1061
+ const resolvedA = this.eval(expression.arguments[0]);
1062
+ const resolvedB = this.eval(expression.arguments[1]);
1063
+ if (resolvedA === void 0 || resolvedB === void 0) return {
1064
+ type: ValueType.boolean,
1065
+ value: false
1066
+ };
1067
+ if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
1068
+ return {
1069
+ type: ValueType.boolean,
1070
+ value: resolvedA.value > resolvedB.value
1071
+ };
1072
+ }
1073
+ evaluateGte(expression) {
1074
+ if (expression.arguments.length !== 2) throw new Error(`Greater than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
1075
+ const resolvedA = this.eval(expression.arguments[0]);
1076
+ const resolvedB = this.eval(expression.arguments[1]);
1077
+ if (resolvedA === void 0 || resolvedB === void 0) return {
1078
+ type: ValueType.boolean,
1079
+ value: false
1080
+ };
1081
+ if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
1082
+ return {
1083
+ type: ValueType.boolean,
1084
+ value: resolvedA.value >= resolvedB.value
1085
+ };
1086
+ }
1087
+ evaluateLt(expression) {
1088
+ if (expression.arguments.length !== 2) throw new Error(`Less than function expects 2 arguments, got ${expression.arguments.length}`);
1089
+ const resolvedA = this.eval(expression.arguments[0]);
1090
+ const resolvedB = this.eval(expression.arguments[1]);
1091
+ if (resolvedA === void 0 || resolvedB === void 0) return {
1092
+ type: ValueType.boolean,
1093
+ value: false
1094
+ };
1095
+ if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
1096
+ return {
1097
+ type: ValueType.boolean,
1098
+ value: resolvedA.value < resolvedB.value
1099
+ };
1100
+ }
1101
+ evaluateLte(expression) {
1102
+ if (expression.arguments.length !== 2) throw new Error(`Less than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
1103
+ const resolvedA = this.eval(expression.arguments[0]);
1104
+ const resolvedB = this.eval(expression.arguments[1]);
1105
+ if (resolvedA === void 0 || resolvedB === void 0) return {
1106
+ type: ValueType.boolean,
1107
+ value: false
1108
+ };
1109
+ if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
1110
+ return {
1111
+ type: ValueType.boolean,
1112
+ value: resolvedA.value <= resolvedB.value
1113
+ };
1114
+ }
1115
+ evaluateInRange(expression) {
1116
+ if (expression.arguments.length !== 4) throw new Error(`In range function expects 4 arguments, got ${expression.arguments.length}`);
1117
+ const resolvedValue = this.eval(expression.arguments[0]);
1118
+ const resolvedMin = this.eval(expression.arguments[1]);
1119
+ const resolvedMax = this.eval(expression.arguments[2]);
1120
+ const resolvedInclusive = this.eval(expression.arguments[3])?.value === true;
1121
+ if (resolvedValue === void 0 || resolvedMin === void 0 || resolvedMax === void 0) return {
1122
+ type: ValueType.boolean,
1123
+ value: false
1124
+ };
1125
+ if (resolvedValue?.type !== ValueType.number || resolvedMin?.type !== ValueType.number || resolvedMax?.type !== ValueType.number) throw new Error(`In range function expects numbers for all arguments, got ${resolvedValue?.type} and ${resolvedMin?.type} and ${resolvedMax?.type}`);
1126
+ if (resolvedInclusive) return {
1127
+ type: ValueType.boolean,
1128
+ value: resolvedValue.value >= resolvedMin.value && resolvedValue.value <= resolvedMax.value
1129
+ };
1130
+ return {
1131
+ type: ValueType.boolean,
1132
+ value: resolvedValue.value > resolvedMin.value && resolvedValue.value < resolvedMax.value
1133
+ };
1134
+ }
1135
+ evaluateSum(expression) {
1136
+ if (expression.arguments.length < 1) throw new Error(`Sum function expects at least 1 argument, got ${expression.arguments.length}`);
1137
+ return expression.arguments.reduce((sum, arg) => {
1138
+ if (arg === void 0) return sum;
1139
+ const resolvedValue = this.eval(arg);
1140
+ if (resolvedValue === void 0) return sum;
1141
+ if (resolvedValue?.type === ValueType.number) return {
1142
+ type: ValueType.number,
1143
+ value: sum.value + resolvedValue.value
1144
+ };
1145
+ throw new Error(`Sum function expects all arguments to be numbers, got ${resolvedValue?.type}`);
1146
+ }, {
1147
+ type: ValueType.number,
1148
+ value: 0
1149
+ });
1150
+ }
1151
+ evaluateMin(expression) {
1152
+ if (expression.arguments.length < 1) throw new Error(`Min function expects at least 1 argument, got ${expression.arguments.length}`);
1153
+ return expression.arguments.reduce((min, arg) => {
1154
+ if (arg === void 0) return min;
1155
+ const resolvedValue = this.eval(arg);
1156
+ if (resolvedValue === void 0) return min;
1157
+ if (resolvedValue?.type === ValueType.number) return {
1158
+ type: ValueType.number,
1159
+ value: Math.min(min.value, resolvedValue.value)
1160
+ };
1161
+ throw new Error(`Min function expects all arguments to be numbers, got ${typeof resolvedValue}`);
1162
+ }, {
1163
+ type: ValueType.number,
1164
+ value: Infinity
1165
+ });
1166
+ }
1167
+ evaluateMax(expression) {
1168
+ if (expression.arguments.length < 1) throw new Error(`Max function expects at least 1 argument, got ${expression.arguments.length}`);
1169
+ return expression.arguments.reduce((max, arg) => {
1170
+ if (arg === void 0) return max;
1171
+ const resolvedValue = this.eval(arg);
1172
+ if (resolvedValue === void 0) return max;
1173
+ if (resolvedValue?.type === ValueType.number) return {
1174
+ type: ValueType.number,
1175
+ value: Math.max(max.value, resolvedValue.value)
1176
+ };
1177
+ throw new Error(`Max function expects all arguments to be numbers, got ${typeof resolvedValue}`);
1178
+ }, {
1179
+ type: ValueType.number,
1180
+ value: -Infinity
1181
+ });
1182
+ }
1183
+ };
1184
+ //#endregion
1185
+ //#region src/expressions/template-value.ts
1186
+ let TemplateDefTypes = /* @__PURE__ */ function(TemplateDefTypes) {
1187
+ TemplateDefTypes["Default"] = "default";
1188
+ TemplateDefTypes["Date2String"] = "date2string";
1189
+ return TemplateDefTypes;
1190
+ }({});
1191
+ const serializeTemplateValue = (templateValue) => {
1192
+ const json = {
1193
+ type: templateValue.type,
1194
+ returnType: templateValue.returnType,
1195
+ expression: templateValue.expression?.serialize()
1196
+ };
1197
+ if (templateValue.type === "date2string") json.dateFormat = templateValue.dateFormat;
1198
+ return json;
1199
+ };
1200
+ const serializeTemplateValues = (templateValues) => {
1201
+ const json = {};
1202
+ for (const [key, value] of templateValues.entries()) json[key] = serializeTemplateValue(value);
1203
+ return json;
1204
+ };
1205
+ const deserializeTemplateValue = (json) => {
1206
+ return {
1207
+ type: json.type,
1208
+ expression: json.expression ? Expression.deserialize(json.expression) : void 0,
1209
+ returnType: json.returnType,
1210
+ dateFormat: json.dateFormat
1211
+ };
1212
+ };
1213
+ const deserializeTemplateValues = (json) => {
1214
+ return new Map(Object.entries(json).map(([key, value]) => [key, deserializeTemplateValue(value)]));
1215
+ };
1216
+ //#endregion
1217
+ //#region src/expressions/date-functions.ts
1218
+ /** Shared signatures used by authoring, reference diagnostics and assistant validation. */
1219
+ const DATE_FUNCTION_SIGNATURES = {
1220
+ date_eq: {
1221
+ args: [ValueType.date, ValueType.date],
1222
+ returns: ValueType.boolean
1223
+ },
1224
+ date_gt: {
1225
+ args: [ValueType.date, ValueType.date],
1226
+ returns: ValueType.boolean
1227
+ },
1228
+ date_gte: {
1229
+ args: [ValueType.date, ValueType.date],
1230
+ returns: ValueType.boolean
1231
+ },
1232
+ date_lt: {
1233
+ args: [ValueType.date, ValueType.date],
1234
+ returns: ValueType.boolean
1235
+ },
1236
+ date_lte: {
1237
+ args: [ValueType.date, ValueType.date],
1238
+ returns: ValueType.boolean
1239
+ },
1240
+ date_add_days: {
1241
+ args: [ValueType.date, ValueType.number],
1242
+ returns: ValueType.date
1243
+ },
1244
+ date_add_months: {
1245
+ args: [ValueType.date, ValueType.number],
1246
+ returns: ValueType.date
1247
+ },
1248
+ date_add_years: {
1249
+ args: [ValueType.date, ValueType.number],
1250
+ returns: ValueType.date
1251
+ },
1252
+ date_diff: {
1253
+ args: [ValueType.date, ValueType.date],
1254
+ returns: ValueType.number
1255
+ },
1256
+ date_min: {
1257
+ args: [ValueType.date],
1258
+ variadic: true,
1259
+ returns: ValueType.date
1260
+ },
1261
+ date_max: {
1262
+ args: [ValueType.date],
1263
+ variadic: true,
1264
+ returns: ValueType.date
1265
+ }
1266
+ };
1267
+ //#endregion
1268
+ //#region src/expressions/reference-integrity.ts
1269
+ const ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY = {
1270
+ component: false,
1271
+ item: true,
1272
+ survey: false
1273
+ };
1274
+ const ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY = {
1275
+ "display-condition": true,
1276
+ "disabled-condition": false,
1277
+ validation: false,
1278
+ "prefill-condition": false,
1279
+ "prefill-source": false,
1280
+ "template-value": false
1281
+ };
1282
+ /**
1283
+ * Display-condition cycles prevent active-response evaluation, even with prefills.
1284
+ * This item-level analysis is complemented by ActiveResponsePlan's full runtime graph,
1285
+ * which also includes inherited visibility, component activation and computed dependencies.
1286
+ */
1287
+ const SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY = {
1288
+ "invalid-expression": {
1289
+ category: "reference-integrity",
1290
+ disposition: "blocking"
1291
+ },
1292
+ "unknown-response-ref": {
1293
+ category: "reference-integrity",
1294
+ disposition: "blocking"
1295
+ },
1296
+ "expression-reference-type-mismatch": {
1297
+ category: "reference-integrity",
1298
+ disposition: "blocking"
1299
+ },
1300
+ "unknown-reference-value": {
1301
+ category: "reference-integrity",
1302
+ disposition: "blocking"
1303
+ },
1304
+ "display-condition-cycle": {
1305
+ category: "display-dependency",
1306
+ disposition: "blocking"
1307
+ }
1308
+ };
1309
+ const createExpressionReferenceDiagnostic = (diagnostic) => ({
1310
+ ...diagnostic,
1311
+ ...SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY[diagnostic.code]
1312
+ });
1313
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1314
+ const isJsonExpression = (value) => isRecord(value) && typeof value.type === "string";
1315
+ /** Discover references even when a sibling expression is malformed and cannot deserialize. */
1316
+ function collectExpressionResponseReferences(expression) {
1317
+ const refs = /* @__PURE__ */ new Map();
1318
+ const visit = (node) => {
1319
+ if (!isRecord(node)) return;
1320
+ if (node.type === ExpressionType.ResponseVariable && isRecord(node.variableRef)) try {
1321
+ const reference = new ValueReference(node.variableRef);
1322
+ refs.set(reference.toString(), reference);
1323
+ } catch {}
1324
+ if (node.type === ExpressionType.ContextVariable) visit(node.key);
1325
+ if (node.type === ExpressionType.Function || node.type === ExpressionType.ContextVariable) {
1326
+ if (Array.isArray(node.arguments)) node.arguments.forEach(visit);
1327
+ }
1328
+ };
1329
+ visit(expression);
1330
+ return [...refs.values()];
1331
+ }
1332
+ /** Shared by integrity diagnostics and deletion guards for config-owned bounds. */
1333
+ const collectConfigValueExpressions = (config) => {
1334
+ const locations = [];
1335
+ const visit = (value, path) => {
1336
+ if (!value || typeof value !== "object") return;
1337
+ for (const [key, child] of Object.entries(value)) {
1338
+ const location = `${path}.${key}`;
1339
+ if (key === "valueExpression") {
1340
+ if (isJsonExpression(child)) locations.push({
1341
+ expression: child,
1342
+ location
1343
+ });
1344
+ } else visit(child, location);
1345
+ }
1346
+ };
1347
+ visit(config, "config");
1348
+ return locations;
1349
+ };
1350
+ const collectItemExpressionLocations = (item) => {
1351
+ const locations = [];
1352
+ const add = (expression, location, ownerScope, surface) => {
1353
+ if (!isJsonExpression(expression)) return;
1354
+ locations.push({
1355
+ expression,
1356
+ location,
1357
+ ownerItemId: item.id,
1358
+ ownerItemKey: item.metadata?.itemLabel ?? "",
1359
+ ownerScope,
1360
+ surface
1361
+ });
1362
+ };
1363
+ add(item.displayConditions?.root, "displayConditions.root", "item", "display-condition");
1364
+ for (const [componentId, expression] of Object.entries(item.displayConditions?.components ?? {})) add(expression, `displayConditions.components.${componentId}`, "component", "display-condition");
1365
+ for (const [componentId, expression] of Object.entries(item.disabledConditions?.components ?? {})) add(expression, `disabledConditions.components.${componentId}`, "component", "disabled-condition");
1366
+ for (const [validationKey, expression] of Object.entries(item.validations ?? {})) add(expression, `validations.${validationKey}`, "item", "validation");
1367
+ for (const [prefillIndex, prefill] of (item.prefills ?? []).entries()) {
1368
+ const prefillId = prefill.id || String(prefillIndex);
1369
+ add(prefill.when, `prefills.${prefillId}.when`, "item", "prefill-condition");
1370
+ if (prefill.source.type === "expression") add(prefill.source.expression, `prefills.${prefillId}.source.expression`, "item", "prefill-source");
1371
+ }
1372
+ for (const { expression, location } of collectConfigValueExpressions(item.config)) add(expression, location, "item", "validation");
1373
+ return locations;
1374
+ };
1375
+ const collectSurveyExpressionLocations = (survey) => [...survey.surveyItems.flatMap(collectItemExpressionLocations), ...Object.entries(survey.templateValues ?? {}).flatMap(([templateKey, templateValue]) => templateValue.expression ? [{
1376
+ expression: templateValue.expression,
1377
+ location: `templateValues.${templateKey}.expression`,
1378
+ ownerScope: "survey",
1379
+ surface: "template-value"
1380
+ }] : [])];
1381
+ const candidateRefs = (unknownRef, availableRefs) => {
1382
+ let parsed;
1383
+ try {
1384
+ parsed = new ValueReference(unknownRef);
1385
+ } catch {
1386
+ return availableRefs.slice(0, 5);
1387
+ }
1388
+ const sameItemAndMethod = availableRefs.filter((candidate) => {
1389
+ try {
1390
+ return new ValueReference(candidate).name === parsed?.name;
1391
+ } catch {
1392
+ return false;
1393
+ }
1394
+ });
1395
+ return (sameItemAndMethod.length > 0 ? sameItemAndMethod : availableRefs).slice(0, 5);
1396
+ };
1397
+ const findCyclicItemDisplayDependencyEdges = ({ edges, itemIds }) => {
1398
+ const adjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
1399
+ const reverseAdjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
1400
+ for (const edge of edges) {
1401
+ adjacency.get(edge.fromItemId)?.add(edge.toItemId);
1402
+ reverseAdjacency.get(edge.toItemId)?.add(edge.fromItemId);
1403
+ }
1404
+ const finishOrder = [];
1405
+ const visited = /* @__PURE__ */ new Set();
1406
+ for (const startItemId of itemIds) {
1407
+ if (visited.has(startItemId)) continue;
1408
+ const stack = [{
1409
+ expanded: false,
1410
+ itemId: startItemId
1411
+ }];
1412
+ while (stack.length > 0) {
1413
+ const current = stack.pop();
1414
+ if (!current) break;
1415
+ if (current.expanded) {
1416
+ finishOrder.push(current.itemId);
1417
+ continue;
1418
+ }
1419
+ if (visited.has(current.itemId)) continue;
1420
+ visited.add(current.itemId);
1421
+ stack.push({
1422
+ expanded: true,
1423
+ itemId: current.itemId
1424
+ });
1425
+ const dependencies = [...adjacency.get(current.itemId) ?? []];
1426
+ for (let index = dependencies.length - 1; index >= 0; index -= 1) {
1427
+ const dependencyId = dependencies[index];
1428
+ if (visited.has(dependencyId)) continue;
1429
+ stack.push({
1430
+ expanded: false,
1431
+ itemId: dependencyId
1432
+ });
1433
+ }
1434
+ }
1435
+ }
1436
+ const componentByItemId = /* @__PURE__ */ new Map();
1437
+ const componentMembers = /* @__PURE__ */ new Map();
1438
+ for (const startItemId of finishOrder.reverse()) {
1439
+ if (componentByItemId.has(startItemId)) continue;
1440
+ const componentId = componentMembers.size;
1441
+ const members = [];
1442
+ const stack = [startItemId];
1443
+ componentByItemId.set(startItemId, componentId);
1444
+ while (stack.length > 0) {
1445
+ const itemId = stack.pop();
1446
+ if (!itemId) break;
1447
+ members.push(itemId);
1448
+ for (const dependencyOwnerId of reverseAdjacency.get(itemId) ?? []) {
1449
+ if (componentByItemId.has(dependencyOwnerId)) continue;
1450
+ componentByItemId.set(dependencyOwnerId, componentId);
1451
+ stack.push(dependencyOwnerId);
1452
+ }
1453
+ }
1454
+ componentMembers.set(componentId, members.sort());
1455
+ }
1456
+ return edges.flatMap((edge) => {
1457
+ const componentId = componentByItemId.get(edge.fromItemId);
1458
+ if (componentId === void 0 || componentId !== componentByItemId.get(edge.toItemId)) return [];
1459
+ const cycleItemIds = componentMembers.get(componentId) ?? [];
1460
+ if (edge.fromItemId !== edge.toItemId && cycleItemIds.length < 2) return [];
1461
+ return [{
1462
+ edge,
1463
+ cycleItemIds
1464
+ }];
1465
+ });
1466
+ };
1467
+ const inferExpressionType = (expression, slots) => {
1468
+ if (!expression) return void 0;
1469
+ if (expression.type === ExpressionType.Const) return expression.value?.type;
1470
+ if (expression.type === ExpressionType.ResponseVariable) return slots.get(new ValueReference(expression.variableRef).toString())?.valueType;
1471
+ if (expression.type === ExpressionType.ContextVariable) return expression.contextType === "locale" ? ValueType.string : expression.asType;
1472
+ switch (expression.functionName) {
1473
+ case "and":
1474
+ case "or":
1475
+ case "not":
1476
+ case "eq":
1477
+ case "gt":
1478
+ case "gte":
1479
+ case "lt":
1480
+ case "lte":
1481
+ case "in_range":
1482
+ case "list_contains":
1483
+ case "str_eq":
1484
+ case "date_eq":
1485
+ case "date_gt":
1486
+ case "date_gte":
1487
+ case "date_lt":
1488
+ case "date_lte": return ValueType.boolean;
1489
+ case "date_add_days":
1490
+ case "date_add_months":
1491
+ case "date_add_years":
1492
+ case "date_min":
1493
+ case "date_max": return ValueType.date;
1494
+ case "date_diff": return ValueType.number;
1495
+ case "sum":
1496
+ case "min":
1497
+ case "max": return ValueType.number;
1498
+ }
1499
+ };
1500
+ const itemTypeForList = (type) => {
1501
+ switch (type) {
1502
+ case ValueType.stringArray: return ValueType.string;
1503
+ case ValueType.referenceArray: return ValueType.reference;
1504
+ case ValueType.numberArray: return ValueType.number;
1505
+ case ValueType.dateArray: return ValueType.date;
1506
+ case ValueType.durationArray: return ValueType.duration;
1507
+ default: return;
1508
+ }
1509
+ };
1510
+ const inspectFunctionReferenceCompatibility = ({ diagnostics, expression, location, slots }) => {
1511
+ if (expression.type === ExpressionType.ContextVariable) {
1512
+ for (const argument of [expression.key, ...expression.arguments ?? []]) if (argument) inspectFunctionReferenceCompatibility({
1513
+ diagnostics,
1514
+ expression: argument,
1515
+ location,
1516
+ slots
1517
+ });
1518
+ return;
1519
+ }
1520
+ if (expression.type !== ExpressionType.Function) return;
1521
+ expression.arguments.forEach((argument) => {
1522
+ if (argument) inspectFunctionReferenceCompatibility({
1523
+ diagnostics,
1524
+ expression: argument,
1525
+ location,
1526
+ slots
1527
+ });
1528
+ });
1529
+ const signature = DATE_FUNCTION_SIGNATURES[expression.functionName];
1530
+ if (signature) {
1531
+ const invalidArity = signature.variadic ? expression.arguments.length < 1 : expression.arguments.length !== signature.args.length;
1532
+ const invalidType = expression.arguments.some((arg, i) => {
1533
+ const actual = inferExpressionType(arg, slots);
1534
+ return actual && actual !== signature.args[signature.variadic ? 0 : i];
1535
+ });
1536
+ if (invalidArity || invalidType) diagnostics.push({
1537
+ code: "expression-reference-type-mismatch",
1538
+ message: `${location.location} has invalid arguments for ${expression.functionName}.`,
1539
+ location: location.location,
1540
+ ownerItemId: location.ownerItemId,
1541
+ ownerItemKey: location.ownerItemKey
1542
+ });
1543
+ }
1544
+ const [left, right] = expression.arguments;
1545
+ const leftType = inferExpressionType(left, slots);
1546
+ const rightType = inferExpressionType(right, slots);
1547
+ if (expression.functionName === "list_contains" ? Boolean(leftType && rightType && itemTypeForList(leftType) !== rightType) : expression.functionName === "eq" ? Boolean(leftType && rightType && leftType !== rightType) : false) diagnostics.push({
1548
+ code: "expression-reference-type-mismatch",
1549
+ message: `${location.location} compares incompatible values (${leftType} and ${rightType}).`,
1550
+ location: location.location,
1551
+ ownerItemId: location.ownerItemId,
1552
+ ownerItemKey: location.ownerItemKey
1553
+ });
1554
+ if (expression.functionName !== "eq" && expression.functionName !== "str_eq" && expression.functionName !== "list_contains") return;
1555
+ const pairs = [[left, right], [right, left]];
1556
+ for (const [variable, constant] of pairs) {
1557
+ if (variable?.type !== ExpressionType.ResponseVariable || constant?.type !== ExpressionType.Const || !constant.value) continue;
1558
+ const slot = slots.get(new ValueReference(variable.variableRef).toString());
1559
+ const domain = slot?.allowedValues ?? slot?.allowedReferenceValues?.map((value) => ({
1560
+ type: ValueType.reference,
1561
+ value
1562
+ }));
1563
+ if (!domain) continue;
1564
+ for (const invalid of invalidResponseDomainValues(constant.value, domain)) {
1565
+ const referenceValue = typeof invalid.value === "string" ? invalid.value : JSON.stringify(invalid.value);
1566
+ diagnostics.push({
1567
+ code: "unknown-reference-value",
1568
+ message: `${location.location} compares slot "${variable.variableRef.slotId}" with invalid value ${JSON.stringify(invalid)}. Allowed values: ${JSON.stringify(domain)}.`,
1569
+ location: location.location,
1570
+ ownerItemId: location.ownerItemId,
1571
+ ownerItemKey: location.ownerItemKey,
1572
+ reference: new ValueReference(variable.variableRef).toString(),
1573
+ referenceValue,
1574
+ candidates: domain.map((value) => String(value.value))
1575
+ });
1576
+ }
1577
+ }
1578
+ };
1579
+ const analyzeSurveyExpressionReferences = ({ responseSlots, survey }) => {
1580
+ const slots = new Map(responseSlots.map((slot) => [slot.ref, slot]));
1581
+ const availableRefs = responseSlots.map((slot) => slot.ref);
1582
+ const diagnostics = [];
1583
+ const surveyItemIds = survey.surveyItems.map((item) => item.id);
1584
+ const surveyItemIdSet = new Set(surveyItemIds);
1585
+ const itemDisplayDependencyEdges = [];
1586
+ for (const location of collectSurveyExpressionLocations(survey)) {
1587
+ let parsed;
1588
+ try {
1589
+ parsed = Expression.deserialize(location.expression);
1590
+ } catch (error) {
1591
+ diagnostics.push({
1592
+ code: "invalid-expression",
1593
+ message: `${location.location} contains invalid expression JSON: ${error instanceof Error ? error.message : String(error)}`,
1594
+ location: location.location,
1595
+ ownerItemId: location.ownerItemId,
1596
+ ownerItemKey: location.ownerItemKey
1597
+ });
1598
+ continue;
1599
+ }
1600
+ for (const ref of parsed?.responseVariableRefs ?? []) {
1601
+ const reference = ref.toString();
1602
+ if (location.ownerItemId && ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY[location.ownerScope] && ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY[location.surface] && surveyItemIdSet.has(slots.get(reference)?.itemId ?? "")) itemDisplayDependencyEdges.push({
1603
+ fromItemId: location.ownerItemId,
1604
+ location,
1605
+ reference,
1606
+ toItemId: slots.get(reference).itemId
1607
+ });
1608
+ if (!slots.has(reference)) diagnostics.push({
1609
+ code: "unknown-response-ref",
1610
+ message: `${location.location} references unknown response slot "${reference}".`,
1611
+ location: location.location,
1612
+ ownerItemId: location.ownerItemId,
1613
+ ownerItemKey: location.ownerItemKey,
1614
+ reference,
1615
+ candidates: candidateRefs(reference, availableRefs)
1616
+ });
1617
+ }
1618
+ if (location.location.endsWith(".valueExpression")) {
1619
+ const actual = inferExpressionType(location.expression, slots);
1620
+ if (actual && actual !== ValueType.date) diagnostics.push({
1621
+ code: "expression-reference-type-mismatch",
1622
+ message: `${location.location} must return a date.`,
1623
+ location: location.location,
1624
+ ownerItemId: location.ownerItemId,
1625
+ ownerItemKey: location.ownerItemKey
1626
+ });
1627
+ }
1628
+ inspectFunctionReferenceCompatibility({
1629
+ diagnostics,
1630
+ expression: location.expression,
1631
+ location,
1632
+ slots
1633
+ });
1634
+ }
1635
+ for (const { cycleItemIds, edge } of findCyclicItemDisplayDependencyEdges({
1636
+ edges: itemDisplayDependencyEdges,
1637
+ itemIds: surveyItemIds
1638
+ })) {
1639
+ const isSelfReference = edge.fromItemId === edge.toItemId;
1640
+ diagnostics.push({
1641
+ code: "display-condition-cycle",
1642
+ message: isSelfReference ? `${edge.location.location} for item "${edge.fromItemId}" references its own response "${edge.reference}", creating a circular display dependency.` : `${edge.location.location} for item "${edge.fromItemId}" references item "${edge.toItemId}" within a circular display dependency among ${cycleItemIds.map((itemId) => `"${itemId}"`).join(", ")}.`,
1643
+ location: edge.location.location,
1644
+ ownerItemId: edge.location.ownerItemId,
1645
+ ownerItemKey: edge.location.ownerItemKey,
1646
+ reference: edge.reference
1647
+ });
1648
+ }
1649
+ return diagnostics.map(createExpressionReferenceDiagnostic);
1650
+ };
1651
+ const expressionReferenceDiagnosticKey = (diagnostic) => [
1652
+ diagnostic.code,
1653
+ diagnostic.ownerItemId ?? "survey",
1654
+ diagnostic.location,
1655
+ diagnostic.reference ?? "",
1656
+ diagnostic.referenceValue ?? ""
1657
+ ].join("\0");
1658
+ const findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories = ({ after, before }) => {
1659
+ const existing = new Set(before.map(expressionReferenceDiagnosticKey));
1660
+ return after.filter((diagnostic) => !existing.has(expressionReferenceDiagnosticKey(diagnostic)));
1661
+ };
1662
+ const isBlockingSurveyExpressionReferenceDiagnostic = (diagnostic) => {
1663
+ switch (diagnostic.disposition) {
1664
+ case "blocking": return true;
1665
+ case "advisory": return false;
1666
+ default: return true;
1667
+ }
1668
+ };
1669
+ const findIntroducedBlockingExpressionReferenceDiagnostics = ({ after, before }) => findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories({
1670
+ after,
1671
+ before
1672
+ }).filter(isBlockingSurveyExpressionReferenceDiagnostic);
1673
+ const replaceResponseVariableReferences = (value, replacements, optionReplacements = {}) => {
1674
+ let changed = false;
1675
+ const visit = (current) => {
1676
+ if (Array.isArray(current)) return current.map(visit);
1677
+ if (!isRecord(current)) return current;
1678
+ const next = Object.fromEntries(Object.entries(current).map(([key, entry]) => [key, visit(entry)]));
1679
+ if (next.type === ExpressionType.ResponseVariable && isRecord(next.variableRef) && typeof next.variableRef.slotId === "string" && replacements[next.variableRef.slotId]) {
1680
+ next.variableRef.slotId = replacements[next.variableRef.slotId];
1681
+ changed = true;
1682
+ }
1683
+ if (next.type === ExpressionType.Const && isRecord(next.value)) {
1684
+ const remap = (id) => {
1685
+ if (!Object.hasOwn(optionReplacements, id)) return id;
1686
+ changed = true;
1687
+ return optionReplacements[id];
1688
+ };
1689
+ if (next.value.type === ValueType.reference && typeof next.value.value === "string") next.value.value = remap(next.value.value);
1690
+ if (next.value.type === ValueType.referenceArray && Array.isArray(next.value.value)) next.value.value = next.value.value.map((id) => typeof id === "string" ? remap(id) : id);
1691
+ }
1692
+ return next;
1693
+ };
1694
+ const rewritten = visit(value);
1695
+ return {
1696
+ changed,
1697
+ value: rewritten
1698
+ };
1699
+ };
1700
+ const responseValueReferenceIds = (values) => values?.flatMap((value) => value.type === ValueType.reference ? [value.value] : value.type === ValueType.referenceArray ? value.value : []);
1701
+ //#endregion
1702
+ //#region src/expressions/editors/expression-editor.ts
1703
+ var ExpressionEditor = class {
1704
+ returnType;
1705
+ _editorConfig;
1706
+ get editorConfig() {
1707
+ return this._editorConfig;
1708
+ }
1709
+ withEditorConfig(editorConfig) {
1710
+ this._editorConfig = editorConfig;
1711
+ return this;
1712
+ }
1713
+ };
1714
+ var ConstStringArrayEditor = class extends ExpressionEditor {
1715
+ returnType = ValueType.stringArray;
1716
+ _values;
1717
+ constructor(values, editorConfig) {
1718
+ super();
1719
+ this._values = values;
1720
+ this._editorConfig = editorConfig;
1721
+ }
1722
+ get values() {
1723
+ return this._values;
1724
+ }
1725
+ set values(values) {
1726
+ this._values = values;
1727
+ }
1728
+ getExpression() {
1729
+ return new ConstExpression({
1730
+ type: ValueType.stringArray,
1731
+ value: this._values
1732
+ }, this._editorConfig);
1733
+ }
1734
+ };
1735
+ var ConstStringEditor = class extends ExpressionEditor {
1736
+ returnType = ValueType.string;
1737
+ _value;
1738
+ constructor(value, editorConfig) {
1739
+ super();
1740
+ this._value = value;
1741
+ this._editorConfig = editorConfig;
1742
+ }
1743
+ get value() {
1744
+ return this._value;
1745
+ }
1746
+ set value(value) {
1747
+ this._value = value;
1748
+ }
1749
+ getExpression() {
1750
+ return new ConstExpression({
1751
+ type: ValueType.string,
1752
+ value: this._value
1753
+ }, this._editorConfig);
1754
+ }
1755
+ };
1756
+ var ConstNumberEditor = class extends ExpressionEditor {
1757
+ returnType = ValueType.number;
1758
+ _value;
1759
+ constructor(value, editorConfig) {
1760
+ super();
1761
+ this._value = value;
1762
+ this._editorConfig = editorConfig;
1763
+ }
1764
+ get value() {
1765
+ return this._value;
1766
+ }
1767
+ set value(value) {
1768
+ this._value = value;
1769
+ }
1770
+ getExpression() {
1771
+ return new ConstExpression({
1772
+ type: ValueType.number,
1773
+ value: this._value
1774
+ }, this._editorConfig);
1775
+ }
1776
+ };
1777
+ var ConstBooleanEditor = class extends ExpressionEditor {
1778
+ returnType = ValueType.boolean;
1779
+ _value;
1780
+ constructor(value, editorConfig) {
1781
+ super();
1782
+ this._value = value;
1783
+ this._editorConfig = editorConfig;
1784
+ }
1785
+ get value() {
1786
+ return this._value;
1787
+ }
1788
+ set value(value) {
1789
+ this._value = value;
1790
+ }
1791
+ getExpression() {
1792
+ return new ConstExpression({
1793
+ type: ValueType.boolean,
1794
+ value: this._value
1795
+ }, this._editorConfig);
1796
+ }
1797
+ };
1798
+ var ConstDateEditor = class extends ExpressionEditor {
1799
+ returnType = ValueType.date;
1800
+ _value;
1801
+ constructor(value, editorConfig) {
1802
+ super();
1803
+ this._value = value;
1804
+ this._editorConfig = editorConfig;
1805
+ }
1806
+ get value() {
1807
+ return this._value;
1808
+ }
1809
+ set value(value) {
1810
+ this._value = value;
1811
+ }
1812
+ getExpression() {
1813
+ const value = this._value;
1814
+ return new ConstExpression({
1815
+ type: ValueType.date,
1816
+ value
1817
+ }, this._editorConfig);
1818
+ }
1819
+ };
1820
+ var ConstNumberArrayEditor = class extends ExpressionEditor {
1821
+ returnType = ValueType.numberArray;
1822
+ _values;
1823
+ constructor(values, editorConfig) {
1824
+ super();
1825
+ this._values = values;
1826
+ this._editorConfig = editorConfig;
1827
+ }
1828
+ get values() {
1829
+ return this._values;
1830
+ }
1831
+ set values(values) {
1832
+ this._values = values;
1833
+ }
1834
+ getExpression() {
1835
+ return new ConstExpression({
1836
+ type: ValueType.numberArray,
1837
+ value: this._values
1838
+ }, this._editorConfig);
1839
+ }
1840
+ };
1841
+ var ConstDateArrayEditor = class extends ExpressionEditor {
1842
+ returnType = ValueType.dateArray;
1843
+ _values;
1844
+ constructor(values, editorConfig) {
1845
+ super();
1846
+ this._values = values;
1847
+ this._editorConfig = editorConfig;
1848
+ }
1849
+ get values() {
1850
+ return this._values;
1851
+ }
1852
+ set values(values) {
1853
+ this._values = values;
1854
+ }
1855
+ getExpression() {
1856
+ const value = this._values;
1857
+ return new ConstExpression({
1858
+ type: ValueType.dateArray,
1859
+ value
1860
+ }, this._editorConfig);
1861
+ }
1862
+ };
1863
+ var ResponseVariableEditor = class extends ExpressionEditor {
1864
+ _variableName;
1865
+ _variableRef;
1866
+ constructor(variableName, variableType, editorConfig) {
1867
+ super();
1868
+ this._variableName = new ValueReference(variableName).toString();
1869
+ this._variableRef = new ValueReference(variableName);
1870
+ this._editorConfig = editorConfig;
1871
+ this.returnType = variableType;
1872
+ }
1873
+ get variableName() {
1874
+ return this._variableName;
1875
+ }
1876
+ get variableRef() {
1877
+ return this._variableRef;
1878
+ }
1879
+ getExpression() {
1880
+ return new ResponseVariableExpression(this._variableName, this._editorConfig);
1881
+ }
1882
+ };
1883
+ var ContextVariableEditor = class extends ExpressionEditor {
1884
+ _contextType;
1885
+ _key;
1886
+ _args;
1887
+ _asType;
1888
+ constructor(contextType, key, args, asType, editorConfig) {
1889
+ super();
1890
+ this._contextType = contextType;
1891
+ this._key = key;
1892
+ this._args = args;
1893
+ this._asType = asType;
1894
+ this._editorConfig = editorConfig;
1895
+ }
1896
+ getExpression() {
1897
+ return new ContextVariableExpression(this._contextType, this._key?.getExpression(), this._args?.map((arg) => arg.getExpression()), this._asType, this._editorConfig);
1898
+ }
1899
+ };
1900
+ var CtxLocaleEditor = class extends ContextVariableEditor {
1901
+ returnType = ValueType.string;
1902
+ constructor(editorConfig) {
1903
+ super(ContextVariableType.Locale, void 0, void 0, ValueType.string, editorConfig);
1904
+ }
1905
+ };
1906
+ var CtxPFlagIsDefinedEditor = class extends ContextVariableEditor {
1907
+ returnType = ValueType.boolean;
1908
+ constructor(key, editorConfig) {
1909
+ super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.boolean, editorConfig);
1910
+ }
1911
+ };
1912
+ var CtxPFlagStringEditor = class extends ContextVariableEditor {
1913
+ returnType = ValueType.string;
1914
+ constructor(key, editorConfig) {
1915
+ super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.string, editorConfig);
1916
+ }
1917
+ };
1918
+ var CtxPFlagNumEditor = class extends ContextVariableEditor {
1919
+ returnType = ValueType.number;
1920
+ constructor(key, editorConfig) {
1921
+ super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.number, editorConfig);
1922
+ }
1923
+ };
1924
+ var CtxPFlagDateEditor = class extends ContextVariableEditor {
1925
+ returnType = ValueType.date;
1926
+ constructor(key, editorConfig) {
1927
+ super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.date, editorConfig);
1928
+ }
1929
+ };
1930
+ var CtxCustomValueEditor = class extends ContextVariableEditor {
1931
+ constructor(key, asType, editorConfig) {
1932
+ super(ContextVariableType.CustomValue, key, void 0, asType, editorConfig);
1933
+ this.returnType = asType;
1934
+ }
1935
+ };
1936
+ var CtxCustomExpressionEditor = class extends ContextVariableEditor {
1937
+ constructor(key, args, asType, editorConfig) {
1938
+ super(ContextVariableType.CustomExpression, key, args, asType, editorConfig);
1939
+ this.returnType = asType;
1940
+ }
1941
+ };
1942
+ var GroupExpressionEditor = class extends ExpressionEditor {
1943
+ _args;
1944
+ constructor(args, editorConfig) {
1945
+ super();
1946
+ this._args = args;
1947
+ this._editorConfig = editorConfig;
1948
+ }
1949
+ get args() {
1950
+ return this._args;
1951
+ }
1952
+ addArg(arg, position) {
1953
+ if (position === void 0) this._args.push(arg);
1954
+ else this._args.splice(position, 0, arg);
1955
+ }
1956
+ removeArg(position) {
1957
+ this._args.splice(position, 1);
1958
+ }
1959
+ replaceArg(position, arg) {
1960
+ this._args[position] = arg;
1961
+ }
1962
+ swapArgs(activeIndex, overIndex) {
1963
+ const newOrder = [...this._args];
1964
+ newOrder.splice(activeIndex, 1);
1965
+ newOrder.splice(overIndex, 0, this._args[activeIndex]);
1966
+ this._args = newOrder;
1967
+ }
1968
+ };
1969
+ var AndExpressionEditor = class extends GroupExpressionEditor {
1970
+ returnType = ValueType.boolean;
1971
+ constructor(args, editorConfig) {
1972
+ if (args.some((arg) => arg.returnType !== ValueType.boolean)) throw new Error("And expression editor must have boolean arguments");
1973
+ super(args, editorConfig);
1974
+ }
1975
+ getExpression() {
1976
+ return new FunctionExpression("and", this.args.map((arg) => arg.getExpression()), this._editorConfig);
1977
+ }
1978
+ };
1979
+ var OrExpressionEditor = class extends GroupExpressionEditor {
1980
+ returnType = ValueType.boolean;
1981
+ constructor(args, editorConfig) {
1982
+ if (args.some((arg) => arg.returnType !== ValueType.boolean)) throw new Error("Or expression editor must have boolean arguments");
1983
+ super(args, editorConfig);
1984
+ }
1985
+ getExpression() {
1986
+ return new FunctionExpression("or", this.args.map((arg) => arg.getExpression()), this._editorConfig);
1987
+ }
1988
+ };
1989
+ var StrListContainsExpressionEditor = class extends ExpressionEditor {
1990
+ returnType = ValueType.boolean;
1991
+ _list;
1992
+ _item;
1993
+ constructor(list, item, editorConfig) {
1994
+ super();
1995
+ if (list.returnType !== ValueType.stringArray) throw new Error("List contains expression editor must have a string array list");
1996
+ if (item.returnType !== ValueType.string) throw new Error("List contains expression editor must have a string item");
1997
+ this._list = list;
1998
+ this._item = item;
1999
+ this._editorConfig = editorConfig;
2000
+ }
2001
+ get list() {
2002
+ return this._list;
2003
+ }
2004
+ get item() {
2005
+ return this._item;
2006
+ }
2007
+ set list(list) {
2008
+ this._list = list;
2009
+ }
2010
+ set item(item) {
2011
+ this._item = item;
2012
+ }
2013
+ getExpression() {
2014
+ return new FunctionExpression("list_contains", [this._list?.getExpression(), this._item?.getExpression()], this._editorConfig);
2015
+ }
2016
+ };
2017
+ var StrEqExpressionEditor = class extends ExpressionEditor {
2018
+ returnType = ValueType.boolean;
2019
+ _a;
2020
+ _b;
2021
+ constructor(a, b, editorConfig) {
2022
+ super();
2023
+ this._a = a;
2024
+ this._b = b;
2025
+ this._editorConfig = editorConfig;
2026
+ }
2027
+ get a() {
2028
+ return this._a;
2029
+ }
2030
+ get b() {
2031
+ return this._b;
2032
+ }
2033
+ set a(a) {
2034
+ this._a = a;
2035
+ }
2036
+ set b(b) {
2037
+ this._b = b;
2038
+ }
2039
+ getExpression() {
2040
+ return new FunctionExpression("str_eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
2041
+ }
2042
+ };
2043
+ var EqExpressionEditor = class extends ExpressionEditor {
2044
+ returnType = ValueType.boolean;
2045
+ _a;
2046
+ _b;
2047
+ constructor(a, b, editorConfig) {
2048
+ super();
2049
+ this._a = a;
2050
+ this._b = b;
2051
+ this._editorConfig = editorConfig;
2052
+ }
2053
+ get a() {
2054
+ return this._a;
2055
+ }
2056
+ get b() {
2057
+ return this._b;
2058
+ }
2059
+ set a(a) {
2060
+ this._a = a;
2061
+ }
2062
+ set b(b) {
2063
+ this._b = b;
2064
+ }
2065
+ getExpression() {
2066
+ return new FunctionExpression("eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
2067
+ }
2068
+ };
2069
+ var GtExpressionEditor = class extends ExpressionEditor {
2070
+ returnType = ValueType.boolean;
2071
+ _a;
2072
+ _b;
2073
+ constructor(a, b, editorConfig) {
2074
+ super();
2075
+ this._a = a;
2076
+ this._b = b;
2077
+ this._editorConfig = editorConfig;
2078
+ }
2079
+ get a() {
2080
+ return this._a;
2081
+ }
2082
+ get b() {
2083
+ return this._b;
2084
+ }
2085
+ set a(a) {
2086
+ this._a = a;
2087
+ }
2088
+ set b(b) {
2089
+ this._b = b;
2090
+ }
2091
+ getExpression() {
2092
+ return new FunctionExpression("gt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
2093
+ }
2094
+ };
2095
+ var GteExpressionEditor = class extends ExpressionEditor {
2096
+ returnType = ValueType.boolean;
2097
+ _a;
2098
+ _b;
2099
+ constructor(a, b, editorConfig) {
2100
+ super();
2101
+ this._a = a;
2102
+ this._b = b;
2103
+ this._editorConfig = editorConfig;
2104
+ }
2105
+ get a() {
2106
+ return this._a;
2107
+ }
2108
+ get b() {
2109
+ return this._b;
2110
+ }
2111
+ set a(a) {
2112
+ this._a = a;
2113
+ }
2114
+ set b(b) {
2115
+ this._b = b;
2116
+ }
2117
+ getExpression() {
2118
+ return new FunctionExpression("gte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
2119
+ }
2120
+ };
2121
+ var LtExpressionEditor = class extends ExpressionEditor {
2122
+ returnType = ValueType.boolean;
2123
+ _a;
2124
+ _b;
2125
+ constructor(a, b, editorConfig) {
2126
+ super();
2127
+ this._a = a;
2128
+ this._b = b;
2129
+ this._editorConfig = editorConfig;
2130
+ }
2131
+ get a() {
2132
+ return this._a;
2133
+ }
2134
+ get b() {
2135
+ return this._b;
2136
+ }
2137
+ set a(a) {
2138
+ this._a = a;
2139
+ }
2140
+ set b(b) {
2141
+ this._b = b;
2142
+ }
2143
+ getExpression() {
2144
+ return new FunctionExpression("lt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
2145
+ }
2146
+ };
2147
+ var LteExpressionEditor = class extends ExpressionEditor {
2148
+ returnType = ValueType.boolean;
2149
+ _a;
2150
+ _b;
2151
+ constructor(a, b, editorConfig) {
2152
+ super();
2153
+ this._a = a;
2154
+ this._b = b;
2155
+ this._editorConfig = editorConfig;
2156
+ }
2157
+ get a() {
2158
+ return this._a;
2159
+ }
2160
+ get b() {
2161
+ return this._b;
2162
+ }
2163
+ set b(b) {
2164
+ this._b = b;
2165
+ }
2166
+ set a(a) {
2167
+ this._a = a;
2168
+ }
2169
+ getExpression() {
2170
+ return new FunctionExpression("lte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
2171
+ }
2172
+ };
2173
+ var InRangeExpressionEditor = class extends ExpressionEditor {
2174
+ returnType = ValueType.boolean;
2175
+ value;
2176
+ min;
2177
+ max;
2178
+ inclusive;
2179
+ constructor(value, min, max, inclusive, editorConfig) {
2180
+ super();
2181
+ this.value = value;
2182
+ this.min = min;
2183
+ this.max = max;
2184
+ this.inclusive = inclusive;
2185
+ this._editorConfig = editorConfig;
2186
+ }
2187
+ getExpression() {
2188
+ return new FunctionExpression("in_range", [
2189
+ this.value?.getExpression(),
2190
+ this.min?.getExpression(),
2191
+ this.max?.getExpression(),
2192
+ this.inclusive?.getExpression()
2193
+ ], this._editorConfig);
2194
+ }
2195
+ };
2196
+ var SumExpressionEditor = class extends GroupExpressionEditor {
2197
+ returnType = ValueType.number;
2198
+ constructor(args, editorConfig) {
2199
+ super(args, editorConfig);
2200
+ }
2201
+ getExpression() {
2202
+ return new FunctionExpression("sum", this.args.map((arg) => arg.getExpression()), this._editorConfig);
2203
+ }
2204
+ };
2205
+ var MinExpressionEditor = class extends GroupExpressionEditor {
2206
+ returnType = ValueType.number;
2207
+ constructor(args, editorConfig) {
2208
+ super(args, editorConfig);
2209
+ }
2210
+ getExpression() {
2211
+ return new FunctionExpression("min", this.args.map((arg) => arg.getExpression()), this._editorConfig);
2212
+ }
2213
+ };
2214
+ var MaxExpressionEditor = class extends GroupExpressionEditor {
2215
+ returnType = ValueType.number;
2216
+ constructor(args, editorConfig) {
2217
+ super(args, editorConfig);
2218
+ }
2219
+ getExpression() {
2220
+ return new FunctionExpression("max", this.args.map((arg) => arg.getExpression()), this._editorConfig);
2221
+ }
2222
+ };
2223
+ //#endregion
2224
+ //#region src/expressions/editors/expression-editor-generators.ts
2225
+ const const_string_array = (...values) => {
2226
+ return new ConstStringArrayEditor(values);
2227
+ };
2228
+ const const_string = (value) => {
2229
+ return new ConstStringEditor(value);
2230
+ };
2231
+ const const_number_array = (...values) => {
2232
+ return new ConstNumberArrayEditor(values);
2233
+ };
2234
+ const const_number = (value) => {
2235
+ return new ConstNumberEditor(value);
2236
+ };
2237
+ const const_boolean = (value) => {
2238
+ return new ConstBooleanEditor(value);
2239
+ };
2240
+ const const_date = (value) => {
2241
+ return new ConstDateEditor(value);
2242
+ };
2243
+ const const_date_array = (...values) => {
2244
+ return new ConstDateArrayEditor(values);
2245
+ };
2246
+ const response_string = (valueRef) => {
2247
+ return new ResponseVariableEditor(valueRef, ValueType.string);
2248
+ };
2249
+ const response_string_array = (valueRef) => {
2250
+ return new ResponseVariableEditor(valueRef, ValueType.stringArray);
2251
+ };
2252
+ const response_number = (valueRef) => {
2253
+ return new ResponseVariableEditor(valueRef, ValueType.number);
2254
+ };
2255
+ const response_boolean = (valueRef) => {
2256
+ return new ResponseVariableEditor(valueRef, ValueType.boolean);
2257
+ };
2258
+ const response_date = (valueRef) => {
2259
+ return new ResponseVariableEditor(valueRef, ValueType.date);
2260
+ };
2261
+ const response_number_array = (valueRef) => {
2262
+ return new ResponseVariableEditor(valueRef, ValueType.numberArray);
2263
+ };
2264
+ const response_date_array = (valueRef) => {
2265
+ return new ResponseVariableEditor(valueRef, ValueType.dateArray);
2266
+ };
2267
+ const ctx_locale = () => {
2268
+ return new CtxLocaleEditor();
2269
+ };
2270
+ const ctx_pflag_is_defined = (key) => {
2271
+ return new CtxPFlagIsDefinedEditor(key);
2272
+ };
2273
+ const ctx_pflag_string = (key) => {
2274
+ return new CtxPFlagStringEditor(key);
2275
+ };
2276
+ const ctx_pflag_num = (key) => {
2277
+ return new CtxPFlagNumEditor(key);
2278
+ };
2279
+ const ctx_pflag_date = (key) => {
2280
+ return new CtxPFlagDateEditor(key);
2281
+ };
2282
+ const ctx_custom_value = (key, dType) => {
2283
+ return new CtxCustomValueEditor(key, dType);
2284
+ };
2285
+ const ctx_custom_expression = (key, args, returnType) => {
2286
+ return new CtxCustomExpressionEditor(key, args, returnType);
2287
+ };
2288
+ const and = (...args) => {
2289
+ return new AndExpressionEditor(args);
2290
+ };
2291
+ const or = (...args) => {
2292
+ return new OrExpressionEditor(args);
2293
+ };
2294
+ const str_list_contains = (list, item) => {
2295
+ return new StrListContainsExpressionEditor(list, item);
2296
+ };
2297
+ const str_eq = (a, b) => {
2298
+ return new StrEqExpressionEditor(a, b);
2299
+ };
2300
+ const eq = (a, b) => {
2301
+ return new EqExpressionEditor(a, b);
2302
+ };
2303
+ const gt = (a, b) => {
2304
+ return new GtExpressionEditor(a, b);
2305
+ };
2306
+ const gte = (a, b) => {
2307
+ return new GteExpressionEditor(a, b);
2308
+ };
2309
+ const lt = (a, b) => {
2310
+ return new LtExpressionEditor(a, b);
2311
+ };
2312
+ const lte = (a, b) => {
2313
+ return new LteExpressionEditor(a, b);
2314
+ };
2315
+ const in_range = (value, min, max, inclusive) => {
2316
+ return new InRangeExpressionEditor(value, min, max, inclusive);
2317
+ };
2318
+ const sum = (...args) => {
2319
+ return new SumExpressionEditor(args);
2320
+ };
2321
+ const min = (...args) => {
2322
+ return new MinExpressionEditor(args);
2323
+ };
2324
+ const max = (...args) => {
2325
+ return new MaxExpressionEditor(args);
2326
+ };
2327
+ var CalendarFunctionEditor = class extends ExpressionEditor {
2328
+ name;
2329
+ args;
2330
+ returnType;
2331
+ constructor(name, args, returnType) {
2332
+ super();
2333
+ this.name = name;
2334
+ this.args = args;
2335
+ this.returnType = returnType;
2336
+ }
2337
+ getExpression() {
2338
+ return new FunctionExpression(this.name, this.args.map((arg) => arg.getExpression()));
2339
+ }
2340
+ };
2341
+ const date_eq = (a, b) => new CalendarFunctionEditor("date_eq", [a, b], ValueType.boolean);
2342
+ const date_gt = (a, b) => new CalendarFunctionEditor("date_gt", [a, b], ValueType.boolean);
2343
+ const date_gte = (a, b) => new CalendarFunctionEditor("date_gte", [a, b], ValueType.boolean);
2344
+ const date_lt = (a, b) => new CalendarFunctionEditor("date_lt", [a, b], ValueType.boolean);
2345
+ const date_lte = (a, b) => new CalendarFunctionEditor("date_lte", [a, b], ValueType.boolean);
2346
+ const date_add_days = (a, b) => new CalendarFunctionEditor("date_add_days", [a, b], ValueType.date);
2347
+ const date_add_months = (a, b) => new CalendarFunctionEditor("date_add_months", [a, b], ValueType.date);
2348
+ const date_add_years = (a, b) => new CalendarFunctionEditor("date_add_years", [a, b], ValueType.date);
2349
+ const date_diff = (a, b) => new CalendarFunctionEditor("date_diff", [a, b], ValueType.number);
2350
+ const date_min = (...args) => new CalendarFunctionEditor("date_min", args, ValueType.date);
2351
+ const date_max = (...args) => new CalendarFunctionEditor("date_max", args, ValueType.date);
2352
+ //#endregion
2353
+ //#region src/survey/items/types.ts
2354
+ let ReservedSurveyItemTypes = /* @__PURE__ */ function(ReservedSurveyItemTypes) {
2355
+ ReservedSurveyItemTypes["Group"] = "group";
2356
+ ReservedSurveyItemTypes["PageBreak"] = "page-break";
2357
+ return ReservedSurveyItemTypes;
2358
+ }({});
2359
+ //#endregion
2360
+ //#region src/utils.ts
2361
+ function hashString(seed) {
2362
+ let hash = 2166136261;
2363
+ for (let i = 0; i < seed.length; i++) {
2364
+ hash ^= seed.charCodeAt(i);
2365
+ hash = Math.imul(hash, 16777619);
2366
+ }
2367
+ return hash >>> 0;
2368
+ }
2369
+ function createSeededRandom(seed) {
2370
+ let state = hashString(seed);
2371
+ return () => {
2372
+ state = state + 1831565813 >>> 0;
2373
+ let next = state;
2374
+ next = Math.imul(next ^ next >>> 15, next | 1);
2375
+ next ^= next + Math.imul(next ^ next >>> 7, next | 61);
2376
+ return ((next ^ next >>> 14) >>> 0) / 4294967296;
2377
+ };
2378
+ }
2379
+ function shuffleArray(values, random = Math.random) {
2380
+ const shuffledValues = [...values];
2381
+ for (let i = shuffledValues.length - 1; i > 0; i--) {
2382
+ const j = Math.floor(random() * (i + 1));
2383
+ [shuffledValues[i], shuffledValues[j]] = [shuffledValues[j], shuffledValues[i]];
2384
+ }
2385
+ return shuffledValues;
2386
+ }
2387
+ /**
2388
+ * Shuffles an array of indices using the Fisher-Yates shuffle algorithm
2389
+ * @param length - The length of the array to create indices for
2390
+ * @returns A shuffled array of indices from 0 to length-1
2391
+ */
2392
+ function shuffleIndices(length, random = Math.random) {
2393
+ return shuffleArray(Array.from({ length }, (_, i) => i), random);
2394
+ }
2395
+ function structuredCloneMethod(obj) {
2396
+ if (obj === void 0) return obj;
2397
+ if (typeof structuredClone !== "undefined") return structuredClone(obj);
2398
+ return JSON.parse(JSON.stringify(obj));
2399
+ }
2400
+ const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2401
+ function generateId() {
2402
+ const length = 10;
2403
+ const timestamp = Date.now();
2404
+ let encodedTimestamp = "";
2405
+ let t = timestamp;
2406
+ const base = 62;
2407
+ while (t > 0) {
2408
+ encodedTimestamp = ALPHABET.charAt(t % base) + encodedTimestamp;
2409
+ t = Math.floor(t / base);
2410
+ }
2411
+ let randomPart = "";
2412
+ for (let i = 0; i < length; i++) randomPart += ALPHABET.charAt(Math.floor(Math.random() * base));
2413
+ return randomPart + encodedTimestamp;
2414
+ }
2415
+ /** Alphanumeric chars without ambiguous ones (0/O, 1/I) for human-readable coding keys. */
2416
+ const CODING_KEY_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
2417
+ /**
2418
+ * Generates a short random coding key suitable for item keys in exports/codebooks.
2419
+ * @param length - Key length (default 4)
2420
+ * @returns A random string of the specified length.
2421
+ */
2422
+ function generateCodingKey(length = 4) {
2423
+ const base = 32;
2424
+ let key = "";
2425
+ for (let i = 0; i < length; i++) key += CODING_KEY_ALPHABET.charAt(Math.floor(Math.random() * base));
2426
+ return key;
2427
+ }
2428
+ /** Resolve a sibling key using the editor's stable numeric suffix convention. */
2429
+ function resolveUniqueItemKey(key, siblingKeys) {
2430
+ let candidate = key;
2431
+ let suffix = 1;
2432
+ while (siblingKeys.has(candidate)) candidate = `${key}_${suffix++}`;
2433
+ return candidate;
2434
+ }
2435
+ //#endregion
2436
+ //#region src/survey/items/utils.ts
2437
+ const displayConditionsFromJson = (json) => {
2438
+ return {
2439
+ root: json.root ? Expression.deserialize(json.root) : void 0,
2440
+ components: json.components ? Object.fromEntries(Object.entries(json.components).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0
2441
+ };
2442
+ };
2443
+ const disabledConditionsFromJson = (json) => {
2444
+ return { components: json.components ? Object.fromEntries(Object.entries(json.components).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0 };
2445
+ };
2446
+ //#endregion
2447
+ //#region src/survey/items/prefill.ts
2448
+ const SurveyItemPrefillApplyMode = { ifEmpty: "ifEmpty" };
2449
+ const SurveyItemPrefillTargetType = {
2450
+ itemResponse: "itemResponse",
2451
+ field: "field",
2452
+ embeddedField: "embeddedField"
2453
+ };
2454
+ function deserializeSurveyItemPrefill(json) {
2455
+ switch (json.source.type) {
2456
+ case "static": return {
2457
+ id: json.id,
2458
+ target: json.target,
2459
+ when: Expression.deserialize(json.when),
2460
+ apply: json.apply,
2461
+ source: {
2462
+ type: "static",
2463
+ value: json.source.value
2464
+ }
2465
+ };
2466
+ case "expression": return {
2467
+ id: json.id,
2468
+ target: json.target,
2469
+ when: Expression.deserialize(json.when),
2470
+ apply: json.apply,
2471
+ source: {
2472
+ type: "expression",
2473
+ expression: Expression.deserialize(json.source.expression) ?? (() => {
2474
+ throw new Error(`Prefill '${json.id}' is missing a valid expression source`);
2475
+ })()
2476
+ }
2477
+ };
2478
+ case "templateValue": return {
2479
+ id: json.id,
2480
+ target: json.target,
2481
+ when: Expression.deserialize(json.when),
2482
+ apply: json.apply,
2483
+ source: {
2484
+ type: "templateValue",
2485
+ key: json.source.key
2486
+ }
2487
+ };
2488
+ case "previousResponse": return {
2489
+ id: json.id,
2490
+ target: json.target,
2491
+ when: Expression.deserialize(json.when),
2492
+ apply: json.apply,
2493
+ source: {
2494
+ type: "previousResponse",
2495
+ ref: json.source.ref,
2496
+ ifUnsupported: json.source.ifUnsupported
2497
+ }
2498
+ };
2499
+ default: throw new Error(`Unsupported prefill source type: ${json.source.type}`);
2500
+ }
2501
+ }
2502
+ function serializeSurveyItemPrefill(prefill) {
2503
+ switch (prefill.source.type) {
2504
+ case "static": return {
2505
+ id: prefill.id,
2506
+ target: prefill.target,
2507
+ when: prefill.when?.serialize(),
2508
+ apply: prefill.apply,
2509
+ source: {
2510
+ type: "static",
2511
+ value: prefill.source.value
2512
+ }
2513
+ };
2514
+ case "expression": return {
2515
+ id: prefill.id,
2516
+ target: prefill.target,
2517
+ when: prefill.when?.serialize(),
2518
+ apply: prefill.apply,
2519
+ source: {
2520
+ type: "expression",
2521
+ expression: prefill.source.expression.serialize() ?? (() => {
2522
+ throw new Error(`Prefill '${prefill.id}' cannot serialize an empty expression source`);
2523
+ })()
2524
+ }
2525
+ };
2526
+ case "templateValue": return {
2527
+ id: prefill.id,
2528
+ target: prefill.target,
2529
+ when: prefill.when?.serialize(),
2530
+ apply: prefill.apply,
2531
+ source: {
2532
+ type: "templateValue",
2533
+ key: prefill.source.key
2534
+ }
2535
+ };
2536
+ case "previousResponse": return {
2537
+ id: prefill.id,
2538
+ target: prefill.target,
2539
+ when: prefill.when?.serialize(),
2540
+ apply: prefill.apply,
2541
+ source: {
2542
+ type: "previousResponse",
2543
+ ref: prefill.source.ref,
2544
+ ifUnsupported: prefill.source.ifUnsupported
2545
+ }
2546
+ };
2547
+ default: throw new Error(`Unsupported prefill source type: ${prefill.source.type}`);
2548
+ }
2549
+ }
2550
+ function prefillTargetsEqual(left, right) {
2551
+ if (left.type !== right.type) return false;
2552
+ switch (left.type) {
2553
+ case SurveyItemPrefillTargetType.itemResponse: return true;
2554
+ case SurveyItemPrefillTargetType.field: return left.fieldId === right.fieldId;
2555
+ case SurveyItemPrefillTargetType.embeddedField: return left.optionId === right.optionId && left.fieldId === right.fieldId;
2556
+ default: return false;
2557
+ }
2558
+ }
2559
+ //#endregion
2560
+ //#region src/survey/items/survey-item.ts
2561
+ /**
2562
+ * Base class for survey item handlers.
2563
+ * Each handler holds type, id, key, and config (properly typed) and implements core methods.
2564
+ * Subclass for each item type; the core works with SurveyItemHandler only—no casting needed.
2565
+ */
2566
+ var SurveyItemCore = class {
2567
+ id;
2568
+ config;
2569
+ _rawItem;
2570
+ displayConditions;
2571
+ disabledConditions;
2572
+ validations;
2573
+ prefills;
2574
+ constructor(rawItem) {
2575
+ this._rawItem = rawItem;
2576
+ this.updateExpressions();
2577
+ this.id = this._rawItem.id;
2578
+ this.config = this.parseConfig(this._rawItem.config);
2579
+ }
2580
+ /** Primitive authors can identify meaningful headings without exposing option/decorative text. */
2581
+ getOutlineContentKeys() {
2582
+ return ["title", "prompt"];
2583
+ }
2584
+ getCompositionTypes() {
2585
+ return [this.type];
2586
+ }
2587
+ /**
2588
+ * Validate/canonicalize updates, initial responses, and configured prefills before evaluation.
2589
+ * Prefills are applied one slot at a time, so multi-slot items must allow partial responses.
2590
+ */
2591
+ normalizeResponse(response) {
2592
+ return { response };
2593
+ }
2594
+ /** Opt-in component ownership for copy/paste. Existing item formats remain unchanged. */
2595
+ getOwnedComponentIds() {
2596
+ return [];
2597
+ }
2598
+ /** Slots omitted from submissions when their component condition is false; session answers are retained. */
2599
+ getConditionalResponseSlots() {
2600
+ return [];
2601
+ }
2602
+ /** Dependencies used when projecting a stored answer (for example visible choice options). */
2603
+ getActiveResponseDependencies(_slotId) {
2604
+ return {
2605
+ slotIds: [],
2606
+ componentIds: []
2607
+ };
2608
+ }
2609
+ /** Project retained data without mutating it. Dependencies must be declared above. */
2610
+ getActiveResponseValue(_slotId, value, _isVisible, _getAnswer) {
2611
+ return value;
2612
+ }
2613
+ /** Source slots used by a computed reference, for component visibility cycle diagnostics. */
2614
+ getComputedResponseDependencies(slotId) {
2615
+ return this.getResponseSlotDefinitions().find((slot) => slot.slotId === slotId)?.computed?.sourceSlotIds ?? [];
2616
+ }
2617
+ /** Computed references are read-only, evaluated on demand, never persisted/exported as answers. */
2618
+ getComputedResponseValue(_slotId, _response, _isVisible) {}
2619
+ getResponseValidationValues(_response, _isVisible, _evaluate) {
2620
+ return {};
2621
+ }
2622
+ /** Runtime-only checks; authoring may retain incomplete drafts. */
2623
+ getExecutionConfigurationIssues() {
2624
+ return [];
2625
+ }
2626
+ getAvailableResponseValueSlots(byType) {
2627
+ const valueRefs = {};
2628
+ for (const slot of this.getResponseSlotDefinitions()) {
2629
+ valueRefs[JSON.stringify([slot.slotId, "get"])] = slot.valueType;
2630
+ valueRefs[JSON.stringify([slot.slotId, "isDefined"])] = ValueType.boolean;
2631
+ }
2632
+ if (!byType) return valueRefs;
2633
+ return Object.fromEntries(Object.entries(valueRefs).filter(([, valueType]) => valueType === byType));
2634
+ }
2635
+ get rawItem() {
2636
+ return {
2637
+ ...this._rawItem,
2638
+ config: this.config
2639
+ };
2640
+ }
2641
+ get metadata() {
2642
+ return this._rawItem.metadata ?? {};
2643
+ }
2644
+ set metadata(metadata) {
2645
+ this.updateRawItem({
2646
+ ...this._rawItem,
2647
+ metadata
2648
+ });
2649
+ }
2650
+ get editorMetadata() {
2651
+ return this._rawItem.editorMetadata ?? {};
2652
+ }
2653
+ setCreatedAt(timestamp) {
2654
+ this.updateRawItem({
2655
+ ...this._rawItem,
2656
+ editorMetadata: {
2657
+ ...this._rawItem.editorMetadata,
2658
+ createdAt: timestamp
2659
+ }
2660
+ });
2661
+ }
2662
+ setModifiedAt(timestamp) {
2663
+ this.updateRawItem({
2664
+ ...this._rawItem,
2665
+ editorMetadata: {
2666
+ ...this._rawItem.editorMetadata,
2667
+ createdAt: this._rawItem.editorMetadata?.createdAt ?? timestamp,
2668
+ updatedAt: timestamp
2669
+ }
2670
+ });
2671
+ }
2672
+ updateRawItem(rawItem) {
2673
+ if (rawItem.id !== this.id) throw new Error("Cannot update raw item with a different id");
2674
+ this._rawItem = rawItem;
2675
+ this.updateExpressions();
2676
+ this.config = this.parseConfig(rawItem.config);
2677
+ }
2678
+ updateExpressions() {
2679
+ this.displayConditions = this._rawItem.displayConditions ? displayConditionsFromJson(this._rawItem.displayConditions) : void 0;
2680
+ this.disabledConditions = this._rawItem.disabledConditions ? disabledConditionsFromJson(this._rawItem.disabledConditions) : void 0;
2681
+ this.validations = this._rawItem.validations ? Object.fromEntries(Object.entries(this._rawItem.validations).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0;
2682
+ this.prefills = this._rawItem.prefills?.map((prefill) => deserializeSurveyItemPrefill(prefill));
2683
+ }
2684
+ getReferenceUsages() {
2685
+ const usages = [];
2686
+ if (this.displayConditions) {
2687
+ for (const ref of this.displayConditions.root?.responseVariableRefs || []) usages.push({
2688
+ itemId: this.id,
2689
+ usageType: "displayConditions",
2690
+ valueReference: ref
2691
+ });
2692
+ for (const [componentKey, expression] of Object.entries(this.displayConditions.components || {})) for (const ref of expression?.responseVariableRefs || []) usages.push({
2693
+ itemId: this.id,
2694
+ fullComponentKey: componentKey,
2695
+ usageType: "displayConditions",
2696
+ valueReference: ref
2697
+ });
2698
+ }
2699
+ if (this.disabledConditions) for (const [componentKey, expression] of Object.entries(this.disabledConditions.components || {})) for (const ref of expression?.responseVariableRefs || []) usages.push({
2700
+ itemId: this.id,
2701
+ fullComponentKey: componentKey,
2702
+ usageType: "disabledConditions",
2703
+ valueReference: ref
2704
+ });
2705
+ if (this.validations) for (const [validationKey, expression] of Object.entries(this.validations)) for (const ref of expression?.responseVariableRefs || []) usages.push({
2706
+ itemId: this.id,
2707
+ fullComponentKey: validationKey,
2708
+ usageType: "validations",
2709
+ valueReference: ref
2710
+ });
2711
+ if (this.prefills) for (const prefill of this.prefills) {
2712
+ for (const ref of prefill.when?.responseVariableRefs || []) usages.push({
2713
+ itemId: this.id,
2714
+ fullComponentKey: prefill.id,
2715
+ usageType: "prefills",
2716
+ valueReference: ref
2717
+ });
2718
+ if (prefill.source.type === "expression") for (const ref of prefill.source.expression.responseVariableRefs || []) usages.push({
2719
+ itemId: this.id,
2720
+ fullComponentKey: prefill.id,
2721
+ usageType: "prefills",
2722
+ valueReference: ref
2723
+ });
2724
+ }
2725
+ for (const { expression, location } of collectConfigValueExpressions(this.config)) for (const ref of collectExpressionResponseReferences(expression)) usages.push({
2726
+ itemId: this.id,
2727
+ fullComponentKey: location,
2728
+ usageType: "validations",
2729
+ valueReference: ref
2730
+ });
2731
+ return usages;
2732
+ }
2733
+ resolvePrefillTarget(target) {
2734
+ const slotDefinitions = this.getResponseSlotDefinitions();
2735
+ const explicitMatch = slotDefinitions.find((slot) => slot.prefillTarget !== void 0 && prefillTargetsEqual(slot.prefillTarget, target));
2736
+ if (explicitMatch) return explicitMatch;
2737
+ if (target.type === SurveyItemPrefillTargetType.itemResponse && slotDefinitions.length === 1) return slotDefinitions[0];
2738
+ }
2739
+ normalizePrefillValue(_prefill, targetSlot, value) {
2740
+ return value.type === targetSlot.valueType ? value : void 0;
2741
+ }
2742
+ };
2743
+ //#endregion
2744
+ //#region src/survey/item-display-name.ts
2745
+ function getCompositionLabel(types, locale) {
2746
+ const language = locale.split("-")[0];
2747
+ const labels = {
2748
+ en: {
2749
+ choiceItem: "Choice",
2750
+ formItem: "Form",
2751
+ formMatrix: "Form matrix",
2752
+ choiceMatrix: "Choice matrix",
2753
+ consentItem: "Consent",
2754
+ infoItem: "Information",
2755
+ group: "Group",
2756
+ "page-break": "Page break",
2757
+ empty: "Empty item"
2758
+ },
2759
+ nl: {
2760
+ choiceItem: "Keuze",
2761
+ formItem: "Formulier",
2762
+ formMatrix: "Formuliermatrix",
2763
+ choiceMatrix: "Keuzematrix",
2764
+ consentItem: "Toestemming",
2765
+ infoItem: "Informatie",
2766
+ group: "Groep",
2767
+ "page-break": "Pagina-einde",
2768
+ empty: "Leeg item"
2769
+ },
2770
+ de: {
2771
+ choiceItem: "Auswahl",
2772
+ formItem: "Formular",
2773
+ formMatrix: "Formularmatrix",
2774
+ choiceMatrix: "Auswahlmatrix",
2775
+ consentItem: "Einwilligung",
2776
+ infoItem: "Information",
2777
+ group: "Gruppe",
2778
+ "page-break": "Seitenumbruch",
2779
+ empty: "Leeres Element"
2780
+ }
2781
+ };
2782
+ const dictionary = labels[language] ?? labels.en;
2783
+ return [...new Set(types)].map((type) => dictionary[type]).filter(Boolean).join(" + ") || dictionary.empty;
2784
+ }
2785
+ //#endregion
2786
+ //#region src/survey/utils/group-utils.ts
2787
+ /**
2788
+ * Shared utilities for array operations.
2789
+ */
2790
+ /**
2791
+ * Swap two elements in an array by index.
2792
+ * @returns A new array with the elements swapped
2793
+ */
2794
+ function swapArrayElements(arr, fromIndex, toIndex) {
2795
+ const result = [...arr];
2796
+ [result[fromIndex], result[toIndex]] = [result[toIndex], result[fromIndex]];
2797
+ return result;
2798
+ }
2799
+ /**
2800
+ * Move an element from one index to another.
2801
+ * @returns A new array with the element moved
2802
+ */
2803
+ function moveArrayElement(arr, fromIndex, toIndex) {
2804
+ const result = [...arr];
2805
+ const [moved] = result.splice(fromIndex, 1);
2806
+ result.splice(toIndex, 0, moved);
2807
+ return result;
2808
+ }
2809
+ //#endregion
2810
+ //#region src/survey/registry/built-in-items.ts
2811
+ var GroupItemCore = class extends SurveyItemCore {
2812
+ type = "group";
2813
+ parseConfig(rawConfig) {
2814
+ const cfg = rawConfig ?? {};
2815
+ return {
2816
+ items: cfg.items,
2817
+ shuffleItems: cfg.shuffleItems,
2818
+ isRoot: cfg.isRoot
2819
+ };
2820
+ }
2821
+ serializeConfig() {
2822
+ return this.config;
2823
+ }
2824
+ isInteractive() {
2825
+ return false;
2826
+ }
2827
+ getResponseSlotDefinitions() {
2828
+ return [];
2829
+ }
2830
+ isRoot() {
2831
+ return this.config.isRoot ?? false;
2832
+ }
2833
+ get items() {
2834
+ return this.config.items ?? [];
2835
+ }
2836
+ /**
2837
+ * Add a child item id to this group at the given index.
2838
+ * @param itemId The id of the item to add to this group.
2839
+ * @param index The index at which to add the item. If not provided, the item is added to the end of the group.
2840
+ */
2841
+ addChild(itemId, index) {
2842
+ const items = this.config.items ??= [];
2843
+ const insertIndex = index !== void 0 ? Math.min(index, items.length) : items.length;
2844
+ items.splice(insertIndex, 0, itemId);
2845
+ this.updateRawItem({
2846
+ ...this.rawItem,
2847
+ config: { ...this.config }
2848
+ });
2849
+ }
2850
+ /**
2851
+ * Remove a child item id from this group.
2852
+ * @param itemId The id of the item to remove from this group.
2853
+ */
2854
+ removeChild(itemId) {
2855
+ const items = this.config.items ??= [];
2856
+ const index = items.indexOf(itemId);
2857
+ if (index !== -1) {
2858
+ items.splice(index, 1);
2859
+ this.updateRawItem({
2860
+ ...this.rawItem,
2861
+ config: { ...this.config }
2862
+ });
2863
+ }
2864
+ }
2865
+ /**
2866
+ * Check if an item is a direct child of this group.
2867
+ */
2868
+ hasChild(itemId) {
2869
+ return this.items.includes(itemId);
2870
+ }
2871
+ /** Whether items in this group should be shuffled. */
2872
+ get shuffleItems() {
2873
+ return this.config.shuffleItems ?? false;
2874
+ }
2875
+ set shuffleItems(value) {
2876
+ this.config.shuffleItems = value;
2877
+ this.updateRawItem({
2878
+ ...this.rawItem,
2879
+ config: { ...this.config }
2880
+ });
2881
+ }
2882
+ /**
2883
+ * Get the ordered list of child item ids.
2884
+ */
2885
+ getChildrenIds() {
2886
+ return [...this.items];
2887
+ }
2888
+ /**
2889
+ * Swap two items by their positions in the group.
2890
+ * @throws Error if indices are out of bounds
2891
+ */
2892
+ swapItemsByIndex(from, to) {
2893
+ const items = this.items;
2894
+ if (from < 0 || from >= items.length || to < 0 || to >= items.length) throw new Error(`Index out of bounds. Valid range is 0-${items.length - 1}`);
2895
+ if (from === to) return;
2896
+ this.config.items = swapArrayElements(items, from, to);
2897
+ this.updateRawItem({
2898
+ ...this.rawItem,
2899
+ config: { ...this.config }
2900
+ });
2901
+ }
2902
+ /**
2903
+ * Swap two items by their ids.
2904
+ * @throws Error if either id is not found in this group
2905
+ */
2906
+ swapItemsById(id, withId) {
2907
+ const items = this.items;
2908
+ const index1 = items.indexOf(id);
2909
+ const index2 = items.indexOf(withId);
2910
+ if (index1 === -1) throw new Error(`Item '${id}' not found in group '${this.metadata?.itemLabel ?? ""}'`);
2911
+ if (index2 === -1) throw new Error(`Item '${withId}' not found in group '${this.metadata?.itemLabel ?? ""}'`);
2912
+ this.swapItemsByIndex(index1, index2);
2913
+ }
2914
+ /**
2915
+ * Move an item by id to a specific index.
2916
+ * @throws Error if the item is not found or index is out of bounds
2917
+ */
2918
+ moveItemToIndex(id, index) {
2919
+ const items = this.items;
2920
+ const fromIndex = items.indexOf(id);
2921
+ if (fromIndex === -1) throw new Error(`Item '${id}' not found in group '${this.metadata?.itemLabel ?? ""}'`);
2922
+ if (index < 0 || index >= items.length) throw new Error(`Index out of bounds. Valid range is 0-${items.length - 1}`);
2923
+ if (fromIndex === index) return;
2924
+ this.config.items = moveArrayElement(items, fromIndex, index);
2925
+ this.updateRawItem({
2926
+ ...this.rawItem,
2927
+ config: { ...this.config }
2928
+ });
2929
+ }
2930
+ };
2931
+ var PageBreakItemCore = class extends SurveyItemCore {
2932
+ type = "page-break";
2933
+ parseConfig(_rawConfig) {
2934
+ return {};
2935
+ }
2936
+ serializeConfig() {}
2937
+ isInteractive() {
2938
+ return false;
2939
+ }
2940
+ getResponseSlotDefinitions() {
2941
+ return [];
2942
+ }
2943
+ };
2944
+ const builtInItemCoreRegistry = {
2945
+ ["group"]: GroupItemCore,
2946
+ ["page-break"]: PageBreakItemCore
2947
+ };
2948
+ const isBuiltInItemType = (itemType) => itemType in builtInItemCoreRegistry;
2949
+ //#endregion
2950
+ //#region src/survey/registry/item-registry.ts
2951
+ /** Convert constructor registry to explicit definitions for higher-layer composition. */
2952
+ function toItemTypeDefinitionRegistry(registry) {
2953
+ return Object.fromEntries(Object.entries(registry).map(([itemType, constructor]) => [itemType, {
2954
+ itemType,
2955
+ constructor
2956
+ }]));
2957
+ }
2958
+ /**
2959
+ * Creates a handler instance for a raw survey item.
2960
+ * Uses built-in registry for group/page-break, otherwise the provided plugin registry.
2961
+ */
2962
+ function createItemCore(rawItem, pluginRegistry) {
2963
+ if (isBuiltInItemType(rawItem.itemType)) {
2964
+ const HandlerClass = builtInItemCoreRegistry[rawItem.itemType];
2965
+ return new HandlerClass(rawItem);
2966
+ }
2967
+ if (pluginRegistry && rawItem.itemType in pluginRegistry) {
2968
+ const HandlerClass = pluginRegistry[rawItem.itemType];
2969
+ return new HandlerClass(rawItem);
2970
+ }
2971
+ throw new Error(`Unknown item type: ${rawItem.itemType}`);
2972
+ }
2973
+ /**
2974
+ * Merge built-in handlers with plugin registry for a complete registry.
2975
+ */
2976
+ function createFullRegistry(pluginRegistry) {
2977
+ return {
2978
+ ...builtInItemCoreRegistry,
2979
+ ...pluginRegistry
2980
+ };
2981
+ }
2982
+ /** Build full registry as explicit definitions for editor/ui composition. */
2983
+ function createItemTypeDefinitionRegistry(pluginRegistry) {
2984
+ return toItemTypeDefinitionRegistry(createFullRegistry(pluginRegistry));
2985
+ }
2986
+ //#endregion
2987
+ //#region src/survey/survey-file-schema.ts
2988
+ const CURRENT_SURVEY_SCHEMA_VERSION = 2;
2989
+ const CURRENT_SURVEY_SCHEMA = "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json";
2990
+ //#endregion
2991
+ //#region src/survey/utils/translations.ts
2992
+ const validateLocale = (locale) => {
2993
+ if (locale.trim() === "") throw new Error("Locale cannot be empty");
2994
+ };
2995
+ var SurveyItemTranslations = class SurveyItemTranslations {
2996
+ _translations;
2997
+ constructor() {
2998
+ this._translations = {};
2999
+ }
3000
+ setContent(locale, contentKey, content) {
3001
+ validateLocale(locale);
3002
+ if (!this._translations?.[locale]) {
3003
+ if (!content) return;
3004
+ this._translations[locale] = {};
3005
+ }
3006
+ if (!content) delete this._translations[locale][contentKey];
3007
+ else this._translations[locale][contentKey] = content;
3008
+ }
3009
+ setAllForLocale(locale, content) {
3010
+ validateLocale(locale);
3011
+ if (!this._translations?.[locale]) {
3012
+ if (!content) return;
3013
+ this._translations[locale] = {};
3014
+ }
3015
+ this._translations[locale] = content || {};
3016
+ }
3017
+ get locales() {
3018
+ return Object.keys(this._translations || {});
3019
+ }
3020
+ getAllForLocale(locale) {
3021
+ return this._translations?.[locale];
3022
+ }
3023
+ getContent(locale, contentKey, fallbackLocale) {
3024
+ const content = this._translations?.[locale]?.[contentKey];
3025
+ if (content) return content;
3026
+ if (fallbackLocale) return this._translations?.[fallbackLocale]?.[contentKey];
3027
+ }
3028
+ /**
3029
+ * Create a deep clone that can be safely mutated before persisting with
3030
+ * `setItemTranslations` or `updateItemTranslations`.
3031
+ */
3032
+ clone() {
3033
+ const cloned = new SurveyItemTranslations();
3034
+ for (const locale of this.locales) {
3035
+ const localeContent = this.getAllForLocale(locale);
3036
+ if (!localeContent) continue;
3037
+ cloned.setAllForLocale(locale, structuredCloneMethod(localeContent));
3038
+ }
3039
+ return cloned;
3040
+ }
3041
+ /**
3042
+ * Remove a single content key from every locale in this translation set.
3043
+ */
3044
+ removeContentKey(contentKey) {
3045
+ for (const locale of this.locales) delete this._translations?.[locale]?.[contentKey];
3046
+ }
3047
+ /**
3048
+ * Remove every content key that is exactly the prefix or nested beneath it.
3049
+ */
3050
+ removeContentKeysWithPrefix(prefix) {
3051
+ for (const locale of this.locales) {
3052
+ const localeContent = this._translations?.[locale];
3053
+ if (!localeContent) continue;
3054
+ for (const contentKey of Object.keys(localeContent)) if (contentKey === prefix || contentKey.startsWith(prefix + ".")) delete localeContent[contentKey];
3055
+ }
3056
+ }
3057
+ /**
3058
+ * Remove empty locales in place.
3059
+ * Returns `undefined` when no translations remain so callers can pass the
3060
+ * result directly into `setItemTranslations` / `updateItemTranslations`.
3061
+ */
3062
+ compact() {
3063
+ for (const locale of this.locales) {
3064
+ const localeContent = this._translations?.[locale];
3065
+ if (!localeContent) continue;
3066
+ if (Object.keys(localeContent).length === 0) delete this._translations?.[locale];
3067
+ }
3068
+ return this.locales.length > 0 ? this : void 0;
3069
+ }
3070
+ };
3071
+ var SurveyTranslations = class SurveyTranslations {
3072
+ _translations;
3073
+ constructor(translations) {
3074
+ this._translations = structuredCloneMethod(translations || {});
3075
+ }
3076
+ /** Cheap revision fork. Every writer replaces its path before changing it. */
3077
+ fork() {
3078
+ const next = new SurveyTranslations();
3079
+ next._translations = this._translations;
3080
+ return next;
3081
+ }
3082
+ serialize() {
3083
+ return this.locales.length ? structuredCloneMethod(this._translations) : void 0;
3084
+ }
3085
+ get locales() {
3086
+ return Object.keys(this._translations);
3087
+ }
3088
+ replaceLocale(locale, value) {
3089
+ this._translations = {
3090
+ ...this._translations,
3091
+ [locale]: value
3092
+ };
3093
+ }
3094
+ replaceItem(locale, itemId, fields, keepEmpty = false) {
3095
+ const current = this._translations[locale] ?? {};
3096
+ const items = { ...current.itemTranslations };
3097
+ if (fields && (keepEmpty || Object.keys(fields).length)) items[itemId] = fields;
3098
+ else delete items[itemId];
3099
+ this.replaceLocale(locale, {
3100
+ ...current,
3101
+ itemTranslations: items
3102
+ });
3103
+ }
3104
+ addLocale(locale) {
3105
+ validateLocale(locale);
3106
+ if (this._translations[locale]) return false;
3107
+ this.replaceLocale(locale, {});
3108
+ return true;
3109
+ }
3110
+ removeLocale(locale) {
3111
+ validateLocale(locale);
3112
+ const next = { ...this._translations };
3113
+ delete next[locale];
3114
+ this._translations = next;
3115
+ }
3116
+ renameLocale(oldLocale, newLocale) {
3117
+ validateLocale(oldLocale);
3118
+ validateLocale(newLocale);
3119
+ if (this._translations[oldLocale]) {
3120
+ this.replaceLocale(newLocale, this._translations[oldLocale]);
3121
+ this.removeLocale(oldLocale);
3122
+ }
3123
+ }
3124
+ cloneLocaleAs(locale, newLocale) {
3125
+ validateLocale(locale);
3126
+ validateLocale(newLocale);
3127
+ if (this._translations[locale]) this.replaceLocale(newLocale, this._translations[locale]);
3128
+ }
3129
+ get surveyCardContent() {
3130
+ const result = {};
3131
+ for (const locale of this.locales) {
3132
+ const content = this._translations[locale].surveyCardContent;
3133
+ if (content) result[locale] = structuredCloneMethod(content);
3134
+ }
3135
+ return result;
3136
+ }
3137
+ get navigationContent() {
3138
+ const result = {};
3139
+ for (const locale of this.locales) {
3140
+ const content = this._translations[locale].navigationContent;
3141
+ if (content) result[locale] = structuredCloneMethod(content);
3142
+ }
3143
+ return result;
3144
+ }
3145
+ get validationMessages() {
3146
+ const result = {};
3147
+ for (const locale of this.locales) {
3148
+ const content = this._translations[locale].validationMessages;
3149
+ if (content) result[locale] = structuredCloneMethod(content);
3150
+ }
3151
+ return result;
3152
+ }
3153
+ setSurveyCardContent(locale, content) {
3154
+ validateLocale(locale);
3155
+ if (!content && !this._translations[locale]) return;
3156
+ this.replaceLocale(locale, {
3157
+ ...this._translations[locale],
3158
+ surveyCardContent: structuredCloneMethod(content)
3159
+ });
3160
+ }
3161
+ setNavigationContent(locale, content) {
3162
+ validateLocale(locale);
3163
+ if (!content && !this._translations[locale]) return;
3164
+ this.replaceLocale(locale, {
3165
+ ...this._translations[locale],
3166
+ navigationContent: structuredCloneMethod(content)
3167
+ });
3168
+ }
3169
+ setValidationMessages(locale, content) {
3170
+ validateLocale(locale);
3171
+ if (!content && !this._translations[locale]) return;
3172
+ this.replaceLocale(locale, {
3173
+ ...this._translations[locale],
3174
+ validationMessages: structuredCloneMethod(content)
3175
+ });
3176
+ }
3177
+ getItemTranslationContent(itemId, locale, contentKey, fallbackLocale) {
3178
+ return structuredCloneMethod(this._translations[locale]?.itemTranslations?.[itemId]?.[contentKey] ?? (fallbackLocale ? this._translations[fallbackLocale]?.itemTranslations?.[itemId]?.[contentKey] : void 0));
3179
+ }
3180
+ setItemTranslationContent(itemId, locale, contentKey, content) {
3181
+ validateLocale(locale);
3182
+ const current = this._translations[locale]?.itemTranslations?.[itemId];
3183
+ if (JSON.stringify(current?.[contentKey]) === JSON.stringify(content)) return false;
3184
+ const fields = { ...current };
3185
+ if (content === void 0) delete fields[contentKey];
3186
+ else fields[contentKey] = structuredCloneMethod(content);
3187
+ this.replaceItem(locale, itemId, fields);
3188
+ return true;
3189
+ }
3190
+ getItemTranslations(itemId) {
3191
+ const result = new SurveyItemTranslations();
3192
+ for (const locale of this.locales) result.setAllForLocale(locale, structuredCloneMethod(this._translations[locale].itemTranslations?.[itemId]));
3193
+ return result.locales.length ? result : void 0;
3194
+ }
3195
+ setItemTranslations(itemId, content) {
3196
+ const normalized = content?.clone().compact();
3197
+ for (const locale of /* @__PURE__ */ new Set([...this.locales, ...normalized?.locales ?? []])) {
3198
+ validateLocale(locale);
3199
+ const fields = normalized?.getAllForLocale(locale);
3200
+ if (fields || this._translations[locale]?.itemTranslations?.[itemId]) this.replaceItem(locale, itemId, fields);
3201
+ }
3202
+ }
3203
+ onComponentKeyChanged(itemId, oldKey, newKey) {
3204
+ for (const locale of this.locales) {
3205
+ const current = this._translations[locale].itemTranslations?.[itemId];
3206
+ if (!current) continue;
3207
+ const fields = { ...current };
3208
+ for (const key of Object.keys(current)) if (key === oldKey || key.startsWith(oldKey + ".")) {
3209
+ fields[key.replace(oldKey, newKey)] = current[key];
3210
+ delete fields[key];
3211
+ }
3212
+ this.replaceItem(locale, itemId, fields, true);
3213
+ }
3214
+ }
3215
+ onComponentDeleted(itemId, componentKey) {
3216
+ for (const locale of this.locales) {
3217
+ const current = this._translations[locale].itemTranslations?.[itemId];
3218
+ if (!current) continue;
3219
+ const fields = { ...current };
3220
+ for (const key of Object.keys(current)) if (key === componentKey || key.startsWith(componentKey + ".")) delete fields[key];
3221
+ this.replaceItem(locale, itemId, fields, true);
3222
+ }
3223
+ }
3224
+ onItemDeleted(itemId) {
3225
+ for (const locale of this.locales) if (this._translations[locale].itemTranslations?.[itemId]) this.replaceItem(locale, itemId);
3226
+ }
3227
+ };
3228
+ //#endregion
3229
+ //#region src/survey/utils/content.ts
3230
+ let ContentType = /* @__PURE__ */ function(ContentType) {
3231
+ ContentType["richText"] = "richText";
3232
+ ContentType["plain"] = "plain";
3233
+ ContentType["md"] = "md";
3234
+ return ContentType;
3235
+ }({});
3236
+ function hasRenderableRichTextBlock(block) {
3237
+ switch (block.type) {
3238
+ case "paragraph": return block.children.length > 0;
3239
+ case "heading": return block.children.length > 0;
3240
+ case "bulletList": return block.items.some((item) => item.children.some((paragraph) => paragraph.children.length > 0));
3241
+ case "image": return true;
3242
+ case "infoBox": return true;
3243
+ case "separator": return true;
3244
+ case "blockExtension": return true;
3245
+ }
3246
+ }
3247
+ function hasRenderableRichTextContent(content) {
3248
+ return content.doc.blocks.some((block) => hasRenderableRichTextBlock(block));
3249
+ }
3250
+ function createRichTextContent(text = "") {
3251
+ return {
3252
+ type: "richText",
3253
+ version: 1,
3254
+ doc: {
3255
+ type: "doc",
3256
+ blocks: [{
3257
+ type: "paragraph",
3258
+ children: text.length > 0 ? text.split("\n").flatMap((line, index) => {
3259
+ const parts = [];
3260
+ if (index > 0) parts.push({ type: "lineBreak" });
3261
+ if (line.length > 0) parts.push({
3262
+ type: "text",
3263
+ text: line
3264
+ });
3265
+ return parts;
3266
+ }) : []
3267
+ }]
3268
+ }
3269
+ };
3270
+ }
3271
+ function getPlainTextFromLinkChildren(children) {
3272
+ return children.map((inline) => {
3273
+ switch (inline.type) {
3274
+ case "text": return inline.text;
3275
+ case "template": return `{${inline.key}}`;
3276
+ case "lineBreak": return "\n";
3277
+ }
3278
+ }).join("");
3279
+ }
3280
+ function getPlainTextFromInlineNodes(inlines) {
3281
+ return inlines.map((inline) => {
3282
+ switch (inline.type) {
3283
+ case "text": return inline.text;
3284
+ case "template": return `{${inline.key}}`;
3285
+ case "link": return getPlainTextFromLinkChildren(inline.children);
3286
+ case "lineBreak": return "\n";
3287
+ case "inlineExtension": return "";
3288
+ }
3289
+ }).join("");
3290
+ }
3291
+ function getPlainTextFromParagraphs(paragraphs) {
3292
+ return paragraphs.map((paragraph) => getPlainTextFromInlineNodes(paragraph.children)).join("\n");
3293
+ }
3294
+ function getPlainTextFromBlock(block) {
3295
+ switch (block.type) {
3296
+ case "paragraph": return getPlainTextFromInlineNodes(block.children);
3297
+ case "heading": return getPlainTextFromInlineNodes(block.children);
3298
+ case "bulletList": return block.items.map((item) => getPlainTextFromParagraphs(item.children)).join("\n");
3299
+ case "image": {
3300
+ const captionText = block.caption ? getPlainTextFromParagraphs(block.caption) : "";
3301
+ return [block.alt ?? "", captionText].filter(Boolean).join("\n");
3302
+ }
3303
+ case "infoBox": return [block.title ? getPlainTextFromInlineNodes(block.title) : "", getPlainTextFromParagraphs(block.children)].filter(Boolean).join("\n");
3304
+ case "separator": return "";
3305
+ case "blockExtension": return "";
3306
+ }
3307
+ }
3308
+ function getPlainTextFromRichTextContent(content) {
3309
+ return content.doc.blocks.map((block) => getPlainTextFromBlock(block)).filter((text) => text.length > 0).join("\n").trim();
3310
+ }
3311
+ function getContentPlainText(content) {
3312
+ if (!content) return "";
3313
+ if (content.type !== "richText") return content.content ?? "";
3314
+ return getPlainTextFromRichTextContent(content);
3315
+ }
3316
+ function isContentEmpty(content) {
3317
+ if (!content) return true;
3318
+ if (content.type !== "richText") return content.content.trim().length === 0;
3319
+ return !hasRenderableRichTextContent(content);
3320
+ }
3321
+ function getAssetUsagesFromContent(content) {
3322
+ if (!content || content.type !== "richText") return [];
3323
+ const usages = [];
3324
+ content.doc.blocks.forEach((block, blockIndex) => {
3325
+ if (block.type === "image" && block.source.type === "asset") usages.push({
3326
+ assetId: block.source.assetId,
3327
+ blockIndex
3328
+ });
3329
+ });
3330
+ return usages;
3331
+ }
3332
+ //#endregion
3333
+ //#region src/survey/survey.ts
3334
+ var Survey = class Survey {
3335
+ pluginRegistry;
3336
+ maxItemsPerPage;
3337
+ metadata;
3338
+ surveyItems = /* @__PURE__ */ new Map();
3339
+ _templateValues = /* @__PURE__ */ new Map();
3340
+ _assets = /* @__PURE__ */ new Map();
3341
+ _translations;
3342
+ constructor(pluginRegistry) {
3343
+ this.pluginRegistry = pluginRegistry;
3344
+ this._translations = new SurveyTranslations();
3345
+ }
3346
+ /**
3347
+ * Derive a read revision from an already detached view. Editable core instances
3348
+ * are never reused: the changed item is reconstructed from owned raw data.
3349
+ * Other detached items are unchanged by translation publication.
3350
+ */
3351
+ withItemTranslationContent(rawItem, locale, contentKey, content) {
3352
+ const next = new Survey(this.pluginRegistry);
3353
+ next.surveyItems = new Map(this.surveyItems);
3354
+ next.surveyItems.set(rawItem.id, next.createItemFromRaw(structuredCloneMethod(rawItem)));
3355
+ next._translations = this.translations.fork();
3356
+ next._translations.setItemTranslationContent(rawItem.id, locale, contentKey, content);
3357
+ next._assets = this._assets;
3358
+ next._templateValues = this._templateValues;
3359
+ next.metadata = this.metadata;
3360
+ next.maxItemsPerPage = this.maxItemsPerPage;
3361
+ return next;
3362
+ }
3363
+ /** Plugin registry used when parsing items. Exposed for editors that re-parse without their own registry. */
3364
+ getPluginRegistry() {
3365
+ return this.pluginRegistry;
3366
+ }
3367
+ /**
3368
+ * Create a survey item from raw JSON data.
3369
+ * Uses the survey's plugin registry when available.
3370
+ */
3371
+ createItemFromRaw(rawItem) {
3372
+ return createItemCore(rawItem, this.pluginRegistry);
3373
+ }
3374
+ /**
3375
+ * Create a minimal blank survey with a root group and one empty page.
3376
+ */
3377
+ static createBlankSurvey(pluginRegistry, surveyKey) {
3378
+ const rawSurvey = {
3379
+ $schema: CURRENT_SURVEY_SCHEMA,
3380
+ schemaVersion: 2,
3381
+ metadata: { surveyKey: surveyKey ?? "survey" },
3382
+ surveyItems: [{
3383
+ id: generateId(),
3384
+ itemType: "group",
3385
+ config: {
3386
+ isRoot: true,
3387
+ items: [],
3388
+ shuffleItems: false
3389
+ }
3390
+ }]
3391
+ };
3392
+ return Survey.fromJson(rawSurvey, pluginRegistry);
3393
+ }
3394
+ static fromJson(json, pluginRegistry) {
3395
+ const survey = new Survey(pluginRegistry);
3396
+ const rawSurvey = json;
3397
+ if (rawSurvey.$schema !== "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json") throw new Error(`Unsupported survey schema: ${rawSurvey.$schema}`);
3398
+ if (rawSurvey.schemaVersion !== 2) throw new Error(`Unsupported survey format version: ${String(rawSurvey.schemaVersion)}. Recreate this draft using the current schema.`);
3399
+ survey.surveyItems = /* @__PURE__ */ new Map();
3400
+ if (!rawSurvey.surveyItems || rawSurvey.surveyItems.length === 0) throw new Error("surveyItems is required");
3401
+ rawSurvey.surveyItems.forEach((item) => {
3402
+ if (survey.surveyItems.has(item.id)) throw new Error(`Duplicate survey item identity: ${item.id}`);
3403
+ const surveyItem = createItemCore(item, pluginRegistry);
3404
+ survey.surveyItems.set(surveyItem.id, surveyItem);
3405
+ });
3406
+ if (rawSurvey.templateValues) survey._templateValues = deserializeTemplateValues(rawSurvey.templateValues);
3407
+ if (rawSurvey.assets) survey._assets = new Map(Object.entries(rawSurvey.assets));
3408
+ survey._translations = new SurveyTranslations(rawSurvey.translations);
3409
+ if (rawSurvey.maxItemsPerPage) survey.maxItemsPerPage = rawSurvey.maxItemsPerPage;
3410
+ if (rawSurvey.metadata) survey.metadata = rawSurvey.metadata;
3411
+ return survey;
3412
+ }
3413
+ serialize() {
3414
+ const json = {
3415
+ $schema: CURRENT_SURVEY_SCHEMA,
3416
+ schemaVersion: 2,
3417
+ surveyItems: Array.from(this.surveyItems.values()).map((item) => item.rawItem)
3418
+ };
3419
+ json.translations = this._translations?.serialize();
3420
+ if (this._templateValues) json.templateValues = serializeTemplateValues(this._templateValues);
3421
+ if (this._assets && this._assets.size > 0) json.assets = Object.fromEntries(this._assets.entries());
3422
+ if (this.maxItemsPerPage) json.maxItemsPerPage = this.maxItemsPerPage;
3423
+ if (this.metadata) json.metadata = this.metadata;
3424
+ return json;
3425
+ }
3426
+ get surveyKey() {
3427
+ return this.metadata?.surveyKey;
3428
+ }
3429
+ resolveItemEditorName(itemId, locale = this.locales[0] ?? "en") {
3430
+ const item = this.surveyItems.get(itemId);
3431
+ if (!item) return getCompositionLabel([], locale);
3432
+ const explicit = item.metadata.itemLabel?.trim();
3433
+ if (explicit) return explicit;
3434
+ const translations = this.getItemTranslations(itemId);
3435
+ for (const key of item.getOutlineContentKeys()) {
3436
+ const content = translations?.getContent(locale, key, this.locales[0]);
3437
+ if (content) {
3438
+ const summary = getContentPlainText(content).replace(/\s+/g, " ").trim();
3439
+ if (summary) return summary;
3440
+ }
3441
+ }
3442
+ return getCompositionLabel(item.getCompositionTypes(), locale);
3443
+ }
3444
+ get locales() {
3445
+ return this._translations?.locales || [];
3446
+ }
3447
+ get rootItem() {
3448
+ return Array.from(this.surveyItems.values()).find((item) => item instanceof GroupItemCore && item.isRoot);
3449
+ }
3450
+ get itemOutline() {
3451
+ return [...this.surveyItems.keys()].map((id) => this.getItemDisplayPath(id));
3452
+ }
3453
+ getItemPath(itemId) {
3454
+ const item = this.surveyItems.get(itemId);
3455
+ if (!item) throw new Error(`Item ${itemId} not found`);
3456
+ if (item instanceof GroupItemCore && item.isRoot()) return [];
3457
+ const parentItem = this.getParentItem(itemId);
3458
+ if (!parentItem) throw new Error(`Parent item for ${itemId} not found`);
3459
+ return [...this.getItemPath(parentItem.id), parentItem.id];
3460
+ }
3461
+ getItemDisplayPath(itemId, locale = this.locales[0] ?? "en") {
3462
+ if (!this.surveyItems.has(itemId)) return;
3463
+ const path = this.getItemPath(itemId);
3464
+ const name = this.resolveItemEditorName(itemId, locale);
3465
+ const namePath = path.map((id) => this.resolveItemEditorName(id, locale));
3466
+ return {
3467
+ itemId,
3468
+ name,
3469
+ path,
3470
+ namePath,
3471
+ breadcrumb: [...namePath, name].join(" / "),
3472
+ parentBreadcrumb: namePath.join(" / ")
3473
+ };
3474
+ }
3475
+ getParentItem(itemId) {
3476
+ const item = this.surveyItems.get(itemId);
3477
+ if (!item) throw new Error(`Item ${itemId} not found`);
3478
+ if (item instanceof GroupItemCore && item.isRoot()) return;
3479
+ const parentItem = Array.from(this.surveyItems.values()).find((item) => item instanceof GroupItemCore && item.config.items?.includes(itemId));
3480
+ if (!parentItem) return;
3481
+ return parentItem;
3482
+ }
3483
+ getChildrenItems(parentId) {
3484
+ const parentItem = this.surveyItems.get(parentId);
3485
+ if (!parentItem || !(parentItem instanceof GroupItemCore)) return [];
3486
+ return parentItem.config.items?.map((id) => this.surveyItems.get(id))?.filter((item) => item !== void 0) || [];
3487
+ }
3488
+ getSiblings(itemId) {
3489
+ if (!this.surveyItems.get(itemId)) return [];
3490
+ return this.getParentItem(itemId)?.items?.map((id) => this.surveyItems.get(id))?.filter((item) => item !== void 0) || [];
3491
+ }
3492
+ get translations() {
3493
+ if (!this._translations) this._translations = new SurveyTranslations();
3494
+ return this._translations;
3495
+ }
3496
+ getItemTranslations(id) {
3497
+ if (!this.surveyItems.get(id)) throw new Error(`Item ${id} not found`);
3498
+ return this._translations?.getItemTranslations(id);
3499
+ }
3500
+ getTemplateValue(templateValueKey) {
3501
+ return this._templateValues?.get(templateValueKey);
3502
+ }
3503
+ setTemplateValue(templateValueKey, templateValue) {
3504
+ if (!this._templateValues) this._templateValues = /* @__PURE__ */ new Map();
3505
+ this._templateValues?.set(templateValueKey, templateValue);
3506
+ }
3507
+ deleteTemplateValue(templateValueKey) {
3508
+ this._templateValues?.delete(templateValueKey);
3509
+ }
3510
+ getTemplateValueKeys() {
3511
+ return Array.from(this._templateValues?.keys() || []);
3512
+ }
3513
+ getAsset(assetId) {
3514
+ const asset = this._assets?.get(assetId);
3515
+ return asset ? structuredCloneMethod(asset) : void 0;
3516
+ }
3517
+ setAsset(assetId, asset) {
3518
+ if (!this._assets) this._assets = /* @__PURE__ */ new Map();
3519
+ this._assets.set(assetId, structuredCloneMethod(asset));
3520
+ }
3521
+ deleteAsset(assetId) {
3522
+ this._assets?.delete(assetId);
3523
+ }
3524
+ hasAsset(assetId) {
3525
+ return this._assets?.has(assetId) ?? false;
3526
+ }
3527
+ getAssetIds() {
3528
+ return Array.from(this._assets?.keys() || []);
3529
+ }
3530
+ getAssets() {
3531
+ return new Map(Array.from(this._assets?.entries() || []).map(([assetId, asset]) => [assetId, structuredCloneMethod(asset)]));
3532
+ }
3533
+ getAssetUsages(filterAssetId) {
3534
+ const usages = [];
3535
+ const pushContentUsages = (content, usageScope, contentKey, locale, itemId) => {
3536
+ for (const usage of getAssetUsagesFromContent(content)) {
3537
+ if (filterAssetId && usage.assetId !== filterAssetId) continue;
3538
+ usages.push({
3539
+ assetId: usage.assetId,
3540
+ itemId,
3541
+ locale,
3542
+ contentKey,
3543
+ usageScope,
3544
+ blockIndex: usage.blockIndex
3545
+ });
3546
+ }
3547
+ };
3548
+ const serializedTranslations = this._translations?.serialize();
3549
+ const rootItemId = this.rootItem?.id;
3550
+ if (!serializedTranslations) return usages;
3551
+ for (const [locale, localeTranslations] of Object.entries(serializedTranslations)) {
3552
+ for (const [contentKey, content] of Object.entries(localeTranslations.surveyCardContent || {})) pushContentUsages(content, "surveyCard", contentKey, locale, rootItemId);
3553
+ for (const [contentKey, content] of Object.entries(localeTranslations.navigationContent || {})) pushContentUsages(content, "navigation", contentKey, locale, rootItemId);
3554
+ for (const [contentKey, content] of Object.entries(localeTranslations.validationMessages || {})) pushContentUsages(content, "validationMessages", contentKey, locale, rootItemId);
3555
+ for (const [itemId, itemTranslations] of Object.entries(localeTranslations.itemTranslations || {})) for (const [contentKey, content] of Object.entries(itemTranslations || {})) pushContentUsages(content, "itemTranslation", contentKey, locale, itemId);
3556
+ }
3557
+ return usages;
3558
+ }
3559
+ getAvailableResponseValueSlots(byType) {
3560
+ let valueRefs = {};
3561
+ for (const item of this.surveyItems.values()) valueRefs = {
3562
+ ...valueRefs,
3563
+ ...item.getAvailableResponseValueSlots(byType)
3564
+ };
3565
+ return valueRefs;
3566
+ }
3567
+ getResponseSlotDefinitions() {
3568
+ return Array.from(this.surveyItems.values()).map((item) => ({
3569
+ itemId: item.id,
3570
+ slots: item.getResponseSlotDefinitions().map((slot) => ({
3571
+ ...slot,
3572
+ exportSettings: item.rawItem.responseSettings?.[slot.slotId]
3573
+ }))
3574
+ }));
3575
+ }
3576
+ /** Compile a detached index from item declarations; diagnostics retain incomplete drafts. */
3577
+ getResponseSlotRegistry() {
3578
+ const declarations = this.getResponseSlotDefinitions().map(({ itemId, slots }) => ({
3579
+ itemId,
3580
+ slots: slots.map((slot) => ({
3581
+ ...slot,
3582
+ ...slot.referenceValueLabelKeys ? { referenceValueLabels: Object.fromEntries(this.locales.map((locale) => [locale, Object.fromEntries(Object.entries(slot.referenceValueLabelKeys).map(([id, key]) => [id, getContentPlainText(this.getItemTranslations(itemId)?.getContent(locale, key, this.locales[0]))]))])) } : {}
3583
+ }))
3584
+ }));
3585
+ const registry = new ResponseSlotRegistry(declarations);
3586
+ for (const { itemId, slots } of declarations) for (const slotId of Object.keys(this.surveyItems.get(itemId).rawItem.responseSettings ?? {})) if (!slots.some((slot) => slot.slotId === slotId)) registry.diagnostics.push({
3587
+ code: "unknown-slot",
3588
+ slotId,
3589
+ itemId,
3590
+ message: `Response settings reference an unknown variable "${slotId}" on item "${itemId}".`
3591
+ });
3592
+ return registry;
3593
+ }
3594
+ /** One typed declaration path for editor, runtime and export integrity checks. */
3595
+ getExpressionResponseSlots() {
3596
+ return this.getResponseSlotRegistry().definitions.flatMap((slot) => ["get", "isDefined"].map((method) => ({
3597
+ ref: JSON.stringify([slot.slotId, method]),
3598
+ itemId: slot.itemId,
3599
+ valueType: method === "get" ? slot.valueType : ValueType.boolean,
3600
+ ...method === "get" && slot.allowedValues ? { allowedValues: slot.allowedValues } : {}
3601
+ })));
3602
+ }
3603
+ getExpressionReferenceDiagnostics() {
3604
+ return analyzeSurveyExpressionReferences({
3605
+ survey: this.serialize(),
3606
+ responseSlots: this.getExpressionResponseSlots()
3607
+ });
3608
+ }
3609
+ getBlockingExpressionIssues() {
3610
+ return this.getExpressionReferenceDiagnostics().filter(isBlockingSurveyExpressionReferenceDiagnostic).map((diagnostic) => diagnostic.message);
3611
+ }
3612
+ /**
3613
+ * Get all reference usages for the survey
3614
+ * @param forItemId - optional item id to filter usages for a specific item and its children (if not provided, all usages are returned)
3615
+ * @returns all reference usages for the survey (or for a specific item and its children)
3616
+ */
3617
+ getReferenceUsages(forItemId) {
3618
+ const usages = [];
3619
+ for (const item of this.surveyItems.values()) {
3620
+ if (forItemId && item.id !== forItemId && !this.isDescendantOf(item.id, forItemId)) continue;
3621
+ usages.push(...item.getReferenceUsages());
3622
+ }
3623
+ if (this._templateValues) for (const [templateValueKey, templateValue] of this._templateValues.entries()) for (const ref of templateValue.expression?.responseVariableRefs || []) usages.push({
3624
+ itemId: templateValueKey,
3625
+ usageType: "templateValues",
3626
+ valueReference: ref
3627
+ });
3628
+ return usages;
3629
+ }
3630
+ isDescendantOf(targetId, ancestorId) {
3631
+ if (targetId === ancestorId) return true;
3632
+ return this.getItemPath(targetId).includes(ancestorId);
3633
+ }
3634
+ };
3635
+ //#endregion
3636
+ //#region src/response-exporter/column-id.ts
3637
+ function encodeExportColumnId(identity) {
3638
+ return JSON.stringify(identity);
3639
+ }
3640
+ function decodeExportColumnId(value) {
3641
+ const tuple = JSON.parse(value);
3642
+ 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}`);
3643
+ if (JSON.stringify(tuple) !== value) throw new Error("Export column IDs must use the canonical JSON tuple encoding");
3644
+ return tuple;
3645
+ }
3646
+ //#endregion
3647
+ //#region src/response-exporter/types.ts
3648
+ /** Meta column keys in fixed order (common attributes first). */
3649
+ const META_COLUMN_ORDER = [
3650
+ "submittedAt",
3651
+ "participantId",
3652
+ "responseId",
3653
+ "sourceId"
3654
+ ];
3655
+ /** Built-in transform modes for slot export. */
3656
+ const SlotTransformMode = {
3657
+ default: "default",
3658
+ mask: "mask",
3659
+ dateFormat: "dateFormat"
3660
+ };
3661
+ const EXPORTER_PROFILE_SCHEMA_VERSION = 1;
3662
+ /** Source of an export column. */
3663
+ const ExportColumnSource = {
3664
+ meta: "meta",
3665
+ context: "context",
3666
+ response: "response"
3667
+ };
3668
+ //#endregion
3669
+ //#region src/response-exporter/slot-formatter.ts
3670
+ /** Default transform config when no profile override. */
3671
+ const DEFAULT_TRANSFORM = { mode: SlotTransformMode.default };
3672
+ /**
3673
+ * Export a single response slot to a string. Pure function for unit testing.
3674
+ * Uses default formatting by ValueType unless overridden by transformConfig.
3675
+ */
3676
+ function exportSingleResponseSlot(params) {
3677
+ const { value, valueType, transformConfig } = params;
3678
+ if (value === void 0 || value === null) return "";
3679
+ const mode = transformConfig.mode;
3680
+ if (mode === SlotTransformMode.mask) return maskValue(value, transformConfig.maskChar ?? "***");
3681
+ if (mode === SlotTransformMode.dateFormat && (valueType === ValueType.date || valueType === ValueType.dateArray)) return formatDateValue(value, transformConfig.dateFormat?.pattern ?? "yyyy-MM-dd");
3682
+ if (value.type === ValueType.reference || value.type === ValueType.referenceArray) {
3683
+ const keys = transformConfig.categorical === "labels" ? transformConfig.optionLabels : transformConfig.optionCodes ?? params.referenceValueKeys;
3684
+ const exportReference = (id) => {
3685
+ if (transformConfig.categorical === "ids") return id;
3686
+ const key = keys && Object.hasOwn(keys, id) ? keys[id] : void 0;
3687
+ if (typeof key !== "string" || !key.trim()) throw new Error(`Missing categorical export mapping for option "${id}"`);
3688
+ return key.trim();
3689
+ };
3690
+ return value.type === ValueType.reference ? exportReference(value.value) : JSON.stringify(value.value.map(exportReference));
3691
+ }
3692
+ return formatDefault(value);
3693
+ }
3694
+ const BooleanExportValues = {
3695
+ TRUE: "TRUE",
3696
+ FALSE: "FALSE"
3697
+ };
3698
+ function formatDefault(value) {
3699
+ switch (value.type) {
3700
+ case ValueType.string: return String(value.value);
3701
+ case ValueType.reference: return String(value.value);
3702
+ case ValueType.boolean: return value.value ? BooleanExportValues.TRUE : BooleanExportValues.FALSE;
3703
+ case ValueType.number: return value.precision === NumberPrecision.int ? String(Math.floor(value.value)) : String(value.value);
3704
+ case ValueType.date: return value.value;
3705
+ case ValueType.duration: {
3706
+ const d = value;
3707
+ return `${d.precision === NumberPrecision.int ? Math.floor(d.value) : d.value} ${d.unit}`;
3708
+ }
3709
+ case ValueType.stringArray: return value.value.join("; ");
3710
+ case ValueType.referenceArray: return value.value.join("; ");
3711
+ case ValueType.numberArray: {
3712
+ const arr = value.value;
3713
+ const prec = value.precision;
3714
+ const fmt = (n) => prec === NumberPrecision.int ? Math.floor(n) : n;
3715
+ return arr.map(fmt).join("; ");
3716
+ }
3717
+ case ValueType.dateArray: return value.value.join("; ");
3718
+ case ValueType.durationArray: {
3719
+ const d = value;
3720
+ const prec = d.precision === NumberPrecision.int;
3721
+ const fmt = (n) => prec ? Math.floor(n) : n;
3722
+ return d.value.map((v) => `${fmt(v)} ${d.unit}`).join("; ");
3723
+ }
3724
+ default: return String(value.value ?? "");
3725
+ }
3726
+ }
3727
+ function formatDateValue(value, pattern) {
3728
+ if (value.type === ValueType.date) return formatCalendarDate(value.value, pattern);
3729
+ if (value.type === ValueType.dateArray) return value.value.map((date) => formatCalendarDate(date, pattern)).join("; ");
3730
+ return formatDefault(value);
3731
+ }
3732
+ function maskValue(value, maskChar) {
3733
+ return valueToString(value) ? maskChar : "";
3734
+ }
3735
+ function valueToString(value) {
3736
+ switch (value.type) {
3737
+ case ValueType.string:
3738
+ case ValueType.reference: return String(value.value);
3739
+ case ValueType.stringArray:
3740
+ case ValueType.referenceArray: return value.value.join(" ");
3741
+ default: return String(value.value ?? "");
3742
+ }
3743
+ }
3744
+ //#endregion
3745
+ //#region src/response-exporter/projection.ts
3746
+ function optionIds(slot) {
3747
+ return [...new Set((slot.allowedValues ?? []).flatMap((value) => value.type === ValueType.reference ? [value.value] : value.type === ValueType.referenceArray ? value.value : []))];
3748
+ }
3749
+ function optionMapping(slot, config) {
3750
+ const ids = optionIds(slot);
3751
+ const encoding = config.categorical ?? "codes";
3752
+ const mapping = encoding === "ids" ? Object.fromEntries(ids.map((id) => [id, id])) : encoding === "labels" ? config.optionLabels ?? slot.referenceValueLabels?.[config.locale ?? "en"] ?? {} : config.optionCodes ?? slot.referenceValueKeys ?? {};
3753
+ const used = /* @__PURE__ */ new Set();
3754
+ for (const id of ids) {
3755
+ if (typeof mapping[id] !== "string" || !mapping[id].trim()) throw new Error(`Variable "${slot.variableName}" has no ${encoding} mapping for option "${id}"`);
3756
+ if (used.has(mapping[id])) throw new Error(`Variable "${slot.variableName}" has ambiguous ${encoding} mapping "${mapping[id]}"`);
3757
+ used.add(mapping[id]);
3758
+ }
3759
+ 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}"`);
3760
+ return mapping;
3761
+ }
3762
+ /** Compile every declared role before applying inclusion settings. */
3763
+ function projectSlot(slot, config, allRoles = false) {
3764
+ if (![
3765
+ "default",
3766
+ "mask",
3767
+ "dateFormat"
3768
+ ].includes(config.mode)) throw new Error(`Unsupported export transform "${config.mode}"`);
3769
+ if (config.categorical && ![
3770
+ "codes",
3771
+ "labels",
3772
+ "ids"
3773
+ ].includes(config.categorical)) throw new Error("Unsupported categorical encoding");
3774
+ if (config.multiple && ![
3775
+ "json",
3776
+ "delimited",
3777
+ "indicators"
3778
+ ].includes(config.multiple)) throw new Error("Unsupported multiple-selection encoding");
3779
+ if (config.mode === "dateFormat" && slot.valueType !== "date" && slot.valueType !== "date[]") throw new Error("Date formatting requires a date variable");
3780
+ if (config.mode === "dateFormat") {
3781
+ const pattern = config.dateFormat?.pattern ?? "yyyy-MM-dd";
3782
+ if (!pattern.trim()) throw new Error("Date export pattern cannot be empty");
3783
+ formatCalendarDate("2026-01-15", pattern);
3784
+ }
3785
+ 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");
3786
+ 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");
3787
+ const categorical = slot.valueType === ValueType.reference || slot.valueType === ValueType.referenceArray;
3788
+ if (config.multiple && slot.valueType !== ValueType.referenceArray) throw new Error(`Multiple-selection encoding requires a reference[] variable: ${slot.variableName}`);
3789
+ if ((config.categorical || config.optionCodes || config.optionLabels) && !categorical) throw new Error(`Categorical encoding requires a categorical variable: ${slot.variableName}`);
3790
+ if (config.booleanValues && slot.valueType !== ValueType.boolean) throw new Error(`Boolean encoding requires a boolean variable: ${slot.variableName}`);
3791
+ if (config.precision !== void 0 && ![
3792
+ "number",
3793
+ "number[]",
3794
+ "duration",
3795
+ "duration[]"
3796
+ ].includes(slot.valueType)) throw new Error("Precision requires a numeric or duration variable");
3797
+ 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");
3798
+ 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");
3799
+ if (!allRoles && (config.include === false || slot.computed && config.include !== true)) return [];
3800
+ const mapping = categorical && !allRoles ? optionMapping(slot, config) : {};
3801
+ const base = {
3802
+ source: "response",
3803
+ itemId: slot.itemId,
3804
+ slotId: slot.slotId,
3805
+ valueType: slot.valueType
3806
+ };
3807
+ const scalar = {
3808
+ ...base,
3809
+ id: encodeExportColumnId([
3810
+ "response",
3811
+ slot.slotId,
3812
+ "value"
3813
+ ]),
3814
+ header: slot.variableName
3815
+ };
3816
+ const indicators = () => optionIds(slot).map((optionId) => ({
3817
+ ...base,
3818
+ id: encodeExportColumnId([
3819
+ "response",
3820
+ slot.slotId,
3821
+ "option",
3822
+ optionId
3823
+ ]),
3824
+ header: `${slot.variableName}_${mapping[optionId] ?? optionId}`,
3825
+ optionId
3826
+ }));
3827
+ return allRoles ? [scalar, ...slot.valueType === ValueType.referenceArray ? indicators() : []] : config.multiple === "indicators" ? indicators() : [scalar];
3828
+ }
3829
+ function formatProjectedValue(slot, column, value, config) {
3830
+ if (value === void 0) return "";
3831
+ if (config.mode === "mask") return config.maskChar ?? "***";
3832
+ if (value.type === ValueType.reference || value.type === ValueType.referenceArray) {
3833
+ const mapping = optionMapping(slot, config);
3834
+ const values = value.type === ValueType.reference ? [value.value] : value.value;
3835
+ if (values.some((id) => !Object.hasOwn(mapping, id))) throw new Error(`Unmapped option in "${slot.variableName}"`);
3836
+ if (column.optionId) return values.includes(column.optionId) ? "1" : "0";
3837
+ const encoded = values.map((id) => mapping[id]);
3838
+ if (value.type === ValueType.reference) return encoded[0];
3839
+ if (config.multiple === "delimited") return encoded.map((entry) => `"${entry.replaceAll("\"", "\"\"")}"`).join(config.delimiter);
3840
+ return JSON.stringify(encoded);
3841
+ }
3842
+ if (value.type === ValueType.boolean) {
3843
+ const values = config.booleanValues ?? {
3844
+ true: "TRUE",
3845
+ false: "FALSE"
3846
+ };
3847
+ if (!values.true || !values.false || values.true === values.false) throw new Error("Boolean export values must be distinct and nonempty");
3848
+ return value.value ? values.true : values.false;
3849
+ }
3850
+ if (value.type === ValueType.duration || value.type === ValueType.durationArray) {
3851
+ const unit = config.durationUnit ?? "seconds";
3852
+ const seconds = {
3853
+ seconds: 1,
3854
+ minutes: 60,
3855
+ hours: 3600,
3856
+ days: 86400,
3857
+ weeks: 604800
3858
+ };
3859
+ if (unit !== value.unit && (!seconds[unit] || !seconds[value.unit])) throw new Error("Calendar months and years cannot be converted to fixed durations");
3860
+ const convert = (number) => {
3861
+ const n = number * (unit === value.unit ? 1 : seconds[value.unit] / seconds[unit]);
3862
+ return config.precision === void 0 ? String(n) : n.toFixed(config.precision);
3863
+ };
3864
+ return Array.isArray(value.value) ? JSON.stringify(value.value.map(convert)) : convert(value.value);
3865
+ }
3866
+ if (value.type === ValueType.number && config.precision !== void 0) return value.value.toFixed(config.precision);
3867
+ if (value.type === ValueType.numberArray && config.precision !== void 0) return `[${value.value.map((number) => number.toFixed(config.precision)).join(",")}]`;
3868
+ if (Array.isArray(value.value) && config.mode !== "dateFormat") return JSON.stringify(value.value);
3869
+ return exportSingleResponseSlot({
3870
+ value,
3871
+ valueType: slot.valueType,
3872
+ transformConfig: config
3873
+ });
3874
+ }
3875
+ /** Collect draft diagnostics without stopping at the first unfinished variable. */
3876
+ function getDefaultProjectionIssues(survey) {
3877
+ const issues = [];
3878
+ const headers = /* @__PURE__ */ new Set([
3879
+ "submittedAt",
3880
+ "participantId",
3881
+ "responseId",
3882
+ "sourceId"
3883
+ ]);
3884
+ for (const slot of survey.getResponseSlotRegistry().definitions) try {
3885
+ for (const column of projectSlot(slot, {
3886
+ mode: "default",
3887
+ ...slot.exportSettings
3888
+ })) {
3889
+ if (headers.has(column.header)) issues.push(`Duplicate export column header '${column.header}'`);
3890
+ headers.add(column.header);
3891
+ }
3892
+ } catch (error) {
3893
+ issues.push(error instanceof Error ? error.message : String(error));
3894
+ }
3895
+ return issues;
3896
+ }
3897
+ /** Fully resolved output semantics, shared by compatibility checks and codebooks. */
3898
+ function resolvedSlotEncoding(slot, config) {
3899
+ const encoding = {
3900
+ mode: config.mode,
3901
+ include: config.include ?? !slot.computed
3902
+ };
3903
+ if (config.mode === "mask") return {
3904
+ ...encoding,
3905
+ maskChar: config.maskChar ?? "***"
3906
+ };
3907
+ if (config.mode === "dateFormat") encoding.dateFormat = { pattern: config.dateFormat?.pattern ?? "yyyy-MM-dd" };
3908
+ if (slot.valueType === "reference" || slot.valueType === "reference[]") {
3909
+ encoding.categorical = config.categorical ?? "codes";
3910
+ if (encoding.categorical === "labels") {
3911
+ encoding.locale = config.locale ?? "en";
3912
+ encoding.optionLabels = optionMapping(slot, config);
3913
+ } else if (encoding.categorical === "codes") encoding.optionCodes = optionMapping(slot, config);
3914
+ if (slot.valueType === "reference[]") {
3915
+ encoding.multiple = config.multiple ?? "json";
3916
+ if (encoding.multiple === "delimited") encoding.delimiter = config.delimiter;
3917
+ }
3918
+ }
3919
+ if (slot.valueType === "boolean") encoding.booleanValues = config.booleanValues ?? {
3920
+ true: "TRUE",
3921
+ false: "FALSE"
3922
+ };
3923
+ if (slot.valueType.startsWith("duration")) encoding.durationUnit = config.durationUnit ?? "seconds";
3924
+ if (config.precision !== void 0) encoding.precision = config.precision;
3925
+ return structuredClone(encoding);
3926
+ }
3927
+ //#endregion
3928
+ export { const_date as $, responseValuesEqual as $n, CtxPFlagDateEditor as $t, createItemTypeDefinitionRegistry as A, deserializeTemplateValue as An, min as At, deserializeSurveyItemPrefill as B, FunctionExpression as Bn, str_list_contains as Bt, SurveyItemTranslations as C, findIntroducedBlockingExpressionReferenceDiagnostics as Cn, eq as Ct, CURRENT_SURVEY_SCHEMA_VERSION as D, responseValueReferenceIds as Dn, lt as Dt, CURRENT_SURVEY_SCHEMA as E, replaceResponseVariableReferences as En, in_range as Et, isBuiltInItemType as F, ConstExpression as Fn, response_number as Ft, generateId as G, ValueReferenceMethod as Gn, ConstDateEditor as Gt, serializeSurveyItemPrefill as H, ResponseVariableExpression as Hn, AndExpressionEditor as Ht, getCompositionLabel as I, ContextVariableExpression as In, response_number_array as It, shuffleIndices as J, allocateResponseVariableName as Jn, ConstStringArrayEditor as Jt, resolveUniqueItemKey as K, ResponseSlotRegistry as Kn, ConstNumberArrayEditor as Kt, SurveyItemCore as L, ContextVariableType as Ln, response_string as Lt, GroupItemCore as M, serializeTemplateValue as Mn, response_boolean as Mt, PageBreakItemCore as N, serializeTemplateValues as Nn, response_date as Nt, createFullRegistry as O, DATE_FUNCTION_SIGNATURES as On, lte as Ot, builtInItemCoreRegistry as P, ExpressionEvaluator as Pn, response_date_array as Pt, const_boolean as Q, responseValueElements as Qn, CtxLocaleEditor as Qt, SurveyItemPrefillApplyMode as R, Expression as Rn, response_string_array as Rt, isContentEmpty as S, expressionReferenceDiagnosticKey as Sn, date_min as St, validateLocale as T, isBlockingSurveyExpressionReferenceDiagnostic as Tn, gte as Tt, createSeededRandom as U, ReferenceUsageType as Un, ConstBooleanEditor as Ut, prefillTargetsEqual as V, FunctionExpressionNames as Vn, sum as Vt, generateCodingKey as W, ValueReference as Wn, ConstDateArrayEditor as Wt, ReservedSurveyItemTypes as X, suggestResponseVariableName as Xn, CtxCustomExpressionEditor as Xt, structuredCloneMethod as Y, isValidResponseVariableName as Yn, ConstStringEditor as Yt, and as Z, invalidResponseDomainValues as Zn, CtxCustomValueEditor as Zt, getAssetUsagesFromContent as _, SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY as _n, differenceCalendarDates as _r, date_gt as _t, resolvedSlotEncoding as a, GtExpressionEditor as an, SurveyItemResponse as ar, ctx_custom_expression as at, hasRenderableRichTextBlock as b, collectExpressionResponseReferences as bn, date_lte as bt, EXPORTER_PROFILE_SCHEMA_VERSION as c, LtExpressionEditor as cn, ValueType as cr, ctx_pflag_date as ct, SlotTransformMode as d, MinExpressionEditor as dn, addCalendarDate as dr, ctx_pflag_string as dt, CtxPFlagIsDefinedEditor as en, validateResponseSlotValue as er, const_date_array as et, decodeExportColumnId as f, OrExpressionEditor as fn, calendarDateBounds as fr, date_add_days as ft, createRichTextContent as g, SumExpressionEditor as gn, compareCalendarDates as gr, date_eq as gt, ContentType as h, StrListContainsExpressionEditor as hn, calendarDateToLocalDate as hr, date_diff as ht, projectSlot as i, ExpressionEditor as in, ResponseItem as ir, const_string_array as it, toItemTypeDefinitionRegistry as j, deserializeTemplateValues as jn, or as jt, createItemCore as k, TemplateDefTypes as kn, max as kt, ExportColumnSource as l, LteExpressionEditor as ln, assertResponseValue as lr, ctx_pflag_is_defined as lt, Survey as m, StrEqExpressionEditor as mn, calendarDatePrecision as mr, date_add_years as mt, getDefaultProjectionIssues as n, CtxPFlagStringEditor as nn, SURVEY_RESPONSE_SCHEMA_VERSION as nr, const_number_array as nt, DEFAULT_TRANSFORM as o, GteExpressionEditor as on, DurationUnits as or, ctx_custom_value as ot, encodeExportColumnId as p, ResponseVariableEditor as pn, calendarDateFromLocalDate as pr, date_add_months as pt, shuffleArray as q, RESPONSE_VARIABLE_NAME_MAX_LENGTH as qn, ConstNumberEditor as qt, optionMapping as r, EqExpressionEditor as rn, SurveyResponse as rr, const_string as rt, exportSingleResponseSlot as s, InRangeExpressionEditor as sn, NumberPrecision as sr, ctx_locale as st, formatProjectedValue as t, CtxPFlagNumEditor as tn, initValueForType as tr, const_number as tt, META_COLUMN_ORDER as u, MaxExpressionEditor as un, isResponseValue as ur, ctx_pflag_num as ut, getContentPlainText as v, analyzeSurveyExpressionReferences as vn, formatCalendarDate as vr, date_gte as vt, SurveyTranslations as w, findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories as wn, gt as wt, hasRenderableRichTextContent as x, collectSurveyExpressionLocations as xn, date_max as xt, getPlainTextFromRichTextContent as y, collectConfigValueExpressions as yn, isCalendarDate as yr, date_lt as yt, SurveyItemPrefillTargetType as z, ExpressionType as zn, str_eq as zt };
3929
+
3930
+ //# sourceMappingURL=projection-Bl6qCinx.mjs.map