@funnelsgrove/runtime 0.1.55 → 0.1.59

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.
Files changed (56) hide show
  1. package/dist/components/FunnelContext.d.ts +5 -1
  2. package/dist/components/FunnelContext.js +2 -1
  3. package/dist/components/ManageSubscriptionScreen.js +4 -1
  4. package/dist/components/SubscriptionHandoffScreen.d.ts +2 -1
  5. package/dist/components/SubscriptionHandoffScreen.js +11 -6
  6. package/dist/config/funnel.manifest.types.d.ts +4 -2
  7. package/dist/generated/step-contract-hash.d.ts +1 -0
  8. package/dist/generated/step-contract-hash.js +2 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.js +10 -0
  11. package/dist/migrations/step-contract-v2.d.ts +50 -0
  12. package/dist/migrations/step-contract-v2.js +310 -0
  13. package/dist/migrations/step-contract-v3.d.ts +41 -0
  14. package/dist/migrations/step-contract-v3.js +26 -0
  15. package/dist/runtime/funnel-attribution.d.ts +1 -0
  16. package/dist/runtime/funnel-attribution.js +24 -0
  17. package/dist/runtime/funnel-manifest.validation.d.ts +7 -2
  18. package/dist/runtime/funnel-manifest.validation.js +24 -68
  19. package/dist/runtime/funnel-step-lifecycle.d.ts +35 -0
  20. package/dist/runtime/funnel-step-lifecycle.js +49 -0
  21. package/dist/runtime/funnel-step-metadata.validation.d.ts +7 -0
  22. package/dist/runtime/funnel-step-metadata.validation.js +88 -0
  23. package/dist/runtime/submit-email-capture.d.ts +23 -0
  24. package/dist/runtime/submit-email-capture.js +54 -0
  25. package/dist/runtime/use-funnel-flow-controller.d.ts +71 -5
  26. package/dist/runtime/use-funnel-flow-controller.js +850 -116
  27. package/dist/runtime/use-step-choices.d.ts +58 -0
  28. package/dist/runtime/use-step-choices.js +187 -0
  29. package/dist/sdk/userAnswers.d.ts +1 -0
  30. package/dist/services/api.service.d.ts +10 -0
  31. package/dist/services/api.service.js +32 -0
  32. package/dist/services/funnel-sdk.service.d.ts +17 -0
  33. package/dist/services/funnel-sdk.service.js +13 -0
  34. package/dist/steps/choice-contract.d.ts +33 -0
  35. package/dist/steps/choice-contract.js +302 -0
  36. package/dist/steps/step-contract.d.ts +498 -0
  37. package/dist/steps/step-contract.js +506 -0
  38. package/dist/steps/types.d.ts +7 -5
  39. package/dist/steps/types.js +1 -18
  40. package/dist/testing/funnel-contract-journey-driver.d.ts +35 -0
  41. package/dist/testing/funnel-contract-journey-driver.js +266 -0
  42. package/dist/testing/index.d.ts +1 -0
  43. package/dist/testing/index.js +1 -0
  44. package/dist/validation/fixtures/invalid-reserved-identities.d.ts +249 -0
  45. package/dist/validation/fixtures/invalid-reserved-identities.js +20 -0
  46. package/dist/validation/fixtures/valid-v2-project.d.ts +419 -0
  47. package/dist/validation/fixtures/valid-v2-project.js +228 -0
  48. package/dist/validation/fixtures/valid-v3-project.d.ts +419 -0
  49. package/dist/validation/fixtures/valid-v3-project.js +30 -0
  50. package/dist/validation/funnel-contract-diagnostics.d.ts +49 -0
  51. package/dist/validation/funnel-contract-diagnostics.js +67 -0
  52. package/dist/validation/funnel-contract-validator.d.ts +30 -0
  53. package/dist/validation/funnel-contract-validator.js +1458 -0
  54. package/dist/validation/funnel-manifest-input.normalization.d.ts +67 -0
  55. package/dist/validation/funnel-manifest-input.normalization.js +386 -0
  56. package/package.json +9 -1
