@reform-society/agera-core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as _$nanostores from "nanostores";
2
+ import { z } from "zod";
2
3
 
3
4
  //#region src/types.d.ts
4
5
  type PredicateOp = "eq" | "neq" | "in" | "nin" | "gt" | "gte" | "lt" | "lte" | "exists" | "truthy" | "falsy" | "contains" | "starts_with" | "ends_with";
@@ -218,6 +219,148 @@ type FetchBasinJWTOptions = {
218
219
  };
219
220
  declare function fetchBasinJWT(endpoint: string, storage: StorageAccessor, storageKey: string, options?: FetchBasinJWTOptions): Promise<string>;
220
221
  //#endregion
222
+ //#region src/survey/types.d.ts
223
+ type NavAction = {
224
+ kind: "next";
225
+ } | {
226
+ kind: "goto";
227
+ target: string;
228
+ } | {
229
+ kind: "redirect";
230
+ target: string;
231
+ };
232
+ type SurveyFieldRule = "required" | "email" | "name" | "phone" | "postal" | {
233
+ rule: "pattern" | "min" | "max";
234
+ arg: string;
235
+ };
236
+ type SurveyStepRule = {
237
+ rule: "required";
238
+ } | {
239
+ rule: "minSelected" | "maxSelected";
240
+ arg: number;
241
+ };
242
+ type SurveyOption = {
243
+ value: string;
244
+ label?: string;
245
+ nav?: NavAction; /** Quiz: this option is a correct answer (feeds `expectedValues`). */
246
+ correct?: boolean; /** Quiz: weighted scoring contributions when selected (feeds `weightedScores`). */
247
+ weights?: Record<string, number>; /** Free-text "other" option; the typed text is stored under `<name>_other`. */
248
+ other?: boolean;
249
+ };
250
+ type SurveyField = {
251
+ name: string;
252
+ type: "text" | "email" | "tel" | "textarea" | "postal" | "address";
253
+ rules?: SurveyFieldRule[];
254
+ };
255
+ type SurveyStepQuiz = {
256
+ model: QuizQuestionModel;
257
+ section?: string;
258
+ scoring?: "exact" | "partial";
259
+ };
260
+ type SurveyStep = {
261
+ id: string;
262
+ kind: "content" | "single" | "multi" | "scale" | "fields"; /** Answer key; required for single / multi / scale. */
263
+ name?: string; /** Step is skipped on the path while this evaluates to false. */
264
+ showIf?: Predicate; /** Default true. */
265
+ back?: boolean; /** Advancing from this step submits the survey instead of moving on. */
266
+ terminal?: boolean; /** Step default navigation; a chosen option's `nav` overrides it. */
267
+ nav?: NavAction;
268
+ options?: SurveyOption[];
269
+ scale?: {
270
+ min: number;
271
+ max: number;
272
+ step?: number;
273
+ };
274
+ fields?: SurveyField[];
275
+ validate?: SurveyStepRule[];
276
+ quiz?: SurveyStepQuiz;
277
+ };
278
+ type SurveyDefinition = {
279
+ version: 1; /** Session storage key: `agera:survey:<id>`. */
280
+ id: string; /** Document order = linear order. */
281
+ steps: SurveyStep[]; /** Default "static" (matches the #145 fixture). */
282
+ progress?: "static" | "path";
283
+ quiz?: QuizConfig;
284
+ lang?: string;
285
+ };
286
+ type SurveyStatus = "idle" | "submitting" | "done" | "failed";
287
+ type SurveyState = {
288
+ version: 1;
289
+ definitionId: string;
290
+ current: string;
291
+ path: string[]; /** single → string, multi → string[], scale → number, fields → flattened by field name. */
292
+ answers: Record<string, unknown>;
293
+ status: SurveyStatus;
294
+ sessionId: string;
295
+ };
296
+ type SurveyErrors = Record<string, string>;
297
+ type SurveyView = {
298
+ step: SurveyStep;
299
+ index: number;
300
+ canBack: boolean;
301
+ isTerminal: boolean;
302
+ progress: ProgressModeInfo;
303
+ errors: SurveyErrors;
304
+ quiz: QuizResults | null;
305
+ };
306
+ /** What gets submitted: answers limited to steps on `path`, multi values joined with ", ". */
307
+ type SurveySnapshot = {
308
+ definitionId: string;
309
+ sessionId: string;
310
+ current: string;
311
+ path: string[];
312
+ answers: Record<string, string>;
313
+ };
314
+ type NavResult = {
315
+ kind: "moved";
316
+ to: string;
317
+ } | {
318
+ kind: "submit";
319
+ redirect?: string;
320
+ } | {
321
+ kind: "blocked";
322
+ errors: SurveyErrors;
323
+ } | {
324
+ kind: "stale";
325
+ } | {
326
+ kind: "noop";
327
+ };
328
+ type SurveyValidator = (input: {
329
+ step: SurveyStep;
330
+ answers: Record<string, unknown>;
331
+ lang?: string;
332
+ }) => SurveyErrors | null | Promise<SurveyErrors | null>;
333
+ type SurveyEngineOptions = {
334
+ initialState?: SurveyState; /** Used when `initialState` is absent. */
335
+ sessionId?: string; /** Extra validation on top of the built-in step rules; async results are discarded when stale. */
336
+ validate?: SurveyValidator;
337
+ };
338
+ type SurveyEngine = {
339
+ getState(): SurveyState;
340
+ getView(): SurveyView;
341
+ subscribe(listener: () => void): () => void; /** Set an answer without navigating. */
342
+ answer(name: string, value: unknown): void; /** Atomic answer + navigation against one snapshot. */
343
+ choose(name: string, value: unknown, nav?: NavAction): Promise<NavResult>; /** Validate the current step, then follow `nav` (or the step / option / linear default). */
344
+ next(nav?: NavAction): Promise<NavResult>;
345
+ back(): NavResult;
346
+ goto(id: string): NavResult;
347
+ snapshot(): SurveySnapshot; /** Submission lifecycle is driven by the consumer; navigation is blocked while "submitting". */
348
+ setStatus(status: SurveyStatus): void;
349
+ };
350
+ //#endregion
351
+ //#region src/basin-submission.d.ts
352
+ type BasinFormDataInput = {
353
+ snapshot: SurveySnapshot;
354
+ step: number;
355
+ final: boolean;
356
+ jwt: string;
357
+ utms: Record<string, string>;
358
+ anonymousEmail: string;
359
+ stripFields?: Iterable<string>;
360
+ };
361
+ declare function hashSurveySnapshot(snapshot: SurveySnapshot): string;
362
+ declare function buildBasinFormData(input: BasinFormDataInput): FormData;
363
+ //#endregion
221
364
  //#region src/lifecycle.d.ts
