@appfunnel-dev/sdk 2.0.0-canary.1 → 2.0.0-canary.11

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/capabilities-Dq22RCr_.d.cts +180 -0
  3. package/dist/capabilities-Dq22RCr_.d.ts +180 -0
  4. package/dist/checkout-ClaO5IYV.d.ts +333 -0
  5. package/dist/checkout-DBp4bCpC.d.cts +333 -0
  6. package/dist/chunk-7JLOF6CJ.cjs +172 -0
  7. package/dist/chunk-7JLOF6CJ.cjs.map +1 -0
  8. package/dist/chunk-EMMSS5I5.cjs +37 -0
  9. package/dist/chunk-EMMSS5I5.cjs.map +1 -0
  10. package/dist/chunk-G3PMV62Z.js +33 -0
  11. package/dist/chunk-G3PMV62Z.js.map +1 -0
  12. package/dist/chunk-JAO6AA4R.js +99 -0
  13. package/dist/chunk-JAO6AA4R.js.map +1 -0
  14. package/dist/chunk-OXQBEKZ5.js +157 -0
  15. package/dist/chunk-OXQBEKZ5.js.map +1 -0
  16. package/dist/chunk-RKPRMTSU.cjs +517 -0
  17. package/dist/chunk-RKPRMTSU.cjs.map +1 -0
  18. package/dist/chunk-SXENKZ4U.js +486 -0
  19. package/dist/chunk-SXENKZ4U.js.map +1 -0
  20. package/dist/chunk-VV7TFC64.cjs +106 -0
  21. package/dist/chunk-VV7TFC64.cjs.map +1 -0
  22. package/dist/chunk-WYUDL4FI.cjs +8 -0
  23. package/dist/chunk-WYUDL4FI.cjs.map +1 -0
  24. package/dist/chunk-ZZJG4EYL.js +6 -0
  25. package/dist/chunk-ZZJG4EYL.js.map +1 -0
  26. package/dist/driver-paddle.cjs +817 -0
  27. package/dist/driver-paddle.cjs.map +1 -0
  28. package/dist/driver-paddle.d.cts +10 -0
  29. package/dist/driver-paddle.d.ts +10 -0
  30. package/dist/driver-paddle.js +814 -0
  31. package/dist/driver-paddle.js.map +1 -0
  32. package/dist/driver-stripe.cjs +2312 -0
  33. package/dist/driver-stripe.cjs.map +1 -0
  34. package/dist/driver-stripe.d.cts +24 -0
  35. package/dist/driver-stripe.d.ts +24 -0
  36. package/dist/driver-stripe.js +2305 -0
  37. package/dist/driver-stripe.js.map +1 -0
  38. package/dist/index.cjs +2677 -846
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +358 -1005
  41. package/dist/index.d.ts +358 -1005
  42. package/dist/index.js +2395 -698
  43. package/dist/index.js.map +1 -1
  44. package/dist/manifest-C2mDXWNU.d.cts +996 -0
  45. package/dist/manifest-C2mDXWNU.d.ts +996 -0
  46. package/dist/manifest-entry.cjs +408 -0
  47. package/dist/manifest-entry.cjs.map +1 -0
  48. package/dist/manifest-entry.d.cts +192 -0
  49. package/dist/manifest-entry.d.ts +192 -0
  50. package/dist/manifest-entry.js +270 -0
  51. package/dist/manifest-entry.js.map +1 -0
  52. package/dist/protocol.cjs +13 -0
  53. package/dist/protocol.cjs.map +1 -0
  54. package/dist/protocol.d.cts +182 -0
  55. package/dist/protocol.d.ts +182 -0
  56. package/dist/protocol.js +4 -0
  57. package/dist/protocol.js.map +1 -0
  58. package/package.json +49 -4