@@ -0,0 +1,58 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type FunnelChoiceCompletion } from '../steps/choice-contract.js';
3
+ import { type FunnelChoiceOption, type FunnelEmojiChoiceOption } from '../steps/step-contract.js';
4
+ import type { FunnelStepMeta } from '../steps/types.js';
5
+ import { type FunnelContractDiagnostic } from '../validation/funnel-contract-diagnostics.js';
6
+ type ChoiceOptions = readonly (FunnelChoiceOption | FunnelEmojiChoiceOption)[];
7
+ type ChoiceResult = FunnelChoiceCompletion | FunnelContractDiagnostic;
8
+ type ChoiceCommit = (stepMeta: FunnelStepMeta, result: ChoiceResult) => void;
9
+ export type FunnelStepChoices = {
10
+ selectSingle: (optionId: string) => void;
11
+ completeMulti: (optionIds: readonly string[]) => void;
12
+ };
13
+ export declare const createStepChoiceActions: (stepMeta: FunnelStepMeta, options: ChoiceOptions, commit: ChoiceCommit) => FunnelStepChoices;
14
+ export declare const registerInternalChoiceCommit: (carrier: object, commit: ChoiceCommit) => void;
15
+ export declare const FunnelChoiceCommitProvider: ({ carrier, children, }: {
16
+ carrier: object;
17
+ children: ReactNode;
18
+ }) => import("react").FunctionComponentElement<import("react").ProviderProps<ChoiceCommit | null>>;
19
+ export declare const useStepChoices: (stepMeta: FunnelStepMeta, options: ChoiceOptions) => FunnelStepChoices;
20
+ export type AtomicChoiceCommitResult = 'committed' | 'ignored' | 'quarantined' | 'record-failed';
21
+ export declare const consumeChoiceCompletionMarker: (marker: string | null, previousStepId: string) => {
22
+ consumed: boolean;
23
+ nextMarker: string | null;
24
+ };
25
+ type AtomicChoiceCommitInput<StepId extends string> = {
26
+ activeStepId: StepId;
27
+ activeStepMeta: FunnelStepMeta | undefined;
28
+ stepMeta: FunnelStepMeta;
29
+ result: ChoiceResult;
30
+ environment: 'development' | 'production' | 'test';
31
+ getAttributes: () => Record<string, unknown>;
32
+ writeAttributes: (attributes: Record<string, unknown>) => void;
33
+ isInFlight: () => boolean;
34
+ beginCommit: () => void;
35
+ cancelCommit: () => void;
36
+ recordCompletion: (input: {
37
+ selected?: Record<string, unknown>;
38
+ metadata?: Record<string, unknown>;
39
+ }) => boolean;
40
+ markCompletedBeforeNavigation: () => void;
41
+ clearCompletedBeforeNavigation: (markerWasInstalled: boolean) => void;
42
+ resolveNext: (attributes: Record<string, unknown>) => StepId;
43
+ navigate: (stepId: StepId) => void;
44
+ reportDiagnostic: (diagnostic: FunnelContractDiagnostic) => void;
45
+ };
46
+ export declare const runAtomicChoiceCommit: <StepId extends string>(input: AtomicChoiceCommitInput<StepId>) => AtomicChoiceCommitResult;
47
+ type ChoiceBypassOperation = 'setAnswer' | 'setAttribute' | 'goChoice' | 'completeStep';
48
+ export declare const enforceChoiceBypassGuard: (input: {
49
+ operation: ChoiceBypassOperation;
50
+ activeStepId: string;
51
+ sourceStepId?: string;
52
+ key?: string;
53
+ stepById: Record<string, FunnelStepMeta>;
54
+ stepContractVersion: number;
55
+ environment: "development" | "production" | "test";
56
+ reportDiagnostic: (diagnostic: FunnelContractDiagnostic) => void;
57
+ }) => boolean;
58
+ export {};
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+ import { createContext, createElement, useContext, useMemo, } from 'react';
3
+ import { buildChoiceCompletion, parseFunnelChoiceDescriptor, } from '../steps/choice-contract.js';
4
+ import { LEGACY_UNVERSIONED_STEP_CONTRACT_VERSION, SUPPORTED_STEP_CONTRACT_VERSIONS, } from '../steps/step-contract.js';
5
+ import { createFunnelContractDiagnostic, } from '../validation/funnel-contract-diagnostics.js';
6
+ export const createStepChoiceActions = (stepMeta, options, commit) => ({
7
+ selectSingle: (optionId) => commit(stepMeta, buildChoiceCompletion(stepMeta, options, optionId)),
8
+ completeMulti: (optionIds) => commit(stepMeta, buildChoiceCompletion(stepMeta, options, optionIds)),
9
+ });
10
+ const FunnelChoiceCommitContext = createContext(null);
11
+ const internalChoiceCommits = new WeakMap();
12
+ export const registerInternalChoiceCommit = (carrier, commit) => {
13
+ internalChoiceCommits.set(carrier, commit);
14
+ };
15
+ export const FunnelChoiceCommitProvider = ({ carrier, children, }) => {
16
+ var _a;
17
+ return createElement(FunnelChoiceCommitContext.Provider, { value: (_a = internalChoiceCommits.get(carrier)) !== null && _a !== void 0 ? _a : null }, children);
18
+ };
19
+ export const useStepChoices = (stepMeta, options) => {
20
+ const commit = useContext(FunnelChoiceCommitContext);
21
+ if (!commit) {
22
+ throw new Error('useStepChoices must be used inside FunnelProvider');
23
+ }
24
+ return useMemo(() => createStepChoiceActions(stepMeta, options, commit), [commit, options, stepMeta]);
25
+ };
26
+ const isChoiceCompletion = (value) => {
27
+ try {
28
+ return typeof value === 'object'
29
+ && value !== null
30
+ && typeof value.answerKey === 'string'
31
+ && 'selected' in value
32
+ && 'value' in value;
33
+ }
34
+ catch (_a) {
35
+ return false;
36
+ }
37
+ };
38
+ const choiceIdentityMatches = (candidate, active) => {
39
+ if (!active) {
40
+ return false;
41
+ }
42
+ try {
43
+ const candidateDescriptor = parseFunnelChoiceDescriptor(candidate.type, candidate.choice);
44
+ const activeDescriptor = parseFunnelChoiceDescriptor(active.type, active.choice);
45
+ return candidate.id === active.id
46
+ && candidate.type === active.type
47
+ && candidateDescriptor.ok
48
+ && activeDescriptor.ok
49
+ && candidateDescriptor.descriptor.answerKey === activeDescriptor.descriptor.answerKey
50
+ && candidateDescriptor.descriptor.allowEmpty === activeDescriptor.descriptor.allowEmpty;
51
+ }
52
+ catch (_a) {
53
+ return false;
54
+ }
55
+ };
56
+ export const consumeChoiceCompletionMarker = (marker, previousStepId) => marker === previousStepId
57
+ ? { consumed: true, nextMarker: null }
58
+ : { consumed: false, nextMarker: marker };
59
+ export const runAtomicChoiceCommit = (input) => {
60
+ if (input.isInFlight()) {
61
+ return 'ignored';
62
+ }
63
+ const completion = isChoiceCompletion(input.result) ? input.result : null;
64
+ if (completion === null) {
65
+ const diagnostic = input.result;
66
+ if (diagnostic.stepId !== input.activeStepId) {
67
+ input.reportDiagnostic(diagnostic);
68
+ return 'ignored';
69
+ }
70
+ if (input.environment !== 'production') {
71
+ throw new Error(`${diagnostic.code}: ${diagnostic.reason}`);
72
+ }
73
+ input.beginCommit();
74
+ const currentAttributes = input.getAttributes();
75
+ let abortStarted = false;
76
+ let markerInstallationStarted = false;
77
+ const abortCommit = () => {
78
+ if (abortStarted) {
79
+ return;
80
+ }
81
+ abortStarted = true;
82
+ try {
83
+ input.clearCompletedBeforeNavigation(markerInstallationStarted);
84
+ }
85
+ finally {
86
+ input.cancelCommit();
87
+ }
88
+ };
89
+ try {
90
+ input.reportDiagnostic(diagnostic);
91
+ const nextStepId = input.resolveNext(currentAttributes);
92
+ if (!input.recordCompletion({})) {
93
+ abortCommit();
94
+ return 'record-failed';
95
+ }
96
+ markerInstallationStarted = true;
97
+ input.markCompletedBeforeNavigation();
98
+ input.navigate(nextStepId);
99
+ return 'quarantined';
100
+ }
101
+ catch (error) {
102
+ abortCommit();
103
+ throw error;
104
+ }
105
+ }
106
+ if (input.activeStepId !== input.stepMeta.id
107
+ || !choiceIdentityMatches(input.stepMeta, input.activeStepMeta)) {
108
+ return 'ignored';
109
+ }
110
+ input.beginCommit();
111
+ const currentAttributes = input.getAttributes();
112
+ let abortStarted = false;
113
+ let attributesWritten = false;
114
+ let markerInstallationStarted = false;
115
+ const abortCommit = () => {
116
+ if (abortStarted) {
117
+ return;
118
+ }
119
+ abortStarted = true;
120
+ try {
121
+ if (attributesWritten) {
122
+ input.writeAttributes(currentAttributes);
123
+ }
124
+ }
125
+ finally {
126
+ try {
127
+ input.clearCompletedBeforeNavigation(markerInstallationStarted);
128
+ }
129
+ finally {
130
+ input.cancelCommit();
131
+ }
132
+ }
133
+ };
134
+ try {
135
+ const nextAttributes = Object.assign(Object.assign({}, currentAttributes), { [completion.answerKey]: completion.value });
136
+ const nextStepId = input.resolveNext(nextAttributes);
137
+ attributesWritten = true;
138
+ input.writeAttributes(nextAttributes);
139
+ if (!input.recordCompletion({
140
+ selected: completion.selected,
141
+ metadata: { choice_answer_key: completion.answerKey },
142
+ })) {
143
+ abortCommit();
144
+ return 'record-failed';
145
+ }
146
+ markerInstallationStarted = true;
147
+ input.markCompletedBeforeNavigation();
148
+ input.navigate(nextStepId);
149
+ return 'committed';
150
+ }
151
+ catch (error) {
152
+ abortCommit();
153
+ throw error;
154
+ }
155
+ };
156
+ export const enforceChoiceBypassGuard = (input) => {
157
+ var _a;
158
+ const enforcesTypedChoices = SUPPORTED_STEP_CONTRACT_VERSIONS.some((version) => (version !== LEGACY_UNVERSIONED_STEP_CONTRACT_VERSION
159
+ && version === input.stepContractVersion));
160
+ if (!enforcesTypedChoices) {
161
+ return true;
162
+ }
163
+ const sourceStepId = (_a = input.sourceStepId) !== null && _a !== void 0 ? _a : input.activeStepId;
164
+ const sourceMeta = input.stepById[sourceStepId];
165
+ const activeMeta = input.stepById[input.activeStepId];
166
+ const shouldReject = input.operation === 'setAnswer' || input.operation === 'setAttribute'
167
+ ? Boolean((activeMeta === null || activeMeta === void 0 ? void 0 : activeMeta.choice) && input.key === activeMeta.choice.answerKey)
168
+ : Boolean(sourceMeta === null || sourceMeta === void 0 ? void 0 : sourceMeta.choice);
169
+ if (!shouldReject) {
170
+ return true;
171
+ }
172
+ const diagnostic = createFunnelContractDiagnostic({
173
+ code: 'FG-CHOICE-006',
174
+ file: null,
175
+ stepId: sourceStepId,
176
+ expected: 'useStepChoices',
177
+ received: { operation: input.operation },
178
+ reason: 'A generic answer or completion API attempted to bypass the typed choice transaction.',
179
+ guide: 'docs/product-specs/funnel-template-step-authoring.md',
180
+ repair: 'Complete descriptor-bearing choice steps with useStepChoices.',
181
+ });
182
+ if (input.environment !== 'production') {
183
+ throw new Error(`${diagnostic.code}: ${diagnostic.reason}`);
184
+ }
185
+ input.reportDiagnostic(diagnostic);
186
+ return false;
187
+ };
@@ -1,4 +1,5 @@
1
1
  import type { FunnelStepId } from '../runtime/funnel-runtime.js';