222
365
  type NormalizedResult = {
223
366
  delivery: "observed" | "unobserved";
@@ -618,5 +761,52 @@ declare function assembleCrmRequest({
618
761
  //#region src/polling.d.ts
619
762
  declare function matchesFormat(example: unknown, value: unknown): boolean;
620
763
  //#endregion
621
- export { type AssembleCrmRequestParams, type AttemptStorage, type BandQuizResults, CRM_CONTACT_KEYS, type CompositePredicate, type CounterDisplayConfig, type CounterPayload, type CounterResponse, type CounterUpdateConfig, type CrmCommand, type CrmConfig, type CrmContactKey, type CrmDispatch, type CrmFetchOptions, type CrmFetchResult, type CrmFrontendKey, type CrmGateway, type CrmGatewayResult, type CrmGroupedAnswer, type CrmJsonGroups, type CrmOutcome, type CrmV2PatchRequest, type CrmV2Payload, type CrmV2Request, type DeviceInput, type DeviceType, type ErrorMessages, type EventSource, type EventSourceKind, type FetchBasinJWTOptions, type FlowAttempt, type InsertDirective, type JsonValue, type JwtPayload, type LifecycleEvent, type LifecycleType, type MemoryCache, type MemoryCacheOptions, type MemoryCacheSetOptions, type NormalizedResult, type PassQuizResults, type PlacesMessageKey, type PlacesMessages, type Predicate, type PredicateOp, type ProgressInfo, type ProgressModeInfo, type QuestionMetrics, type QuizBandDefinition, type QuizConfig, type QuizOutcomeModel, type QuizPassFailConfig, type QuizQuestionInput, type QuizQuestionModel, type QuizQuestionResult, type QuizQuestionState, type QuizResults, type QuizScoringModel, type QuizSectionConfig, type QuizSectionResults, type RequestSnapshot, type ResolveValueSources, type ScoreQuizResults, type SessionState, type ShareConfig, type SimplePredicate, type SplitConfig, type StepState, type StorageAccessor, type SurveyOptions, type TrackingConfig, type Utms, VISITOR_ID_STORAGE_KEY, type ValidationRule, type Validators, type ValueResolver, type WeightedQuizResults, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildContext, buildCounterPayload, buildFcrmUtms, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, createAttemptStore, createLifecycleBus, createMemoryCache, createSessionStore, createStepsStore, createTrackingConsumer, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, firstDefined, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getOrCreateVisitorId, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, mapTrackingEvent, matchesFormat, mergeAnswers, normalizeResult, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, snapshot, unknownResult, updateUrlWithParams, validators };
764
+ //#region src/survey/engine.d.ts
765
+ declare function createSurveyEngine(definition: SurveyDefinition, options?: SurveyEngineOptions): SurveyEngine;
766
+ //#endregion
767
+ //#region src/survey/progress.d.ts
768
+ declare function computeProgress(definition: SurveyDefinition, state: SurveyState): ProgressModeInfo;
769
+ //#endregion
770
+ //#region src/survey/snapshot.d.ts
771
+ declare function buildSurveySnapshot(definition: SurveyDefinition, state: SurveyState): SurveySnapshot;
772
+ //#endregion
773
+ //#region src/survey/quiz.d.ts
774
+ declare function deriveQuizInputs(definition: SurveyDefinition, state: SurveyState): QuizQuestionInput[];
775
+ //#endregion
776
+ //#region src/survey/schema.d.ts
777
+ declare const surveyDefinitionSchema: z.ZodObject<{
778
+ version: z.ZodLiteral<1>;
779
+ id: z.ZodString;
780
+ steps: z.ZodArray<z.ZodType<SurveyStep, unknown, z.core.$ZodTypeInternals<SurveyStep, unknown>>>;
781
+ progress: z.ZodOptional<z.ZodEnum<{
782
+ path: "path";
783
+ static: "static";
784
+ }>>;
785
+ quiz: z.ZodOptional<z.ZodObject<{
786
+ passFail: z.ZodOptional<z.ZodNullable<z.ZodObject<{
787
+ percentGte: z.ZodNumber;
788
+ }, z.core.$strip>>>;
789
+ bands: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
790
+ id: z.ZodString;
791
+ label: z.ZodString;
792
+ minPercent: z.ZodOptional<z.ZodNumber>;
793
+ maxPercent: z.ZodOptional<z.ZodNumber>;
794
+ }, z.core.$strip>>>>;
795
+ sections: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodObject<{
796
+ passFail: z.ZodOptional<z.ZodNullable<z.ZodObject<{
797
+ percentGte: z.ZodNumber;
798
+ }, z.core.$strip>>>;
799
+ bands: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
800
+ id: z.ZodString;
801
+ label: z.ZodString;
802
+ minPercent: z.ZodOptional<z.ZodNumber>;
803
+ maxPercent: z.ZodOptional<z.ZodNumber>;
804
+ }, z.core.$strip>>>>;
805
+ }, z.core.$strip>>>>;
806
+ }, z.core.$strip>>;
807
+ lang: z.ZodOptional<z.ZodString>;
808
+ }, z.core.$strip>;
809
+ declare function parseSurveyDefinition(input: unknown): SurveyDefinition;
810
+ //#endregion
811
+ export { type AssembleCrmRequestParams, type AttemptStorage, type BandQuizResults, CRM_CONTACT_KEYS, type CompositePredicate, type CounterDisplayConfig, type CounterPayload, type CounterResponse, type CounterUpdateConfig, type CrmCommand, type CrmConfig, type CrmContactKey, type CrmDispatch, type CrmFetchOptions, type CrmFetchResult, type CrmFrontendKey, type CrmGateway, type CrmGatewayResult, type CrmGroupedAnswer, type CrmJsonGroups, type CrmOutcome, type CrmV2PatchRequest, type CrmV2Payload, type CrmV2Request, type DeviceInput, type DeviceType, type ErrorMessages, type EventSource, type EventSourceKind, type FetchBasinJWTOptions, type FlowAttempt, type InsertDirective, type JsonValue, type JwtPayload, type LifecycleEvent, type LifecycleType, type MemoryCache, type MemoryCacheOptions, type MemoryCacheSetOptions, NavAction, NavResult, type NormalizedResult, type PassQuizResults, type PlacesMessageKey, type PlacesMessages, type Predicate, type PredicateOp, type ProgressInfo, type ProgressModeInfo, type QuestionMetrics, type QuizBandDefinition, type QuizConfig, type QuizOutcomeModel, type QuizPassFailConfig, type QuizQuestionInput, type QuizQuestionModel, type QuizQuestionResult, type QuizQuestionState, type QuizResults, type QuizScoringModel, type QuizSectionConfig, type QuizSectionResults, type RequestSnapshot, type ResolveValueSources, type ScoreQuizResults, type SessionState, type ShareConfig, type SimplePredicate, type SplitConfig, type StepState, type StorageAccessor, SurveyDefinition, SurveyEngine, SurveyEngineOptions, SurveyErrors, SurveyField, SurveyFieldRule, SurveyOption, type SurveyOptions, SurveySnapshot, SurveyState, SurveyStatus, SurveyStep, SurveyStepQuiz, SurveyStepRule, SurveyValidator, SurveyView, type TrackingConfig, type Utms, VISITOR_ID_STORAGE_KEY, type ValidationRule, type Validators, type ValueResolver, type WeightedQuizResults, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildBasinFormData, buildContext, buildCounterPayload, buildFcrmUtms, buildSurveySnapshot, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, computeProgress, createAttemptStore, createLifecycleBus, createMemoryCache, createSessionStore, createStepsStore, createSurveyEngine, createTrackingConsumer, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, deriveQuizInputs, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, firstDefined, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getOrCreateVisitorId, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, hashSurveySnapshot, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, mapTrackingEvent, matchesFormat, mergeAnswers, normalizeResult, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, parseSurveyDefinition, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, snapshot, surveyDefinitionSchema, unknownResult, updateUrlWithParams, validators };
622
812
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { atom } from "nanostores";
2
2
  import log from "loglevel";
3
+ import { z } from "zod";
3
4
  //#region src/stores.ts
4
5
  function createSessionStore(initial) {
5
6
  return atom({
@@ -754,6 +755,54 @@ async function fetchBasinJWT(endpoint, storage, storageKey, options = {}) {
754
755
  }
755
756
  }
756
757
  //#endregion
758
+ //#region src/basin-submission.ts
759
+ function normalizePiiKey(key) {
760
+ const lower = key.toLowerCase();
761
+ return lower.endsWith("-display") ? lower.slice(0, -8) : lower;
762
+ }
763
+ function stableSerialize(value) {
764
+ if (value === null) return "null";
765
+ if (typeof value === "string") return JSON.stringify(value);
766
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
767
+ if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`;
768
+ if (typeof value === "object") {
769
+ const record = value;
770
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`).join(",")}}`;
771
+ }
772
+ return JSON.stringify(String(value));
773
+ }
774
+ function hashSurveySnapshot(snapshot) {
775
+ const stable = stableSerialize({
776
+ current: snapshot.current,
777
+ path: snapshot.path,
778
+ answers: snapshot.answers
779
+ });
780
+ let hash = 2166136261;
781
+ for (let index = 0; index < stable.length; index += 1) {
782
+ hash ^= stable.charCodeAt(index);
783
+ hash = Math.imul(hash, 16777619);
784
+ }
785
+ return (hash >>> 0).toString(16).padStart(8, "0");
786
+ }
787
+ function buildBasinFormData(input) {
788
+ const formData = new FormData();
789
+ const stripFields = input.stripFields ? new Set(Array.from(input.stripFields, normalizePiiKey)) : null;
790
+ for (const [name, value] of Object.entries(input.snapshot.answers)) {
791
+ if (stripFields?.has(normalizePiiKey(name))) continue;
792
+ formData.append(name, value);
793
+ }
794
+ formData.append("frombasinjs", "true");
795
+ formData.append("_gotcha", "");
796
+ const email = formData.get("email");
797
+ if (typeof email !== "string" || email.trim() === "") formData.set("email", input.anonymousEmail);
798
+ for (const [name, value] of Object.entries(input.utms)) formData.append(name, value);
799
+ if (input.jwt.trim() !== "") formData.append("basinjwt", input.jwt);
800
+ formData.append("basinjsmultistep", "true");
801
+ formData.append("basinjsdraft", input.final ? "false" : "true");
802
+ if (!input.final) formData.append("basinjsstep", String(input.step));
803
+ return formData;
804
+ }
805
+ //#endregion
757
806
  //#region src/lifecycle.ts
758
807
  function unknownResult(delivery = "unobserved") {
759
808
  return {
@@ -1566,6 +1615,719 @@ function matchesFormat(example, value) {
1566
1615
  return example === value;
1567
1616
  }
1568
1617
  //#endregion
1569
- export { CRM_CONTACT_KEYS, VISITOR_ID_STORAGE_KEY, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildContext, buildCounterPayload, buildFcrmUtms, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, createAttemptStore, createLifecycleBus, createMemoryCache, createSessionStore, createStepsStore, createTrackingConsumer, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, firstDefined, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getOrCreateVisitorId, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, mapTrackingEvent, matchesFormat, mergeAnswers, normalizeResult, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, snapshot, unknownResult, updateUrlWithParams, validators };
1618
+ //#region src/survey/progress.ts
1619
+ function stepPassesShowIf(step, answers) {
1620
+ return step.showIf === void 0 || evaluatePredicate(step.showIf, answers);
1621
+ }
1622
+ function percent(current, total) {
1623
+ if (total === 0) return 0;
1624
+ return Math.round(current / total * 100);
1625
+ }
1626
+ function computeProgress(definition, state) {
1627
+ if ((definition.progress ?? "static") === "path") {
1628
+ const currentIndex = definition.steps.findIndex((step) => step.id === state.current);
1629
+ const stepsAfterCurrent = currentIndex < 0 ? [] : definition.steps.slice(currentIndex + 1).filter((step) => stepPassesShowIf(step, state.answers));
1630
+ const current = state.path.length;
1631
+ const total = current + stepsAfterCurrent.length;
1632
+ return {
1633
+ current,
1634
+ total,
1635
+ percent: percent(current, total)
1636
+ };
1637
+ }
1638
+ const documentIndex = definition.steps.findIndex((step) => step.id === state.current);
1639
+ const current = documentIndex >= 0 ? documentIndex + 1 : 1;
1640
+ const total = definition.steps.length;
1641
+ return {
1642
+ current,
1643
+ total,
1644
+ percent: percent(current, total)
1645
+ };
1646
+ }
1647
+ //#endregion
1648
+ //#region src/survey/quiz.ts
1649
+ function answeredValuesForStep(step, state) {
1650
+ if (!step.name) return [];
1651
+ const answer = state.answers[step.name];
1652
+ if (step.quiz?.model === "single_select") return typeof answer === "string" ? [answer] : [];
1653
+ return Array.isArray(answer) ? answer.filter((value) => typeof value === "string") : [];
1654
+ }
1655
+ function weightedScoresForValues(values, step) {
1656
+ const scores = {};
1657
+ for (const value of values) {
1658
+ const option = step.options?.find((candidate) => candidate.value === value);
1659
+ if (!option?.weights) continue;
1660
+ for (const [category, weight] of Object.entries(option.weights)) scores[category] = (scores[category] ?? 0) + weight;
1661
+ }
1662
+ return scores;
1663
+ }
1664
+ function labelsForValues(values, step) {
1665
+ return values.map((value) => step.options?.find((option) => option.value === value)?.label ?? value);
1666
+ }
1667
+ function deriveQuizInputs(definition, state) {
1668
+ const pathIds = new Set(state.path);
1669
+ return definition.steps.flatMap((step) => {
1670
+ if (!step.quiz) return [];
1671
+ const answeredValues = answeredValuesForStep(step, state);
1672
+ const correctOptions = (step.options ?? []).filter((option) => option.correct);
1673
+ const expectedValues = correctOptions.length > 0 ? correctOptions.map((option) => option.value) : null;
1674
+ const expectedLabels = correctOptions.length > 0 ? correctOptions.map((option) => option.label ?? option.value) : null;
1675
+ return [{
1676
+ key: step.name ?? step.id,
1677
+ model: step.quiz.model,
1678
+ section: step.quiz.section ?? null,
1679
+ eligible: pathIds.has(step.id),
1680
+ answeredValues,
1681
+ answeredLabels: labelsForValues(answeredValues, step),
1682
+ expectedValues,
1683
+ expectedLabels,
1684
+ scoringModel: step.quiz.scoring ?? (expectedValues ? "exact" : null),
1685
+ usesWeightedScoring: (step.options ?? []).some((option) => option.weights !== void 0),
1686
+ weightedScores: weightedScoresForValues(answeredValues, step)
1687
+ }];
1688
+ });
1689
+ }
1690
+ //#endregion
1691
+ //#region src/survey/snapshot.ts
1692
+ function stringifyAnswer(value) {
1693
+ if (typeof value === "string") return value;
1694
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) return value.join(", ");
1695
+ if (typeof value === "number") return String(value);
1696
+ if (typeof value === "boolean") return value ? "true" : "false";
1697
+ }
1698
+ function addAnswer(output, answers, name) {
1699
+ const value = stringifyAnswer(answers[name]);
1700
+ if (value !== void 0) output[name] = value;
1701
+ }
1702
+ function addStepAnswers(output, answers, step) {
1703
+ if (step.kind === "fields") {
1704
+ for (const field of step.fields ?? []) addAnswer(output, answers, field.name);
1705
+ return;
1706
+ }
1707
+ if (step.name) {
1708
+ addAnswer(output, answers, step.name);
1709
+ if (step.kind === "single" || step.kind === "multi") {
1710
+ const selected = answers[step.name];
1711
+ const selectedValues = typeof selected === "string" ? [selected] : Array.isArray(selected) && selected.every((value) => typeof value === "string") ? selected : [];
1712
+ const otherOption = step.options?.find((option) => option.other === true && selectedValues.includes(option.value));
1713
+ const otherAnswer = answers[`${step.name}_other`];
1714
+ if (otherOption && typeof otherAnswer === "string" && otherAnswer.trim() !== "") addAnswer(output, answers, `${step.name}_other`);
1715
+ }
1716
+ }
1717
+ }
1718
+ function buildSurveySnapshot(definition, state) {
1719
+ const pathIds = new Set(state.path);
1720
+ const answers = {};
1721
+ for (const step of definition.steps) if (pathIds.has(step.id)) addStepAnswers(answers, state.answers, step);
1722
+ return {
1723
+ definitionId: state.definitionId,
1724
+ sessionId: state.sessionId,
1725
+ current: state.current,
1726
+ path: [...state.path],
1727
+ answers
1728
+ };
1729
+ }
1730
+ //#endregion
1731
+ //#region src/survey/validate.ts
1732
+ function isEmpty(value) {
1733
+ if (value === void 0 || value === null) return true;
1734
+ if (typeof value === "string") return value.trim() === "";
1735
+ return Array.isArray(value) && value.length === 0;
1736
+ }
1737
+ function asString(value) {
1738
+ if (typeof value === "string") return value;
1739
+ if (value === void 0 || value === null) return "";
1740
+ return String(value);
1741
+ }
1742
+ function replaceMessage(template, replacements) {
1743
+ return Object.entries(replacements).reduce((message, [key, replacement]) => message.replaceAll(`{${key}}`, replacement), template);
1744
+ }
1745
+ function getMessage(lang, key, replacements = {}) {
1746
+ const messages = getErrorMessagesForLanguage(lang);
1747
+ const fallbackKey = key === "postal" ? "pattern" : key;
1748
+ return replaceMessage(messages[key] ?? messages[fallbackKey] ?? messages.required ?? "Invalid value", {
1749
+ ...key === "phone" ? { example: getPhoneExample() } : {},
1750
+ ...replacements
1751
+ });
1752
+ }
1753
+ function addError(errors, key, message) {
1754
+ if (errors[key] === void 0) errors[key] = message;
1755
+ }
1756
+ function validateRequired(value, key, lang, errors) {
1757
+ if (isEmpty(value)) addError(errors, key, getMessage(lang, "required"));
1758
+ }
1759
+ function validateStepRule(rule, value, key, lang, errors) {
1760
+ if (rule.rule === "required") {
1761
+ validateRequired(value, key, lang, errors);
1762
+ return;
1763
+ }
1764
+ const selectedCount = Array.isArray(value) ? value.length : 0;
1765
+ if (rule.rule === "minSelected" && selectedCount < rule.arg) {
1766
+ addError(errors, key, getMessage(lang, "minValue", { minValue: String(rule.arg) }));
1767
+ return;
1768
+ }
1769
+ if (rule.rule === "maxSelected" && selectedCount > rule.arg) addError(errors, key, getMessage(lang, "maxValue", { maxValue: String(rule.arg) }));
1770
+ }
1771
+ function validateScale(step, value, key, lang, errors) {
1772
+ if (!step.scale) return;
1773
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1774
+ addError(errors, key, getMessage(lang, "minValue", { minValue: String(step.scale.min) }));
1775
+ return;
1776
+ }
1777
+ if (value < step.scale.min) {
1778
+ addError(errors, key, getMessage(lang, "minValue", { minValue: String(step.scale.min) }));
1779
+ return;
1780
+ }
1781
+ if (value > step.scale.max) addError(errors, key, getMessage(lang, "maxValue", { maxValue: String(step.scale.max) }));
1782
+ }
1783
+ function phoneIsValid(value) {
1784
+ const stripped = value.replace(/[\s()+-]/g, "");
1785
+ return /^\d{6,15}$/.test(stripped);
1786
+ }
1787
+ function postalIsValid(value) {
1788
+ return /^\d{4}$/.test(value);
1789
+ }
1790
+ function fieldRuleIsValid(rule, value) {
1791
+ if (typeof rule === "string") {
1792
+ const stringValue = asString(value);
1793
+ switch (rule) {
1794
+ case "required": return !isEmpty(value);
1795
+ case "email": return validators.email(stringValue);
1796
+ case "name": return validators.name(stringValue);
1797
+ case "phone": return phoneIsValid(stringValue);
1798
+ case "postal": return postalIsValid(stringValue);
1799
+ }
1800
+ }
1801
+ const stringValue = asString(value);
1802
+ return validators[rule.rule](stringValue, rule.arg);
1803
+ }
1804
+ function fieldRuleMessage(rule, lang) {
1805
+ if (typeof rule === "string") return getMessage(lang, rule);
1806
+ if (rule.rule === "pattern") return getMessage(lang, "pattern");
1807
+ return getMessage(lang, rule.rule, { [rule.rule]: rule.arg });
1808
+ }
1809
+ function validateSurveyStep(step, answers, lang) {
1810
+ const errors = {};
1811
+ if (step.kind === "fields") {
1812
+ for (const field of step.fields ?? []) {
1813
+ const value = answers[field.name];
1814
+ for (const rule of field.rules ?? []) {
1815
+ if (fieldRuleIsValid(rule, value)) continue;
1816
+ addError(errors, field.name, fieldRuleMessage(rule, lang));
1817
+ break;
1818
+ }
1819
+ }
1820
+ return Object.keys(errors).length > 0 ? errors : null;
1821
+ }
1822
+ const key = step.name;
1823
+ if (!key) return null;
1824
+ const value = answers[key];
1825
+ for (const rule of step.validate ?? []) validateStepRule(rule, value, key, lang, errors);
1826
+ if (step.kind === "scale") validateScale(step, value, key, lang, errors);
1827
+ return Object.keys(errors).length > 0 ? errors : null;
1828
+ }
1829
+ //#endregion
1830
+ //#region src/survey/engine.ts
1831
+ function cloneValue(value, seen = /* @__PURE__ */ new WeakMap()) {
1832
+ if (value === null || typeof value !== "object") return value;
1833
+ const existing = seen.get(value);
1834
+ if (existing !== void 0) return existing;
1835
+ if (value instanceof Date) return new Date(value.getTime());
1836
+ if (Array.isArray(value)) {
1837
+ const copy = [];
1838
+ seen.set(value, copy);
1839
+ for (const item of value) copy.push(cloneValue(item, seen));
1840
+ return copy;
1841
+ }
1842
+ const copy = {};
1843
+ seen.set(value, copy);
1844
+ for (const [key, item] of Object.entries(value)) copy[key] = cloneValue(item, seen);
1845
+ return copy;
1846
+ }
1847
+ function cloneState(state) {
1848
+ return {
1849
+ version: state.version,
1850
+ definitionId: state.definitionId,
1851
+ current: state.current,
1852
+ path: [...state.path],
1853
+ answers: cloneValue(state.answers),
1854
+ status: state.status,
1855
+ sessionId: state.sessionId
1856
+ };
1857
+ }
1858
+ function randomSessionId() {
1859
+ const cryptoApi = globalThis.crypto;
1860
+ if (cryptoApi && typeof cryptoApi.randomUUID === "function") return cryptoApi.randomUUID();
1861
+ if (cryptoApi && typeof cryptoApi.getRandomValues === "function") {
1862
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(8));
1863
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1864
+ }
1865
+ return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
1866
+ }
1867
+ function findStep(definition, id) {
1868
+ return definition.steps.find((step) => step.id === id);
1869
+ }
1870
+ function getInitialStep(definition, answers) {
1871
+ return definition.steps.find((step) => stepPassesShowIf(step, answers)) ?? definition.steps[0];
1872
+ }
1873
+ function restoreOrCreateState(definition, options, sessionId) {
1874
+ const initialState = options.initialState;
1875
+ if (initialState?.definitionId === definition.id && findStep(definition, initialState.current)) return {
1876
+ ...cloneState(initialState),
1877
+ sessionId
1878
+ };
1879
+ const current = getInitialStep(definition, {});
1880
+ return {
1881
+ version: 1,
1882
+ definitionId: definition.id,
1883
+ current: current.id,
1884
+ path: [current.id],
1885
+ answers: {},
1886
+ status: "idle",
1887
+ sessionId
1888
+ };
1889
+ }
1890
+ function findNextVisibleStep(definition, index, answers) {
1891
+ return definition.steps.slice(index + 1).find((step) => stepPassesShowIf(step, answers));
1892
+ }
1893
+ function resolveDestination(definition, current, action, answers) {
1894
+ if (action.kind === "goto") {
1895
+ const target = findStep(definition, action.target);
1896
+ if (!target) return { kind: "noop" };
1897
+ if (current.terminal) return { kind: "submit" };
1898
+ if (stepPassesShowIf(target, answers)) return {
1899
+ kind: "step",
1900
+ id: target.id,
1901
+ truncate: true
1902
+ };
1903
+ const next = findNextVisibleStep(definition, definition.steps.findIndex((step) => step.id === target.id), answers);
1904
+ return next ? {
1905
+ kind: "step",
1906
+ id: next.id,
1907
+ truncate: false
1908
+ } : { kind: "submit" };
1909
+ }
1910
+ if (action.kind === "redirect") return {
1911
+ kind: "submit",
1912
+ redirect: action.target
1913
+ };
1914
+ if (current.terminal) return { kind: "submit" };
1915
+ const next = findNextVisibleStep(definition, definition.steps.findIndex((step) => step.id === current.id), answers);
1916
+ return next ? {
1917
+ kind: "step",
1918
+ id: next.id,
1919
+ truncate: false
1920
+ } : { kind: "submit" };
1921
+ }
1922
+ function submissionResult(redirect) {
1923
+ return redirect === void 0 ? { kind: "submit" } : {
1924
+ kind: "submit",
1925
+ redirect
1926
+ };
1927
+ }
1928
+ function createSurveyEngine(definition, options = {}) {
1929
+ if (definition.steps.length === 0) throw new Error("A survey definition must contain at least one step");
1930
+ const ids = /* @__PURE__ */ new Set();
1931
+ for (const step of definition.steps) {
1932
+ if (ids.has(step.id)) throw new Error(`Duplicate survey step id: ${step.id}`);
1933
+ ids.add(step.id);
1934
+ }
1935
+ let state = restoreOrCreateState(definition, options, options.sessionId ?? options.initialState?.sessionId ?? randomSessionId());
1936
+ let errors = {};
1937
+ let revision = 0;
1938
+ const listeners = /* @__PURE__ */ new Set();
1939
+ function notify() {
1940
+ for (const listener of listeners) try {
1941
+ listener();
1942
+ } catch {}
1943
+ }
1944
+ function replaceState(next) {
1945
+ state = cloneState(next);
1946
+ revision += 1;
1947
+ notify();
1948
+ }
1949
+ function replaceErrors(next) {
1950
+ errors = { ...next };
1951
+ notify();
1952
+ }
1953
+ function clearErrors() {
1954
+ if (Object.keys(errors).length === 0) return;
1955
+ errors = {};
1956
+ }
1957
+ function currentStep() {
1958
+ return findStep(definition, state.current) ?? definition.steps[0];
1959
+ }
1960
+ function isNavigationBlocked() {
1961
+ return state.status === "submitting" || state.status === "done";
1962
+ }
1963
+ function applyAnswer(name, value) {
1964
+ clearErrors();
1965
+ replaceState({
1966
+ ...state,
1967
+ answers: {
1968
+ ...state.answers,
1969
+ [name]: cloneValue(value)
1970
+ }
1971
+ });
1972
+ }
1973
+ function moveTo(id, truncate) {
1974
+ const pathIndex = truncate ? state.path.lastIndexOf(id) : -1;
1975
+ const path = pathIndex >= 0 ? state.path.slice(0, pathIndex + 1) : [...state.path, id];
1976
+ if (state.current === id && path.length === state.path.length) return {
1977
+ kind: "moved",
1978
+ to: id
1979
+ };
1980
+ clearErrors();
1981
+ replaceState({
1982
+ ...state,
1983
+ current: id,
1984
+ path
1985
+ });
1986
+ return {
1987
+ kind: "moved",
1988
+ to: id
1989
+ };
1990
+ }
1991
+ function applyDestination(destination) {
1992
+ if (destination.kind === "noop") return { kind: "noop" };
1993
+ if (destination.kind === "submit") {
1994
+ replaceState({
1995
+ ...state,
1996
+ status: "submitting"
1997
+ });
1998
+ return submissionResult(destination.redirect);
1999
+ }
2000
+ return moveTo(destination.id, destination.truncate);
2001
+ }
2002
+ function resolveAction(step, value, explicitNav) {
2003
+ if (explicitNav) return explicitNav;
2004
+ if (step.kind === "single" && typeof value === "string") {
2005
+ const optionNav = step.options?.find((option) => option.value === value)?.nav;
2006
+ if (optionNav) return optionNav;
2007
+ }
2008
+ return step.nav ?? { kind: "next" };
2009
+ }
2010
+ async function validateBeforeMove(step, answers) {
2011
+ const builtInErrors = validateSurveyStep(step, answers, definition.lang);
2012
+ if (builtInErrors) return builtInErrors;
2013
+ if (!options.validate) return null;
2014
+ return options.validate({
2015
+ step: cloneValue(step),
2016
+ answers: cloneValue(answers),
2017
+ lang: definition.lang
2018
+ });
2019
+ }
2020
+ async function next(nav) {
2021
+ if (isNavigationBlocked()) return { kind: "noop" };
2022
+ const step = currentStep();
2023
+ const answers = cloneValue(state.answers);
2024
+ const capturedRevision = revision;
2025
+ const validationErrors = await validateBeforeMove(step, answers);
2026
+ if (revision !== capturedRevision) return { kind: "stale" };
2027
+ if (validationErrors && Object.keys(validationErrors).length > 0) {
2028
+ replaceErrors(validationErrors);
2029
+ return {
2030
+ kind: "blocked",
2031
+ errors: { ...validationErrors }
2032
+ };
2033
+ }
2034
+ return applyDestination(resolveDestination(definition, step, resolveAction(step, step.name ? answers[step.name] : void 0, nav), answers));
2035
+ }
2036
+ async function choose(name, value, nav) {
2037
+ if (isNavigationBlocked()) return { kind: "noop" };
2038
+ const step = currentStep();
2039
+ applyAnswer(name, value);
2040
+ const answers = cloneValue(state.answers);
2041
+ const capturedRevision = revision;
2042
+ const validationErrors = await validateBeforeMove(step, answers);
2043
+ if (revision !== capturedRevision) return { kind: "stale" };
2044
+ if (validationErrors && Object.keys(validationErrors).length > 0) {
2045
+ replaceErrors(validationErrors);
2046
+ return {
2047
+ kind: "blocked",
2048
+ errors: { ...validationErrors }
2049
+ };
2050
+ }
2051
+ return applyDestination(resolveDestination(definition, step, resolveAction(step, value, nav), answers));
2052
+ }
2053
+ function back() {
2054
+ if (isNavigationBlocked()) return { kind: "noop" };
2055
+ if (currentStep().back === false || state.path.length <= 1) return { kind: "noop" };
2056
+ const path = state.path.slice(0, -1);
2057
+ const previous = path[path.length - 1];
2058
+ if (!previous) return { kind: "noop" };
2059
+ clearErrors();
2060
+ replaceState({
2061
+ ...state,
2062
+ current: previous,
2063
+ path
2064
+ });
2065
+ return {
2066
+ kind: "moved",
2067
+ to: previous
2068
+ };
2069
+ }
2070
+ function goto(id) {
2071
+ if (isNavigationBlocked()) return { kind: "noop" };
2072
+ const step = currentStep();
2073
+ const answers = cloneValue(state.answers);
2074
+ return applyDestination(resolveDestination(definition, step, {
2075
+ kind: "goto",
2076
+ target: id
2077
+ }, answers));
2078
+ }
2079
+ function getState() {
2080
+ return cloneState(state);
2081
+ }
2082
+ function getView() {
2083
+ const step = currentStep();
2084
+ const inputs = deriveQuizInputs(definition, state);
2085
+ return {
2086
+ step: cloneValue(step),
2087
+ index: definition.steps.findIndex((candidate) => candidate.id === step.id),
2088
+ canBack: !isNavigationBlocked() && state.path.length > 1 && step.back !== false,
2089
+ isTerminal: step.terminal === true,
2090
+ progress: computeProgress(definition, state),
2091
+ errors: { ...errors },
2092
+ quiz: inputs.length > 0 ? scoreQuiz(inputs, definition.quiz ?? {}) : null
2093
+ };
2094
+ }
2095
+ function subscribe(listener) {
2096
+ listeners.add(listener);
2097
+ return () => {
2098
+ listeners.delete(listener);
2099
+ };
2100
+ }
2101
+ function answer(name, value) {
2102
+ applyAnswer(name, value);
2103
+ }
2104
+ function snapshot() {
2105
+ return buildSurveySnapshot(definition, state);
2106
+ }
2107
+ function setStatus(status) {
2108
+ replaceState({
2109
+ ...state,
2110
+ status
2111
+ });
2112
+ }
2113
+ return {
2114
+ getState,
2115
+ getView,
2116
+ subscribe,
2117
+ answer,
2118
+ choose,
2119
+ next,
2120
+ back,
2121
+ goto,
2122
+ snapshot,
2123
+ setStatus
2124
+ };
2125
+ }
2126
+ //#endregion
2127
+ //#region src/survey/schema.ts
2128
+ const predicateOpSchema = z.enum([
2129
+ "eq",
2130
+ "neq",
2131
+ "in",
2132
+ "nin",
2133
+ "gt",
2134
+ "gte",
2135
+ "lt",
2136
+ "lte",
2137
+ "exists",
2138
+ "truthy",
2139
+ "falsy",
2140
+ "contains",
2141
+ "starts_with",
2142
+ "ends_with"
2143
+ ]);
2144
+ const predicateSchema = z.lazy(() => z.union([z.object({
2145
+ field: z.string(),
2146
+ op: predicateOpSchema,
2147
+ value: z.unknown().optional()
2148
+ }), z.object({
2149
+ all: z.array(predicateSchema).optional(),
2150
+ any: z.array(predicateSchema).optional(),
2151
+ not: predicateSchema.optional()
2152
+ }).refine((predicate) => predicate.all !== void 0 || predicate.any !== void 0 || predicate.not !== void 0)]));
2153
+ const navActionSchema = z.discriminatedUnion("kind", [
2154
+ z.object({ kind: z.literal("next") }),
2155
+ z.object({
2156
+ kind: z.literal("goto"),
2157
+ target: z.string()
2158
+ }),
2159
+ z.object({
2160
+ kind: z.literal("redirect"),
2161
+ target: z.string()
2162
+ })
2163
+ ]);
2164
+ const surveyOptionSchema = z.object({
2165
+ value: z.string(),
2166
+ label: z.string().optional(),
2167
+ nav: navActionSchema.optional(),
2168
+ correct: z.boolean().optional(),
2169
+ weights: z.record(z.string(), z.number()).optional(),
2170
+ other: z.boolean().optional()
2171
+ });
2172
+ const surveyFieldRuleSchema = z.union([z.enum([
2173
+ "required",
2174
+ "email",
2175
+ "name",
2176
+ "phone",
2177
+ "postal"
2178
+ ]), z.object({
2179
+ rule: z.enum([
2180
+ "pattern",
2181
+ "min",
2182
+ "max"
2183
+ ]),
2184
+ arg: z.string()
2185
+ })]);
2186
+ const surveyFieldSchema = z.object({
2187
+ name: z.string(),
2188
+ type: z.enum([
2189
+ "text",
2190
+ "email",
2191
+ "tel",
2192
+ "textarea",
2193
+ "postal",
2194
+ "address"
2195
+ ]),
2196
+ rules: z.array(surveyFieldRuleSchema).optional()
2197
+ });
2198
+ const surveyStepRuleSchema = z.union([z.object({ rule: z.literal("required") }), z.object({
2199
+ rule: z.enum(["minSelected", "maxSelected"]),
2200
+ arg: z.number()
2201
+ })]);
2202
+ const surveyStepQuizSchema = z.object({
2203
+ model: z.enum(["single_select", "multi_select"]),
2204
+ section: z.string().optional(),
2205
+ scoring: z.enum(["exact", "partial"]).optional()
2206
+ });
2207
+ const quizPassFailSchema = z.object({ percentGte: z.number() });
2208
+ const quizBandSchema = z.object({
2209
+ id: z.string(),
2210
+ label: z.string(),
2211
+ minPercent: z.number().optional(),
2212
+ maxPercent: z.number().optional()
2213
+ });
2214
+ const quizSectionSchema = z.object({
2215
+ passFail: quizPassFailSchema.nullable().optional(),
2216
+ bands: z.array(quizBandSchema).nullable().optional()
2217
+ });
2218
+ const quizConfigSchema = z.object({
2219
+ passFail: quizPassFailSchema.nullable().optional(),
2220
+ bands: z.array(quizBandSchema).nullable().optional(),
2221
+ sections: z.record(z.string(), quizSectionSchema).nullable().optional()
2222
+ });
2223
+ const surveyStepSchema = z.object({
2224
+ id: z.string(),
2225
+ kind: z.enum([
2226
+ "content",
2227
+ "single",
2228
+ "multi",
2229
+ "scale",
2230
+ "fields"
2231
+ ]),
2232
+ name: z.string().optional(),
2233
+ showIf: predicateSchema.optional(),
2234
+ back: z.boolean().optional(),
2235
+ terminal: z.boolean().optional(),
2236
+ nav: navActionSchema.optional(),
2237
+ options: z.array(surveyOptionSchema).optional(),
2238
+ scale: z.object({
2239
+ min: z.number(),
2240
+ max: z.number(),
2241
+ step: z.number().optional()
2242
+ }).optional(),
2243
+ fields: z.array(surveyFieldSchema).optional(),
2244
+ validate: z.array(surveyStepRuleSchema).optional(),
2245
+ quiz: surveyStepQuizSchema.optional()
2246
+ });
2247
+ const surveyDefinitionSchema = z.object({
2248
+ version: z.literal(1),
2249
+ id: z.string(),
2250
+ steps: z.array(surveyStepSchema),
2251
+ progress: z.enum(["static", "path"]).optional(),
2252
+ quiz: quizConfigSchema.optional(),
2253
+ lang: z.string().optional()
2254
+ }).superRefine((definition, context) => {
2255
+ const ids = /* @__PURE__ */ new Set();
2256
+ definition.steps.forEach((step, index) => {
2257
+ if (ids.has(step.id)) context.addIssue({
2258
+ code: "custom",
2259
+ message: `Duplicate step id: ${step.id}`,
2260
+ path: [
2261
+ "steps",
2262
+ index,
2263
+ "id"
2264
+ ]
2265
+ });
2266
+ ids.add(step.id);
2267
+ if ((step.kind === "single" || step.kind === "multi" || step.kind === "scale") && !step.name) context.addIssue({
2268
+ code: "custom",
2269
+ message: `Step "${step.id}" requires a name`,
2270
+ path: [
2271
+ "steps",
2272
+ index,
2273
+ "name"
2274
+ ]
2275
+ });
2276
+ if ((step.kind === "single" || step.kind === "multi") && step.options === void 0) context.addIssue({
2277
+ code: "custom",
2278
+ message: `Step "${step.id}" requires options`,
2279
+ path: [
2280
+ "steps",
2281
+ index,
2282
+ "options"
2283
+ ]
2284
+ });
2285
+ if (step.kind === "scale" && step.scale === void 0) context.addIssue({
2286
+ code: "custom",
2287
+ message: `Step "${step.id}" requires scale`,
2288
+ path: [
2289
+ "steps",
2290
+ index,
2291
+ "scale"
2292
+ ]
2293
+ });
2294
+ if (step.kind === "fields" && step.fields === void 0) context.addIssue({
2295
+ code: "custom",
2296
+ message: `Step "${step.id}" requires fields`,
2297
+ path: [
2298
+ "steps",
2299
+ index,
2300
+ "fields"
2301
+ ]
2302
+ });
2303
+ });
2304
+ const knownIds = new Set(definition.steps.map((step) => step.id));
2305
+ const checkNav = (nav, path) => {
2306
+ if (nav?.kind === "goto" && !knownIds.has(nav.target)) context.addIssue({
2307
+ code: "custom",
2308
+ message: `Unknown goto target: ${nav.target}`,
2309
+ path
2310
+ });
2311
+ };
2312
+ definition.steps.forEach((step, index) => {
2313
+ checkNav(step.nav, [
2314
+ "steps",
2315
+ index,
2316
+ "nav"
2317
+ ]);
2318
+ for (const [optionIndex, option] of (step.options ?? []).entries()) checkNav(option.nav, [
2319
+ "steps",
2320
+ index,
2321
+ "options",
2322
+ optionIndex,
2323
+ "nav"
2324
+ ]);
2325
+ });
2326
+ });
2327
+ function parseSurveyDefinition(input) {
2328
+ return surveyDefinitionSchema.parse(input);
2329
+ }
2330
+ //#endregion
2331
+ export { CRM_CONTACT_KEYS, VISITOR_ID_STORAGE_KEY, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildBasinFormData, buildContext, buildCounterPayload, buildFcrmUtms, buildSurveySnapshot, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, computeProgress, createAttemptStore, createLifecycleBus, createMemoryCache, createSessionStore, createStepsStore, createSurveyEngine, createTrackingConsumer, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, deriveQuizInputs, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, firstDefined, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getOrCreateVisitorId, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, hashSurveySnapshot, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, mapTrackingEvent, matchesFormat, mergeAnswers, normalizeResult, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, parseSurveyDefinition, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, snapshot, surveyDefinitionSchema, unknownResult, updateUrlWithParams, validators };
1570
2332
 
1571
2333
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@reform-society/agera-core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "DOM-free core logic for Agera",
5
5
  "type": "module",
6
+ "sideEffects": false,
6
7
  "main": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
8
9
  "exports": {
@@ -42,6 +43,7 @@
42
43
  },
43
44
  "dependencies": {
44
45
  "loglevel": "^1.9.2",
45
- "nanostores": "^1.5.2"
46
+ "nanostores": "^1.5.2",
47
+ "zod": "^4.1.0"
46
48
  }
47
49
  }