@@ -0,0 +1,996 @@
1
+ import { ComponentType } from 'react';
2
+
3
+ type VariableType = 'string' | 'number' | 'boolean' | 'stringArray';
4
+ type VariableValue = string | number | boolean | string[] | null | undefined;
5
+ interface VariableConfig {
6
+ type: VariableType;
7
+ default?: VariableValue;
8
+ persist?: boolean;
9
+ }
10
+
11
+ /**
12
+ * v2 `context` — the **read-only, computed** namespace.
13
+ *
14
+ * Everything the funnel can *read* but never *write*: the device, the visitor's
15
+ * locale, the current page, acquisition params (UTM), the session, and system
16
+ * facts (funnel id, test/live mode, clock). It supersedes the v0 flat
17
+ * `page.*`/`device.*`/`browser.*`/`os.*`/`query.*`/`metadata.*` system variables
18
+ * (see {@link ./systemVariables}) by folding them into one nested object.
19
+ *
20
+ * Why it's *not* in the {@link ./variableStore}: the store holds **writable**
21
+ * state (`user`/`responses`/`data`) that persists + re-renders per key. Context
22
+ * is derived and read-only, so it lives as a plain object on a React context —
23
+ * no subscriptions, no churn. The one moving part (`page`) is merged into the
24
+ * snapshot from navigation state at read time.
25
+ *
26
+ * Authors read it through hooks (`useDevice()`, `useLocale()`, `usePage()`,
27
+ * `useUtm()`) or inside routing/condition predicates as `s.context.*`.
28
+ */
29
+ interface DeviceContext {
30
+ type: 'mobile' | 'tablet' | 'desktop';
31
+ isMobile: boolean;
32
+ isTablet: boolean;
33
+ screenWidth: number;
34
+ screenHeight: number;
35
+ viewportWidth: number;
36
+ viewportHeight: number;
37
+ colorDepth: number;
38
+ pixelRatio: number;
39
+ }
40
+ interface BrowserContext {
41
+ name: string;
42
+ version: string;
43
+ userAgent: string;
44
+ language: string;
45
+ cookieEnabled: boolean;
46
+ online: boolean;
47
+ }
48
+ interface OsContext {
49
+ name: string;
50
+ timezone: string;
51
+ }
52
+ interface LocaleContext {
53
+ /** Full locale, e.g. `en-US`. */
54
+ locale: string;
55
+ /** Language subtag, e.g. `en`. */
56
+ language: string;
57
+ /** Region subtag, e.g. `US` (may be empty). */
58
+ region: string;
59
+ /** IANA timezone, e.g. `America/New_York`. */
60
+ timeZone: string;
61
+ }
62
+ interface PageContext {
63
+ /** Current page key. */
64
+ key: string;
65
+ /** Current page URL slug (`meta.slug`, defaults to the key) — what the renderer puts in the path. */
66
+ slug: string;
67
+ /** Current page's progress-bar group label (`meta.group`), or undefined when ungrouped. */
68
+ group?: string;
69
+ /** 0-based position in the visited history. */
70
+ index: number;
71
+ /** Total pages in the (expected) flow. */
72
+ total: number;
73
+ /** 0–100. */
74
+ progressPercentage: number;
75
+ /** Epoch ms the current page was entered. */
76
+ startedAt: number;
77
+ }
78
+ /** Acquisition / ad params captured on entry (the old `query.*` utm slice). */
79
+ interface UtmContext {
80
+ source?: string;
81
+ medium?: string;
82
+ campaign?: string;
83
+ content?: string;
84
+ term?: string;
85
+ [key: string]: string | undefined;
86
+ }
87
+ /**
88
+ * Per-network ad **click-ids** captured from the entry URL — the matching signals
89
+ * the server CAPI needs. Named per network (not a generic bag) so the platform +
90
+ * each integration map them precisely; `[key]` catches any others.
91
+ */
92
+ interface ClickIdContext {
93
+ /** Meta / Facebook */
94
+ fbclid?: string;
95
+ /** Google Ads (+ gbraid/wbraid for app↔web) */
96
+ gclid?: string;
97
+ gbraid?: string;
98
+ wbraid?: string;
99
+ /** Microsoft / Bing */
100
+ msclkid?: string;
101
+ /** TikTok */
102
+ ttclid?: string;
103
+ /** Twitter / X */
104
+ twclid?: string;
105
+ /** Snapchat */
106
+ sccid?: string;
107
+ /** LinkedIn */
108
+ li_fat_id?: string;
109
+ /** Pinterest */
110
+ epik?: string;
111
+ /** Reddit */
112
+ rdt_cid?: string;
113
+ [key: string]: string | undefined;
114
+ }
115
+ /**
116
+ * The full acquisition snapshot captured once on entry — the funnel reads `utm` /
117
+ * `clickIds` off {@link FunnelContext}; the SDK forwards this whole thing to the
118
+ * tracker (`setAcquisition`) so the platform persists it **first-class**: per-visit
119
+ * on the session + **first-touch (write-once) on the Customer**, and the click-ids /
120
+ * `_fbp`/`_fbc` cookies feed CAPI. Not stored as a generic attribute/answer.
121
+ */
122
+ interface Acquisition {
123
+ utm: UtmContext;
124
+ clickIds: ClickIdContext;
125
+ /** CAPI cookies (`_fbp`/`_fbc`) read from `document.cookie` (not funnel-readable). */
126
+ cookies: {
127
+ fbp?: string;
128
+ fbc?: string;
129
+ };
130
+ /** `document.referrer` at entry. */
131
+ referrer: string;
132
+ /** Entry URL path (the landing page). */
133
+ landingPath: string;
134
+ }
135
+ interface SessionContext {
136
+ /** Server session id; null until resolved. */
137
+ id: string | null;
138
+ /** Epoch ms the session began. */
139
+ startedAt: number;
140
+ /** `document.referrer` at entry. */
141
+ referrer: string;
142
+ }
143
+ /**
144
+ * Durable identity for **stable experiment bucketing** (phase-4 §4.2). The
145
+ * preferred seed is the signed anonymous `visitorId` (stable from the first
146
+ * pageload, matching the server-side FUNNEL split in resolve.ts); `customerId` is
147
+ * only the fallback for cookieless SDK embeds. visitorId-FIRST is deliberate — the
148
+ * anon customerId is minted server-side AFTER the first tracked event, so seeding
149
+ * on it would flip a returning visitor's arm between visit 1 and 2 (see
150
+ * {@link ../flow/experiments}.bucketingSeed). It is deliberately **not** the
151
+ * session id or fingerprint — a returning visitor (e.g. via winback) must keep
152
+ * their variant across sessions (landmine #4). Both null = unidentifiable traffic,
153
+ * and the experiment layer falls back to control rather than bucket on nothing.
154
+ */
155
+ interface IdentityContext {
156
+ /** Known customer id (post-identification). The fallback bucketing seed (cookieless embeds). */
157
+ customerId: string | null;
158
+ /** Signed anonymous visitor id. The preferred bucketing seed (stable from visit 1). */
159
+ visitorId: string | null;
160
+ }
161
+ interface SystemContext {
162
+ funnelId: string;
163
+ campaignId: string;
164
+ /** Render mode — `test` for preview/QA, `live` for public traffic. */
165
+ mode: 'test' | 'live';
166
+ /** Epoch ms captured at build time (stable per render, not a live clock). */
167
+ now: number;
168
+ }
169
+ /** The whole read-only computed namespace, as read in predicates: `s.context.*`. */
170
+ interface FunnelContext {
171
+ device: DeviceContext;
172
+ browser: BrowserContext;
173
+ os: OsContext;
174
+ locale: LocaleContext;
175
+ page: PageContext;
176
+ utm: UtmContext;
177
+ /** Per-network ad click-ids captured on entry (funnel-readable; also forwarded for CAPI). */
178
+ clickIds: ClickIdContext;
179
+ session: SessionContext;
180
+ identity: IdentityContext;
181
+ system: SystemContext;
182
+ }
183
+ /**
184
+ * Assemble the full {@link Acquisition} snapshot from the context + `document`
185
+ * (cookies, referrer, path) — captured once on entry, forwarded to the tracker for
186
+ * first-class persistence (session + first-touch Customer) + CAPI.
187
+ */
188
+ declare function buildAcquisition(context: FunnelContext): Acquisition;
189
+ interface BuildContextOptions {
190
+ funnelId?: string;
191
+ campaignId?: string;
192
+ mode?: 'test' | 'live';
193
+ /** Stable build-time clock (avoids `Date.now()` churn in renders/tests). */
194
+ now?: number;
195
+ sessionId?: string | null;
196
+ /** Known customer id — the preferred stable bucketing seed (phase-4 §4.2). */
197
+ customerId?: string | null;
198
+ /** Signed anonymous visitor id — the fallback bucketing seed. */
199
+ visitorId?: string | null;
200
+ /** Overrides for any slice — used by the server to hydrate `utm`/`session`. */
201
+ overrides?: Partial<FunnelContext>;
202
+ }
203
+ /**
204
+ * Build the read-only {@link FunnelContext} once, at mount. `page` starts at the
205
+ * first page and is replaced per navigation when assembling a snapshot.
206
+ */
207
+ declare function buildContext(opts?: BuildContextOptions): FunnelContext;
208
+
209
+ /**
210
+ * Pure catalog math — no React, no I/O. Split out of {@link ./catalog} so the
211
+ * server/tooling entry (`@appfunnel-dev/sdk/manifest`) can compile/resolve
212
+ * offerings without pulling react-dom or any component code into its graph.
213
+ * {@link ./catalog} re-exports everything here and adds the React wiring.
214
+ */
215
+ type Interval = 'day' | 'week' | 'month' | 'year' | 'one_time';
216
+ /** What the platform hands the funnel per offering (a single provider-resolved price). */
217
+ interface OfferingInput {
218
+ id: string;
219
+ /** The catalog charge KEY this input charges (the slot's assignment). Defaults to `id` (legacy/identity slots). */
220
+ catalogKey?: string;
221
+ name?: string;
222
+ displayName?: string;
223
+ /** Provider-resolved price in **minor units** (the provider chose the currency). */
224
+ amount: number;
225
+ /** Currency of `amount`, as the provider resolved it (e.g. `DKK`). */
226
+ currency: string;
227
+ interval?: Interval;
228
+ intervalCount?: number;
229
+ trialDays?: number;
230
+ /** Trial price in minor units (omit/0 = free trial). */
231
+ trialAmount?: number;
232
+ /** Settlement provider — opaque to the author, used by the checkout driver. */
233
+ provider?: string;
234
+ }
235
+ /** A money value with a locale-formatted string. */
236
+ interface Money {
237
+ /** Major units, e.g. `79`. */
238
+ amount: number;
239
+ /** Minor units, e.g. `7900`. */
240
+ minor: number;
241
+ currency: string;
242
+ /** Locale-formatted, e.g. `kr 79,00` / `€9,99` / `$9.99`. */
243
+ formatted: string;
244
+ }
245
+ /**
246
+ * The offering with its amounts resolved (currency + period math), **before**
247
+ * locale formatting — what the catalog stores. `useOffering` formats it.
248
+ */
249
+ interface ResolvedOffering {
250
+ id: string;
251
+ /** The catalog charge KEY charged (the CHARGE identity; `id` is the SLOT/display identity). */
252
+ catalogKey: string;
253
+ name: string;
254
+ displayName: string;
255
+ provider: string;
256
+ currency: string;
257
+ priceMinor: number;
258
+ perDayMinor: number;
259
+ perWeekMinor: number;
260
+ perMonthMinor: number;
261
+ perYearMinor: number;
262
+ period: string;
263
+ periodly: string;
264
+ hasTrial: boolean;
265
+ trialDays: number;
266
+ trialMinor: number;
267
+ }
268
+ /** Display-ready offering the author reads (every amount locale-formatted). */
269
+ interface Offering {
270
+ id: string;
271
+ /** The catalog charge KEY charged (the CHARGE identity; `id` is the SLOT/display identity). */
272
+ catalogKey: string;
273
+ name: string;
274
+ displayName: string;
275
+ provider: string;
276
+ price: Money;
277
+ /** e.g. `month`, `quarter`, `one-time`. */
278
+ period: string;
279
+ /** e.g. `monthly`, `quarterly`, `one-time`. */
280
+ periodly: string;
281
+ /** Period-normalized prices (for "just $0.27/day" style copy). */
282
+ perDay: Money;
283
+ perWeek: Money;
284
+ perMonth: Money;
285
+ perYear: Money;
286
+ hasTrial: boolean;
287
+ trialDays: number;
288
+ trialPrice: Money | null;
289
+ }
290
+ /**
291
+ * Resolve an {@link OfferingInput} into amounts + period math (no formatting yet).
292
+ * Per-period amounts are **integer minor units** (each derived from the exact
293
+ * daily rate, then rounded — never round-then-multiply, so error doesn't compound).
294
+ */
295
+ declare function resolveOffering(input: OfferingInput): ResolvedOffering;
296
+ /** Build the id → {@link ResolvedOffering} catalog. Formatting happens at read time. */
297
+ declare function buildCatalog(inputs: OfferingInput[]): Map<string, ResolvedOffering>;
298
+ /**
299
+ * The minor-unit exponent for a currency (`USD`→2, `JPY`→0, `KWD`/`BHD`→3),
300
+ * derived from `Intl.NumberFormat(...).resolvedOptions().maximumFractionDigits`
301
+ * with a fallback of 2 for unknown/invalid codes.
302
+ */
303
+ declare function currencyExponent(currency: string): number;
304
+ /** Format minor units of `currency` in `locale` (exponent-aware: 7900 JPY = ¥7,900). */
305
+ declare function formatMoney(minor: number, currency: string, locale: string): Money;
306
+ /** Format a {@link ResolvedOffering} into a display {@link Offering} in `locale`. */
307
+ declare function formatOffering(r: ResolvedOffering, locale: string): Offering;
308
+
309
+ /**
310
+ * The funnel **spine** — the constrained, declarative configuration authored as
311
+ * code (`defineFunnel` / `pageMeta` / `definePage`) yet **dashboard-editable**
312
+ * (static AST parse → pretty UI → codegen) and compiled to the manifest IR.
313
+ *
314
+ * Principle: **presentation = code** (the page components), **configuration =
315
+ * data** (this spine). See docs/platform-v2/06-funnel-project-model.md.
316
+ *
317
+ * Routing is **declarative nodes + code predicates** (decided — doc 06): a
318
+ * page's `next` is an ordered list of {@link Route}s, each with a **static `to`
319
+ * page key** (so the dashboard can draw every possible edge without executing
320
+ * anything) and an optional `when` **gate** that returns true/false. The gate is
321
+ * either a declarative {@link Condition} (one field — UI-editable data) or a code
322
+ * {@link Predicate} (`s => boolean` — the escape hatch, opaque body but the edge
323
+ * is still visible). First matching route wins; a route with no `when` is the
324
+ * fallback. Multiple conditions = multiple routes (or boolean logic in one
325
+ * predicate). Omit `next` entirely for the default linear flow.
326
+ */
327
+ /**
328
+ * Read-only view of every namespace a routing/condition predicate can branch
329
+ * on. `user`/`responses`/`data` are the writable namespaces; `context` is the
330
+ * read-only computed one ({@link FunnelContext}).
331
+ */
332
+ interface FunnelSnapshot {
333
+ user: Record<string, VariableValue>;
334
+ responses: Record<string, VariableValue>;
335
+ data: Record<string, VariableValue>;
336
+ context: FunnelContext;
337
+ }
338
+ /** A code-predicate gate — `s => s.responses.goal === 'gain'`. */
339
+ type Predicate = (s: FunnelSnapshot) => boolean;
340
+ type ConditionOp = 'eq' | 'neq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte' | 'exists' | 'empty' | 'contains';
341
+ /**
342
+ * A declarative gate over **one** namespaced field — UI-editable data the
343
+ * dashboard renders as a condition row (`field` `op` `value`). `field` is a
344
+ * dotted path into the snapshot: `responses.goal`, `user.country`,
345
+ * `context.device.isMobile`.
346
+ */
347
+ interface Condition {
348
+ field: string;
349
+ op: ConditionOp;
350
+ value?: VariableValue | VariableValue[];
351
+ }
352
+ /** An edge gate: either declarative data ({@link Condition}) or a {@link Predicate}. */
353
+ type Gate = Condition | Predicate;
354
+ /**
355
+ * One routing edge out of a page. `to` is a **static** target key (the dashboard
356
+ * reads these to draw the flow graph); `when` gates the edge (omit = fallback).
357
+ */
358
+ interface Route {
359
+ to: string;
360
+ when?: Gate;
361
+ }
362
+ /** Evaluate a declarative {@link Condition} against a snapshot. */
363
+ declare function evaluateCondition(cond: Condition, s: FunnelSnapshot): boolean;
364
+ /** Evaluate either gate kind to a boolean. */
365
+ declare function evaluateGate(gate: Gate, s: FunnelSnapshot): boolean;
366
+ /**
367
+ * Resolve an ordered list of routes to a target page key, or `undefined` to fall
368
+ * through to the linear next. First route whose gate passes (or has no `when`)
369
+ * wins.
370
+ */
371
+ declare function resolveRoute(routes: Route[] | undefined, s: FunnelSnapshot): string | undefined;
372
+ type PageType = 'default' | 'email-capture' | 'paywall' | 'upsell' | 'finish';
373
+ interface PageMeta {
374
+ /** Page kind — drives behavior + analytics (e.g. `paywall`, `upsell`). */
375
+ type?: PageType;
376
+ /** URL slug; defaults to the page key. */
377
+ slug?: string;
378
+ /**
379
+ * Progress-bar GROUP key — a purely declarative membership tag for a **grouped
380
+ * progress bar**. Pages sharing a `group` are one segment; a page with no `group`
381
+ * belongs to no segment. It's inert for routing and analytics —
382
+ * {@link groupProgress} / `useGroupProgress()` read it to build the visitor-routed,
383
+ * branch-aware segment structure a `<ProgressBar>` consumes.
384
+ *
385
+ * CONVENTION: this is a **stable slug identifier**, not a display string — e.g.
386
+ * `'profile'`, `'activity'`, `'nutrition'`, not `'My Profile'`. Recommended charset
387
+ * matches page keys (`[A-Za-z0-9][A-Za-z0-9._-]*`). Keep it stable across copy and
388
+ * locale changes so grouping doesn't shift when wording does. The human-readable
389
+ * **label belongs in the funnel's own code**, mapped from this key — a plain map
390
+ * (`const GROUP_LABELS = { profile: 'My Profile' }`) or a translation lookup
391
+ * (`t('groups.profile')`). Like page keys, this is a tooling convention, not a
392
+ * runtime-enforced rule — the runtime treats any string as an opaque key.
393
+ */
394
+ group?: string;
395
+ /**
396
+ * Ordered routing edges out of this page (first match wins). Each has a static
397
+ * `to` key + an optional `when` gate. Omit for the default linear flow.
398
+ *
399
+ * OPTIONAL per-variant routing: a `@variant` page (`paywall@b`) MAY carry its own
400
+ * `next`. When that variant is the one served for its slot, its `next` governs
401
+ * routing for that visitor; absent, the variant inherits the slot's routing (the
402
+ * default — a pure superset, no behavior change for existing funnels). Assignment,
403
+ * exposure, and progress still anchor on the slot; only the next-page lookup
404
+ * consults the served variant's routes. See {@link FunnelPage}.
405
+ */
406
+ next?: Route[];
407
+ /**
408
+ * Entry precondition for **deep-link / reload** restoration. When the runtime is
409
+ * asked to start *on this page* (not via in-funnel navigation), it restores there
410
+ * only if `guard` passes against the restored snapshot — otherwise it falls back
411
+ * to the start page. e.g. an upsell page: `guard: s => s.data.purchased === true`.
412
+ * Reaching the page via `next()` is unaffected (routing already gated it). Omit =
413
+ * always restorable. Same gate kind as routing (`Condition` or predicate).
414
+ */
415
+ guard?: Gate;
416
+ /**
417
+ * BUILD-TIME PRERENDER OPT-OUT (SSR-isolation Option B, FIX 2). Set `dynamic: true` (alias
418
+ * `prerender: false`) on a page whose **structure** (which sections exist — not just their text
419
+ * values) depends on the visitor's `sessionValues`. Such a page can't be prerendered flash-free:
420
+ * the default-state markup would structurally differ from the value-applied render and reflow
421
+ * after hydration. Opting out makes the build SKIP prerendering it, so the renderer serves it via
422
+ * live SSR (the same fallback branch a build with no prerender entry already uses) — correct
423
+ * first paint, no pop-in. Value-only variation (text/price into a fixed structure) does NOT need
424
+ * this: deferred defaults make it flash-free. Omit = prerendered (the default).
425
+ */
426
+ dynamic?: boolean;
427
+ /** @see {@link PageMeta.dynamic} — `prerender: false` is the inverse alias (author preference). */
428
+ prerender?: boolean;
429
+ }
430
+ /**
431
+ * Identity helper for a page's co-located metadata.
432
+ * `export const meta = pageMeta({ type: 'paywall', next: s => … })`.
433
+ */
434
+ declare function pageMeta(meta: PageMeta): PageMeta;
435
+ /**
436
+ * The effective entry guard for a page: an explicit `meta.guard` if the author set
437
+ * one, **otherwise** the page-type default (e.g. paywall/upsell need email). The
438
+ * explicit guard fully **overrides** the default — it's not combined. Returns
439
+ * `undefined` when neither applies.
440
+ */
441
+ declare function entryGuard(meta?: PageMeta): Gate | undefined;
442
+ /**
443
+ * Identity helper for a page component. The page is plain React that reaches the
444
+ * runtime through hooks (`useResponse`, `useNavigation`, …) — there is no `sdk`
445
+ * prop. `export default definePage(function Welcome() { … })`.
446
+ */
447
+ declare function definePage<P extends object = Record<string, never>>(component: ComponentType<P>): ComponentType<P>;
448
+ /** A page in the flow: its key + (optional) co-located meta. */
449
+ interface FlowPage {
450
+ key: string;
451
+ meta?: PageMeta;
452
+ }
453
+ /**
454
+ * A page as declared in `funnel.ts` — the key + its metadata, flattened. This is the
455
+ * AUTHORED shape (funnel.ts owns the page list, order, and routing); the build generates
456
+ * the code-split `mount.tsx` from it. Routing (`next`) must stay DECLARATIVE (Route[] with
457
+ * `{ to, when }` conditions, no predicate functions) so the web builder UI can edit it.
458
+ *
459
+ * OPTIONAL variant routing (default off): a served variant's OWN routing is authored by
460
+ * declaring the variant here with its `@` key and a `next`, e.g.
461
+ * `{ key: 'paywall@b', next: [{ to: 'special-offer' }] }`. A variant declared this way keys
462
+ * on the `@` marker, so it still collapses out of the linear flow (it is NOT a linear step);
463
+ * its `next` is consulted only when that variant is the one served for its slot. Omit the
464
+ * declaration (leave the variant an undeclared `pages/<slot>@<v>.tsx` file) to inherit the
465
+ * slot's routing — the existing behavior, unchanged. The slot's control keeps owning
466
+ * assignment/exposure/progress; only the next-page lookup for a branched variant differs.
467
+ */
468
+ interface FunnelPage extends PageMeta {
469
+ key: string;
470
+ }
471
+ /**
472
+ * Decide the next page from `currentKey`:
473
+ * 1. the current page's `next` routes ({@link resolveRoute}), if one matches, else
474
+ * 2. the next page in file order (the default linear flow), else
475
+ * 3. `undefined` (end of funnel / current key not found).
476
+ */
477
+ declare function nextPage(pages: FlowPage[], currentKey: string, s: FunnelSnapshot): string | undefined;
478
+ /**
479
+ * Every page a visitor *could* go to next from `currentKey` — all route targets
480
+ * (regardless of gate, since we don't know which will pass) plus the linear next.
481
+ * Used to **prefetch** the likely-next page chunks; not for navigation.
482
+ */
483
+ declare function outgoingKeys(pages: FlowPage[], currentKey: string): string[];
484
+ /**
485
+ * The number of pages on the path from `startKey` to a `finish` (or the end of
486
+ * the flow) **given the current state** — the denominator for progress. It walks
487
+ * the actual routing ({@link nextPage}) rather than counting all files, so a
488
+ * branched funnel reaches 100% on whichever path the visitor's answers select.
489
+ * Re-traced per navigation; a visited set guards against routing cycles.
490
+ */
491
+ declare function expectedPathLength(pages: FlowPage[], startKey: string, s: FunnelSnapshot): number;
492
+ /** One segment of a grouped progress bar (a distinct {@link PageMeta.group}). */
493
+ interface PageGroup {
494
+ /** The group's declarative slug key (`meta.group`) — resolve to a display label in funnel code. */
495
+ key: string;
496
+ /** Pages carrying this label on the visitor's ROUTED path (branch-aware count). */
497
+ pageCount: number;
498
+ /**
499
+ * Pages of this group the visitor has already left behind — strictly before the
500
+ * current page. A group entirely in the past has `completedCount === pageCount`
501
+ * (100%); an upcoming group has `0`; the active group's is its within-group cursor.
502
+ */
503
+ completedCount: number;
504
+ /** True when the current page belongs to this group. */
505
+ active: boolean;
506
+ }
507
+ /**
508
+ * The visitor-routed, branch-aware structure a grouped `<ProgressBar>` renders. See
509
+ * {@link groupProgress}. `groups` are ordered by first appearance along the routed
510
+ * path; `activeIndex` locates the current group (or `-1` when the current page is
511
+ * ungrouped); `activeFraction` is the active group's within-group fill (0–1).
512
+ */
513
+ interface GroupProgress {
514
+ groups: PageGroup[];
515
+ activeIndex: number;
516
+ activeFraction: number;
517
+ }
518
+ /**
519
+ * Build the grouped-progress structure for the CURRENT visitor by walking the actual
520
+ * ROUTED path from `startKey` (mirrors {@link expectedPathLength}: first-match routes
521
+ * against the live snapshot, a visited-guard against cycles, stopping at a `finish`)
522
+ * — NOT the static file order. A visitor on the muscle branch therefore gets the
523
+ * muscle-variant pages counted in their groups, not the lose-variant ones.
524
+ *
525
+ * Rules (documented decisions):
526
+ * - **Distinct by key, first-appearance order.** Groups key on the slug; a key is
527
+ * listed once, positioned by where it first appears. A later re-appearance of the
528
+ * same key folds back into that one group (its pages add to the same count) — it
529
+ * does NOT open a second segment.
530
+ * - **Ungrouped pages don't break contiguity.** A page with no `group` interleaved
531
+ * inside a group's span belongs to no segment (counted in nothing), but neither
532
+ * does it split the surrounding group — because grouping is by key, not by runs.
533
+ * - **Completion by position.** `completedCount` counts a group's pages strictly
534
+ * before the current page's index in the routed path; the current page is the
535
+ * in-progress step (credited via `activeFraction`, not `completedCount`).
536
+ * - **Current page ungrouped ⇒ `activeIndex = -1`, `activeFraction = 0`** (other
537
+ * groups still report their completion relative to the cursor).
538
+ * - **Loops / back-navigation.** Position is the current page's index in the routed
539
+ * path (`indexOf`), so going back rewinds completion. An off-path jump (current key
540
+ * not on the projected path) falls back to measuring from the path start.
541
+ */
542
+ declare function groupProgress(pages: FlowPage[], startKey: string, currentKey: string, s: FunnelSnapshot): GroupProgress;
543
+ interface FunnelLocales {
544
+ default: string;
545
+ supported: string[];
546
+ fallback?: string;
547
+ /**
548
+ * When `true`, a visitor with no explicit language in the URL (no `/​<locale>/` path prefix and no
549
+ * `?language=`) is served the best match for their **browser** `Accept-Language` among `supported`
550
+ * (base-matched, so `en-GB` → `en`); no match falls back to `default`. When `false`/absent (the
551
+ * default), an unspecified visitor always gets `default` — the URL is the only way to switch.
552
+ */
553
+ autoDetectLanguage?: boolean;
554
+ }
555
+ /**
556
+ * The funnel-level spine. Flow is mostly inferred from page file order + each
557
+ * page's `next`; this holds the funnel-wide config. Stays **thin** — routing
558
+ * lives co-located on pages (doc 06: "funnel.ts stays thin").
559
+ */
560
+ interface FunnelDefinition {
561
+ id: string;
562
+ /**
563
+ * The funnel's pages — order, type, and declarative routing — the source of truth the
564
+ * web builder edits and the build generates `mount.tsx` from. Each `key` maps to a
565
+ * `pages/<key>.tsx` component. Omit `pages` on hand-written funnels that still ship their
566
+ * own `mount.tsx` (back-compat); new/generated funnels declare them here.
567
+ */
568
+ pages?: FunnelPage[];
569
+ /** Which checkout provider the paywall/checkout uses. The build wires the driver. */
570
+ checkout?: 'stripe' | 'paddle';
571
+ /** `responses.*` runtime variables (default values). */
572
+ responses?: Record<string, VariableConfig>;
573
+ /** `data.*` working/scratch variables (default values). */
574
+ data?: Record<string, VariableConfig>;
575
+ /**
576
+ * The catalog offerings this funnel sells, as SLOTS: a local, stable key each PAGE
577
+ * references (`useOffering('primary')`, `<Checkout offering="primary">`) → the catalog charge
578
+ * KEY it's assigned to. Swap the assignment to change the offer WITHOUT touching page code;
579
+ * `null` = a declared-but-unassigned slot. A bare `string[]` is legacy sugar for IDENTITY
580
+ * slots (`['pro'] ≡ { pro: 'pro' }`) — every existing funnel keeps working. Prices resolve
581
+ * from the project catalog per environment (Live/Test) at render.
582
+ */
583
+ offerings?: string[] | Record<string, string | null>;
584
+ locales?: FunnelLocales;
585
+ /**
586
+ * When `true`, the funnel shows the **geo-specific** currency/price the payment
587
+ * provider resolves (Stripe `currency_options`, Paddle preview, Whop, SolidGate,
588
+ * …). When `false` (default), a single fixed currency is used. Appfunnel does no
589
+ * FX — this only toggles whether the platform asks the provider for the geo
590
+ * price; the SDK just displays whatever resolved `{amount, currency}` it's handed.
591
+ * (See docs/platform-v2 phase-3 §3.6.)
592
+ */
593
+ locationAwareCurrency?: boolean;
594
+ }
595
+ /** Identity helper for a funnel definition. */
596
+ declare function defineFunnel(def: FunnelDefinition): FunnelDefinition;
597
+ /** A funnel offering SLOT: the local key pages reference → the assigned catalog charge key (null = unassigned). */
598
+ interface OfferingSlot {
599
+ slot: string;
600
+ catalogKey: string | null;
601
+ }
602
+ /**
603
+ * Normalize `FunnelDefinition.offerings` to a slot list. A legacy `string[]` becomes IDENTITY
604
+ * slots (`['pro'] → [{ slot:'pro', catalogKey:'pro' }]`); a `{ slot: key|null }` map becomes its
605
+ * entries (insertion order preserved, so `useOfferings()` is deterministic).
606
+ */
607
+ declare function normalizeOfferings(offerings: FunnelDefinition['offerings']): OfferingSlot[];
608
+ /** The unique, ASSIGNED catalog keys a funnel charges — for the promote gate + catalog reverse-lookup. */
609
+ declare function funnelCatalogKeys(offerings: FunnelDefinition['offerings']): string[];
610
+
611
+ /**
612
+ * Pure locale resolution — no React. Split out of {@link ./translation} so the
613
+ * server/tooling entry (`@appfunnel-dev/sdk/manifest`) can resolve a visitor's
614
+ * locale without pulling component code. {@link ./translation} re-exports it.
615
+ */
616
+
617
+ type Direction = 'ltr' | 'rtl';
618
+ /** Whether a locale is right-to-left (by language subtag). */
619
+ declare function isRtl(locale: string): boolean;
620
+ /**
621
+ * Resolve the active locale: `override` (URL/param) → `detected` (device) →
622
+ * funnel default, each only if supported (matched by exact tag or language
623
+ * subtag). Mirrors doc 05's resolution chain (the geo/header steps happen
624
+ * server-side and arrive via `override`/`detected`).
625
+ */
626
+ declare function resolveLocale(config: FunnelLocales | undefined, detected: string, override?: string): string;
627
+
628
+ /**
629
+ * **Runtime page-level experiments** — A/B/n on a *single page inside one funnel*,
630
+ * resolved at render time (phase-4). No funnel duplication: a slot (e.g. the page
631
+ * `welcome`) has alternate versions filed as siblings (`welcome@b.tsx`), and the
632
+ * runtime shows **one** of them per visitor, collapsing the losers out of the
633
+ * linear flow. The flow keeps stable **slot keys** (the control's key); only the
634
+ * rendered component swaps — so routing, the manifest, and analytics all key on the
635
+ * slot, not the variant.
636
+ *
637
+ * **Where the wiring lives:** the *variant pages* are code (compiled into the
638
+ * bundle). The *experiment wiring* — slot, variant→page map, weights, status — is
639
+ * **operational data the platform owns** (a DB record, edited by the dashboard,
640
+ * changed without recompiling the funnel). The SDK is storage-agnostic: the
641
+ * platform hands the runtime experiment records to `FunnelProvider`, the same way
642
+ * it hands in resolved product prices. This module just resolves them.
643
+ *
644
+ * **The only source-side marker** is the `@` in a variant file's key
645
+ * (`welcome@b`): it tells the *build* (manifest, flow graph) that a page is an
646
+ * off-flow variant of its slot, so the structure is correct without the DB. The
647
+ * label/weight/experiment-id are not in source.
648
+ *
649
+ * Bucketing is a deterministic FNV-1a hash, **namespaced by experiment id** so
650
+ * independent experiments bucket orthogonally (the basis for layering 100+ tests,
651
+ * §4.3). The seed is the durable identity ({@link bucketingSeed}) — never the
652
+ * session/fingerprint — so a returning visitor keeps their variant (landmine #4).
653
+ * Stability across an *add-a-variant* edit is the platform's job: an experiment's
654
+ * variant set is immutable while running (reweighting / adding an arm = a new
655
+ * experiment version), so within one record the assignment never moves.
656
+ *
657
+ * This module is pure (no React, no I/O) → it unit-tests trivially and the
658
+ * dashboard/server can reuse the same assignment math.
659
+ */
660
+
661
+ /**
662
+ * FNV-1a 32-bit hash → unsigned int. Deterministic and isomorphic (no `crypto`,
663
+ * runs the same in the browser, a worker, and a build). The same math v0 used,
664
+ * kept because it's sound — only the *seed* changes (identity, not session).
665
+ */
666
+ declare function fnv1a(input: string): number;
667
+ /** Map a seed string to a stable float in `[0, 1)`. */
668
+ declare function hashToUnit(seed: string): number;
669
+ /**
670
+ * Pick a variant key by weight from a stable unit position `u ∈ [0,1)`. Variants
671
+ * are walked in declaration order and the first whose cumulative weight band
672
+ * contains `u` wins, so the same `u` always lands in the same variant.
673
+ */
674
+ declare function pickByWeight(weights: Record<string, number>, u: number): string;
675
+ /**
676
+ * Deterministically assign a visitor (`seed`) to a variant of one experiment.
677
+ * The hash is **namespaced by `experimentId`** so two experiments on the same
678
+ * traffic don't correlate (orthogonal layering).
679
+ */
680
+ declare function assignVariant(experimentId: string, seed: string, weights: Record<string, number>): string;
681
+ /**
682
+ * The stable bucketing seed: the signed `visitorId`, else the known `customerId`.
683
+ * Never the session id or fingerprint (landmine #4). `null` when neither is known
684
+ * — the caller then serves control instead of bucketing on nothing.
685
+ *
686
+ * visitorId-FIRST (not customerId-first) is deliberate: the customerId injected for an
687
+ * anonymous visitor is minted server-side AFTER the first tracked event, so it is absent on
688
+ * visit 1 and present on visit 2 — seeding on it would flip a returning visitor's arm and
689
+ * count them in two arms of the same version. The signed `af_vid` cookie is the durable
690
+ * identity that's stable from the first pageload, so it seeds; customerId is only the fallback
691
+ * for cookieless SDK embeds. This matches the server-side FUNNEL split, which already seeds on
692
+ * the verified visitorId (resolve.ts). (Cross-device stable bucketing for IDENTIFIED customers
693
+ * is P3's IdentitySDK job, not this anonymous path.)
694
+ */
695
+ declare function bucketingSeed(context: FunnelContext): string | null;
696
+ /** Split a page key into its slot + optional variant: `welcome@b` → `{ slot, variant: 'b' }`. */
697
+ declare function parseSlotKey(key: string): {
698
+ slot: string;
699
+ variant?: string;
700
+ };
701
+ /** True if a page key is an off-flow variant (`welcome@b`), by the source-side `@` marker. */
702
+ declare function isVariantKey(key: string): boolean;
703
+ /** One arm of a running experiment: which page renders, at what weight. */
704
+ interface ExperimentArm {
705
+ /** The page key this arm renders (the slot's control, or a `@variant` sibling). */
706
+ page: string;
707
+ /** Relative weight in the split (need not sum to 100; zero = paused arm). */
708
+ weight: number;
709
+ }
710
+ /**
711
+ * A runtime experiment record — the operational wiring the **platform** owns and
712
+ * hands to the SDK (not authored in the funnel source). Projected from the unified
713
+ * `Experiment`/`ExperimentArm` graph (a RUNNING PAGE experiment + its arms): the
714
+ * experiment's `slot` names the base page, and each arm becomes a `variants` entry
715
+ * `{ page: arm.pageKey, weight: arm.weight }` keyed by `arm.label`. The variant
716
+ * *pages* are code in the LIVE build; this is the live split over them.
717
+ *
718
+ * The serve join only ever hands over RUNNING experiments, and the server's
719
+ * draft-write + serve guards make every active arm's page GUARANTEED present in the
720
+ * LIVE build (an experiment owns its arm pages' lifecycle — you end the experiment to
721
+ * remove a page, you can't drift it away). So there is no lifecycle/winner state and
722
+ * no "arm page missing" case to resolve here.
723
+ */
724
+ interface RuntimeExperiment {
725
+ id: string;
726
+ /** The page key holding the flow position (the anchor / the control arm's base page). */
727
+ slot: string;
728
+ /** label → arm. The control arm's `page` is the `slot`; others are `@variant` siblings. */
729
+ variants: Record<string, ExperimentArm>;
730
+ /** Primary metric (analytics only; not used for resolution). */
731
+ metric?: string;
732
+ /** Platform record version (immutable-while-running; reweight = new version). Rides on
733
+ * exposure events so results key on (experimentId, version, variant). */
734
+ version?: number;
735
+ }
736
+ interface ExperimentResolution {
737
+ /** experiment id → assigned variant label. */
738
+ assignments: Record<string, string>;
739
+ /** Page keys forming the effective linear flow (variant siblings removed), in order. */
740
+ activeKeys: string[];
741
+ /** Slot key → the page key whose component should render there. */
742
+ render: Record<string, string>;
743
+ /** Variant page key → its slot key. For remapping a route that targets a variant. */
744
+ slotOf: Record<string, string>;
745
+ /** Slot key → the experiment id it hosts (drives the exposure event). */
746
+ experimentOf: Record<string, string>;
747
+ /** Experiment id → platform record version (rides on exposure events). */
748
+ versionOf: Record<string, number>;
749
+ }
750
+ /**
751
+ * Weights map (label → weight) for one experiment's arms. Exported (via the pure
752
+ * manifest entry) so the server-side FUNNEL split (renderer resolve) projects arms
753
+ * the same way before calling {@link assignVariant}, instead of re-inlining it.
754
+ */
755
+ declare function weightsOf(variants: Record<string, {
756
+ weight: number;
757
+ }>): Record<string, number>;
758
+ /**
759
+ * Resolve which page version each experiment slot shows for this visitor, and which
760
+ * page keys remain in the linear flow.
761
+ *
762
+ * Variant pages are detected structurally by the `@` in their key, so they collapse
763
+ * out of the linear flow even for a slot with no active experiment. Each served
764
+ * experiment is RUNNING with all its arm pages guaranteed present in the LIVE build
765
+ * (the server draft-write + serve guards enforce the experiment-owned lifecycle
766
+ * invariant), so the visitor is bucketed deterministically on `seed` and the assigned
767
+ * arm's page is swapped in with no presence check. With no `seed` there is nothing
768
+ * stable to bucket on, so every slot renders its own control page.
769
+ */
770
+ declare function resolveExperiments(pages: {
771
+ key: string;
772
+ }[], experiments: RuntimeExperiment[], seed: string | null): ExperimentResolution;
773
+
774
+ /**
775
+ * Pure, React-free page scan: find which offering SLOTS a funnel's pages reference — the first
776
+ * string-literal arg of `useOffering(...)` and the `offering` prop of `<Checkout>` / `<Upsell>`.
777
+ * Ships via `@appfunnel-dev/sdk/manifest` so the BUILD, the EDITOR, and the AI all run the SAME
778
+ * check. The result feeds `compileManifest({ offeringRefs })`, which flags references to slots the
779
+ * funnel doesn't declare (unknown) or hasn't assigned a catalog charge key to (unassigned).
780
+ *
781
+ * Non-literal args (`useOffering(selectedId)`, `offering={expr}`) and the `useOfferings()` wildcard are
782
+ * DYNAMIC — they can't be statically enumerated, so they're reported via `dynamic` and never turned
783
+ * into a hard error (validation relaxes when a funnel is dynamic). Mirrors the mask-then-read
784
+ * discipline the funnel.ts code-mods use, so a `useOffering('x')` inside a comment or string literal
785
+ * can't false-positive.
786
+ */
787
+ interface OfferingRefScan {
788
+ /** Concrete slot names referenced by LITERAL args, deduped, in first-seen order. */
789
+ slots: string[];
790
+ /** True if any ref is a non-literal arg or a `useOfferings()` wildcard (not statically enumerable). */
791
+ dynamic: boolean;
792
+ /**
793
+ * Checkout-SURFACE usage the pages express — feeds the publish (provider × surface) capability
794
+ * gate. Absent-field back-compat: a manifest compiled before this existed simply omits it, and
795
+ * the gate skips (no false blocks). See {@link CheckoutUsageScan}.
796
+ */
797
+ checkout: CheckoutUsageScan;
798
+ }
799
+ /**
800
+ * What the pages ask the checkout to DO, statically — the surfaces they mount and the off-session
801
+ * upsell kinds they charge. Literal values only; a computed value (`surface={expr}`) sets
802
+ * {@link dynamic} so the publish gate WARNS instead of hard-failing (it can't enumerate it).
803
+ */
804
+ interface CheckoutUsageScan {
805
+ /**
806
+ * Purchase SURFACES the pages request, deduped in first-seen order. Captures every literal
807
+ * `<Checkout surface="…">` plus the on-session re-collect surfaces `<Checkout|Upsell fallback="…">`
808
+ * and `recoverySurface="…"` (a fallback re-collects a card → it IS a purchase surface the provider
809
+ * must support). Validated with `validateCheckout(provider, surface)`.
810
+ */
811
+ surfaces: string[];
812
+ /**
813
+ * Off-session upsell KINDS from `<Upsell kind="…">`, deduped. A `<Upsell>` with no `kind` attr
814
+ * defaults to `'subscription'` (mirrors the runtime default), so an unadorned `<Upsell>` records
815
+ * `'subscription'`. Validated with `validateUpsell(provider, paywallSurface, kind)`.
816
+ */
817
+ upsellKinds: string[];
818
+ /** True if any surface/kind was a non-literal (computed) value → the gate WARNS, never blocks, on it. */
819
+ dynamic: boolean;
820
+ }
821
+ declare function scanOfferingRefs(files: Record<string, string>): OfferingRefScan;
822
+
823
+ /**
824
+ * The **manifest** — the declarative IR a funnel project compiles to (doc 06).
825
+ * It's the machine-readable artifact the platform operates on without executing
826
+ * the funnel: the dashboard draws the flow graph from it, analytics binds to its
827
+ * **stable ids**, validation runs against it, and the renderer reads it to resolve
828
+ * routing.
829
+ *
830
+ * `compileManifest` evaluates the spine (the funnel config + each page's co-located
831
+ * `meta`) into that IR. It's pure — no I/O — so it unit-tests trivially. **It runs
832
+ * server-side**, as the IR step of the publish build over the canonical files in our
833
+ * storage (R2). The client/CLI never produces the authoritative manifest — R2 is the
834
+ * source of truth, the server compiles. (A local dev preview may run it for itself,
835
+ * but that output is never published.)
836
+ *
837
+ * Edge model mirrors the runtime ({@link ../flow/spine}.`nextPage`): a page's
838
+ * `meta.next` routes become **branch** edges (with a serialized condition — a
839
+ * declarative {@link Condition}, or an opaque marker for a code predicate), and the
840
+ * implicit file-order fall-through becomes a **linear** edge (unless an
841
+ * unconditional route already covers it, or the page is a `finish`).
842
+ *
843
+ * Experiments are **not** compiled here. A variant page is recognised structurally
844
+ * by the `@` in its key (`welcome@b`) and tagged `variantOf` + collapsed out of the
845
+ * linear flow; the experiment *wiring* (labels/weights/status) is operational data
846
+ * the platform owns and overlays at render time, not part of the funnel build.
847
+ *
848
+ * Stable ids: the page **key** is the source anchor, so `id === key` here. The
849
+ * durable analytics id is the semantic page name (a slot survives a winner being
850
+ * promoted into it); transient `@variant` pages only ever key analytics by
851
+ * `(experimentId, variant)` in immutable results — a platform concern.
852
+ */
853
+
854
+ interface ManifestPage {
855
+ id: string;
856
+ key: string;
857
+ type: PageType;
858
+ slug: string;
859
+ index: number;
860
+ /**
861
+ * The page's progress-bar GROUP label ({@link PageMeta.group}), carried through additively so future
862
+ * editor/analytics surfaces can read grouped-progress membership without executing the funnel. Absent
863
+ * when the page declares no `group`. Inert for routing/validation.
864
+ */
865
+ group?: string;
866
+ /**
867
+ * Set on a variant page (`welcome@b`) to its **slot** (`welcome`) — a structural
868
+ * marker that this node is an off-flow alternate, not a linear step. The
869
+ * experiment *wiring* (label/weight/id) is operational data the platform owns,
870
+ * not the manifest.
871
+ */
872
+ variantOf?: string;
873
+ /**
874
+ * The page's effective ENTRY guard, serialized (SSR-isolation Option B, FIX 1): a `declarative`
875
+ * {@link Condition} the renderer can evaluate SERVER-SIDE against the visitor's session snapshot
876
+ * to pick the right prerendered variant (target vs bounce) without running tenant code, or an
877
+ * opaque `predicate` marker (author code — the renderer must fall back to live SSR for that
878
+ * deep-link). Absent = no entry guard (always restorable). Mirrors {@link entryGuard}.
879
+ */
880
+ entryGuard?: EdgeCondition;
881
+ /**
882
+ * BUILD-TIME PRERENDER OPT-OUT (SSR-isolation Option B, FIX 2). True when the page's `meta.dynamic`
883
+ * (or `prerender: false`) opts it out of prerendering because its STRUCTURE depends on
884
+ * sessionValues. The prerender stage skips it; the renderer serves it via live SSR.
885
+ */
886
+ dynamic?: boolean;
887
+ }
888
+ type EdgeCondition = {
889
+ kind: 'declarative';
890
+ condition: Condition;
891
+ }
892
+ /** A code predicate — opaque body; `label` may be supplied later by AST parsing. */
893
+ | {
894
+ kind: 'predicate';
895
+ label?: string;
896
+ };
897
+ interface ManifestEdge {
898
+ from: string;
899
+ to: string;
900
+ kind: 'branch' | 'linear';
901
+ /** Absent = unconditional (a fallback route, or the linear fall-through). */
902
+ condition?: EdgeCondition;
903
+ /**
904
+ * True on a **variant-only branch**: an edge whose `from` is a `@variant` key,
905
+ * emitted when that served variant carries its OWN `meta.next` (optional divergent
906
+ * routing). Lets consumers (the editor's Flow canvas) tell a variant's private branch
907
+ * apart from a normal slot edge. Optional/absent on every non-variant edge.
908
+ */
909
+ variant?: true;
910
+ }
911
+ interface ManifestValidation {
912
+ /**
913
+ * `ok` = none of the hard errors: the original set (dead ends, unreachable
914
+ * pages, bad targets, missing email capture) AND the structural errors added
915
+ * later (duplicate keys, orphan variants, no reachable finish). Slug
916
+ * collisions are a **warning** — reported but not folded into `ok` (slugs are
917
+ * display/URL sugar; keys are the routing identity).
918
+ */
919
+ ok: boolean;
920
+ /** Page ids you can't proceed from (and that aren't a `finish`). */
921
+ deadEnds: string[];
922
+ /** Page ids not reachable from the start. */
923
+ unreachable: string[];
924
+ /** Routes whose target page key doesn't exist. */
925
+ badTargets: {
926
+ from: string;
927
+ to: string;
928
+ }[];
929
+ /** True if the funnel never writes `user.email` (the mandatory identity step). */
930
+ missingEmailCapture: boolean;
931
+ /** Page keys that appear more than once (keys are the stable ids — hard error). */
932
+ duplicateKeys: string[];
933
+ /** Flow pages sharing a slug (URL ambiguity — warning, not folded into `ok`). */
934
+ slugCollisions: {
935
+ slug: string;
936
+ pages: string[];
937
+ }[];
938
+ /** Variant pages (`x@b`) whose slot (`x`) doesn't exist as a flow page (hard error). */
939
+ orphanVariants: string[];
940
+ /**
941
+ * True when no `finish` page is reachable from the start — e.g. the flow loops
942
+ * (a cycle with no exit) or every path stalls before a terminal page. Dead ends
943
+ * catch pages with no outgoing edge; this catches funnels that *never end* even
944
+ * though every page can proceed. Hard error.
945
+ */
946
+ noReachableFinish: boolean;
947
+ /** Slot names a PAGE references (`useOffering`/`<Checkout offering>`) that the funnel doesn't declare — hard error. */
948
+ unknownOfferingRefs: string[];
949
+ /** Declared slots a page references but which have no catalog charge key assigned (map value `null`) — hard error. */
950
+ unassignedSlots: string[];
951
+ }
952
+ interface FunnelManifest {
953
+ id: string;
954
+ pages: ManifestPage[];
955
+ flow: {
956
+ start: string | null;
957
+ nodes: string[];
958
+ edges: ManifestEdge[];
959
+ };
960
+ /** The unique, ASSIGNED catalog keys the funnel charges (for the promote gate + catalog reverse-lookup). */
961
+ offerings: string[];
962
+ /** Offering SLOTS: slot → assigned catalog key|null. `offerings` above is the unique assigned keys. */
963
+ offeringSlots: OfferingSlot[];
964
+ /** Slot names PAGES reference (from the build's scan); validated against `offeringSlots`. Absent = no scan ran. */
965
+ offeringRefs?: string[];
966
+ /**
967
+ * The funnel's `checkout:` driver bundle from `funnel.ts` (`FunnelDefinition.checkout`), carried so
968
+ * the publish gate can cross-validate it against the offerings' RESOLVED provider (§7 item 5 — the
969
+ * redundant field is never checked today). Absent = the funnel declares no `checkout:`.
970
+ */
971
+ checkout?: 'stripe' | 'paddle';
972
+ /**
973
+ * Checkout-SURFACE usage the pages express (from `scanOfferingRefs(...).checkout`), carried so the
974
+ * publish (provider × surface) capability gate can run `validateCheckout`/`validateUpsell`. Absent =
975
+ * no scan ran (old manifest) → the gate skips capability validation (back-compat, no false blocks).
976
+ */
977
+ checkoutRefs?: CheckoutUsageScan;
978
+ locales?: FunnelLocales;
979
+ validation: ManifestValidation;
980
+ }
981
+ interface CompileInput {
982
+ funnel: FunnelDefinition;
983
+ /** Pages in file order (the default flow); `meta` holds type/slug/next. */
984
+ pages: {
985
+ key: string;
986
+ meta?: PageMeta;
987
+ }[];
988
+ /** Slot names the funnel's PAGES reference (from `scanOfferingRefs`) — cross-checked against the declared slots. */
989
+ offeringRefs?: string[];
990
+ /** Checkout-surface usage the pages express (from `scanOfferingRefs(...).checkout`) — carried onto the manifest for the publish capability gate. */
991
+ checkoutRefs?: CheckoutUsageScan;
992
+ }
993
+ /** Compile a funnel's spine into its declarative {@link FunnelManifest} IR. */
994
+ declare function compileManifest(input: CompileInput): FunnelManifest;
995
+
996
+ export { evaluateCondition as $, type Acquisition as A, type BrowserContext as B, type ClickIdContext as C, type Direction as D, type EdgeCondition as E, type FunnelContext as F, type GroupProgress as G, type Route as H, type IdentityContext as I, type SystemContext as J, assignVariant as K, type LocaleContext as L, type ManifestEdge as M, bucketingSeed as N, type Offering as O, type PageMeta as P, buildAcquisition as Q, type RuntimeExperiment as R, type SessionContext as S, buildCatalog as T, type UtmContext as U, type VariableValue as V, compileManifest as W, currencyExponent as X, defineFunnel as Y, definePage as Z, entryGuard as _, type VariableConfig as a, evaluateGate as a0, expectedPathLength as a1, fnv1a as a2, formatMoney as a3, formatOffering as a4, groupProgress as a5, hashToUnit as a6, isRtl as a7, isVariantKey as a8, nextPage as a9, outgoingKeys as aa, pageMeta as ab, parseSlotKey as ac, pickByWeight as ad, resolveExperiments as ae, resolveLocale as af, resolveOffering as ag, resolveRoute as ah, type CheckoutUsageScan as ai, type FunnelPage as aj, type OfferingRefScan as ak, type OfferingSlot as al, funnelCatalogKeys as am, normalizeOfferings as an, scanOfferingRefs as ao, weightsOf as ap, type FunnelSnapshot as b, type FunnelDefinition as c, buildContext as d, type OfferingInput as e, type PageContext as f, type BuildContextOptions as g, type CompileInput as h, type Condition as i, type ConditionOp as j, type DeviceContext as k, type ExperimentArm as l, type ExperimentResolution as m, type FlowPage as n, type FunnelLocales as o, type FunnelManifest as p, type Gate as q, type Interval as r, type ManifestPage as s, type ManifestValidation as t, type Money as u, type OsContext as v, type PageGroup as w, type PageType as x, type Predicate as y, type ResolvedOffering as z };