2
+ export type FunnelAnswerKey = string;
2
3
  export type FunnelBinaryChoice = 'yes' | 'no';
3
4
  export type FunnelBirthDate = {
4
5
  year: number;
@@ -51,6 +51,11 @@ export type TempPhotoUploadResult = {
51
51
  sizeBytes: number;
52
52
  uploadedAt: string;
53
53
  };
54
+ export type EmailCaptureResult = {
55
+ user: AppUser;
56
+ eventId: string;
57
+ eventCreated: boolean;
58
+ };
54
59
  declare class ApiService {
55
60
  private readFileAsDataUrl;
56
61
  getOrCreateClientUserId(): string;
@@ -68,6 +73,11 @@ declare class ApiService {
68
73
  updateUser(user: AppUser, options?: {
69
74
  attribution?: FunnelUserAttribution;
70
75
  }): Promise<AppUser>;
76
+ captureEmail(input: {
77
+ userId: string;
78
+ email: string;
79
+ environment: 'test' | 'live';
80
+ }): Promise<EmailCaptureResult>;
71
81
  syncUrlUserAttributes(input: {
72
82
  user: AppUserInput;
73
83
  attributes?: FunnelUserAnswers;
@@ -286,6 +286,38 @@ class ApiService {
286
286
  fallbackDocument: withMergedAttributionDocument(user.document, options === null || options === void 0 ? void 0 : options.attribution),
287
287
  });
288
288
  }
289
+ async captureEmail(input) {
290
+ var _a, _b;
291
+ const userId = input.userId.trim();
292
+ const email = input.email.trim().toLowerCase();
293
+ const payload = await funnelSdkService.captureEmail({
294
+ userId,
295
+ email,
296
+ environment: input.environment,
297
+ });
298
+ const eventId = asString(payload.eventId);
299
+ const responseUser = isRecord(payload.user) ? payload.user : null;
300
+ const responseUserId = responseUser === null || responseUser === void 0 ? void 0 : responseUser.user_id;
301
+ const responseEmail = (_b = (_a = asString(responseUser === null || responseUser === void 0 ? void 0 : responseUser.email)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : null;
302
+ if (!eventId
303
+ || typeof payload.eventCreated !== 'boolean'
304
+ || !responseUser
305
+ || typeof responseUserId !== 'string'
306
+ || responseUserId !== userId
307
+ || responseEmail !== email) {
308
+ throw new Error('Invalid email capture response');
309
+ }
310
+ const persistedUserId = persistUserId(userId, FUNNEL_ID);
311
+ return {
312
+ user: toAppUser({
313
+ apiUser: Object.assign(Object.assign({}, responseUser), { user_id: userId, email }),
314
+ userId: persistedUserId,
315
+ fallbackEmail: email,
316
+ }),
317
+ eventId,
318
+ eventCreated: payload.eventCreated,
319
+ };
320
+ }
289
321
  async syncUrlUserAttributes(input) {
290
322
  var _a;
291
323
  if (!hasUrlUserProfileAttributes(input.urlAttributes)) {
@@ -20,6 +20,22 @@ export type FunnelSdkBootstrapUserInput = {
20
20
  attribution?: FunnelSdkJsonObject | null;
21
21
  runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
22
22
  };
23
+ export type FunnelSdkCaptureEmailInput = {
24
+ userId: string;
25
+ email: string;
26
+ environment: FunnelSdkRuntimeMode;
27
+ runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
28
+ };
29
+ export type FunnelSdkCaptureEmailResponse = {
30
+ user: {
31
+ user_id?: unknown;
32
+ fullName?: unknown;
33
+ email?: unknown;
34
+ document?: unknown;
35
+ };
36
+ eventId: string;
37
+ eventCreated: boolean;
38
+ };
23
39
  export type FunnelSdkGetCurrentUserInput = {
24
40
  userId: string;
25
41
  runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
@@ -211,6 +227,7 @@ declare class FunnelSdkService {
211
227
  private getJson;
212
228
  private postJson;
213
229
  bootstrapUser<ResponsePayload = FunnelSdkJsonObject>(input?: FunnelSdkBootstrapUserInput): Promise<ResponsePayload>;
230
+ captureEmail(input: FunnelSdkCaptureEmailInput): Promise<FunnelSdkCaptureEmailResponse>;
214
231
  getCurrentUser<ResponsePayload = FunnelSdkJsonObject>(input: FunnelSdkGetCurrentUserInput): Promise<ResponsePayload>;
215
232
  getUserSubscriptionStatus<ResponsePayload = FunnelSdkJsonObject>(input?: FunnelSdkGetUserSubscriptionStatusInput): Promise<ResponsePayload>;
216
233
  claimUserSubscription<ResponsePayload = FunnelSdkJsonObject>(input: FunnelSdkClaimUserSubscriptionInput): Promise<ResponsePayload>;
@@ -1,5 +1,6 @@
1
1
  import { buildMainApiUrl, FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, FUNNEL_VERSION_ID, } from './runtime-api.config.js';
2
2
  import { resolvePreviewPaymentPublishableKey } from './public-env.js';
3
+ import { CURRENT_STEP_CONTRACT_VERSION } from '../steps/step-contract.js';
3
4
  const trimTrailingSlash = (value) => value.replace(/\/+$/, '');
4
5
  const toNormalizedPath = (path) => (path.startsWith('/') ? path : `/${path}`);
5
6
  const asTrimmedStringOrNull = (value) => {
@@ -113,6 +114,18 @@ class FunnelSdkService {
113
114
  errorMessage: 'Failed to bootstrap user session',
114
115
  });
115
116
  }
117
+ async captureEmail(input) {
118
+ return this.postJson('/sdk/public/users/email-capture', {
119
+ runtimeConfig: input.runtimeConfig,
120
+ body: {
121
+ user_id: input.userId,
122
+ email: input.email,
123
+ environment: input.environment,
124
+ stepContractVersion: CURRENT_STEP_CONTRACT_VERSION,
125
+ },
126
+ errorMessage: 'Failed to capture email',
127
+ });
128
+ }
116
129
  async getCurrentUser(input) {
117
130
  const { funnelId } = resolveRuntimeConfig(input.runtimeConfig);
118
131
  return this.getJson('/sdk/public/users/me', {
@@ -0,0 +1,33 @@
1
+ import type { FunnelAnswerKey } from '../sdk/userAnswers.js';
2
+ import { type FunnelContractDiagnostic } from '../validation/funnel-contract-diagnostics.js';
3
+ import { FUNNEL_STEP_CONTRACTS, type FunnelChoiceDescriptor, type FunnelChoiceOption, type FunnelEmojiChoiceOption, type FunnelStepChoiceContract, type FunnelStepType } from './step-contract.js';
4
+ import type { FunnelStepMeta } from './types.js';
5
+ export type FunnelChoiceStepType = {
6
+ [Type in FunnelStepType]: (typeof FUNNEL_STEP_CONTRACTS)[Type] extends {
7
+ readonly choice: FunnelStepChoiceContract;
8
+ } ? Type : never;
9
+ }[FunnelStepType];
10
+ export declare const FUNNEL_CHOICE_STEP_TYPES: readonly FunnelChoiceStepType[];
11
+ export type FunnelChoiceValue = string | string[];
12
+ export type FunnelChoiceCompletion = {
13
+ answerKey: FunnelAnswerKey;
14
+ value: FunnelChoiceValue;
15
+ selected: Record<string, FunnelChoiceValue>;
16
+ };
17
+ type FunnelChoiceStepMeta = Pick<FunnelStepMeta, 'id' | 'type' | 'choice'>;
18
+ type FunnelChoiceOptions = readonly (FunnelChoiceOption | FunnelEmojiChoiceOption)[];
19
+ export declare const MAX_FUNNEL_CHOICE_ITEMS = 4096;
20
+ export declare const MAX_FUNNEL_CHOICE_IDENTIFIER_LENGTH = 256;
21
+ export declare const MAX_FUNNEL_CHOICE_CONTENT_STRING_LENGTH = 4096;
22
+ export type FunnelChoiceDescriptorParseResult = {
23
+ ok: true;
24
+ descriptor: FunnelChoiceDescriptor;
25
+ contract: FunnelStepChoiceContract;
26
+ stepType: FunnelChoiceStepType;
27
+ } | {
28
+ ok: false;
29
+ issue: 'not-choice-step-type' | 'invalid-descriptor-shape' | 'invalid-answer-key' | 'invalid-allow-empty';
30
+ };
31
+ export declare const parseFunnelChoiceDescriptor: (stepType: unknown, choice: unknown) => FunnelChoiceDescriptorParseResult;
32
+ export declare const buildChoiceCompletion: (stepMeta: FunnelChoiceStepMeta, options: FunnelChoiceOptions, ids: string | readonly string[]) => FunnelChoiceCompletion | FunnelContractDiagnostic;
33
+ export {};