@sonordev/contracts 2.17.0 → 2.20.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.
@@ -85,6 +85,14 @@ export declare const BROADCAST_STYLES: readonly ['glass', 'editorial', 'poster',
85
85
  export type BroadcastStyleId = (typeof BROADCAST_STYLES)[number];
86
86
  export declare const DEFAULT_BROADCAST_STYLE: BroadcastStyleId;
87
87
  export declare const isBroadcastStyleId: (value: unknown) => value is BroadcastStyleId;
88
+ /** Explicit per-review photo presentation; absence keeps the existing automatic layout. */
89
+ export declare const REVIEW_PHOTO_PRESENTATIONS: readonly ['background'];
90
+ export type ReviewPhotoPresentation = (typeof REVIEW_PHOTO_PRESENTATIONS)[number];
91
+ /**
92
+ * A supported explicit photo presentation. Optional input boundaries handle
93
+ * omission (preserve) and null (clear) separately; neither is an explicit value.
94
+ */
95
+ export declare const isReviewPhotoPresentation: (value: unknown) => value is ReviewPhotoPresentation;
88
96
  /**
89
97
  * The review compositions Broadcast draws: `signature` (photo up top, the
90
98
  * quote low) and `frame` (centered). Order matters: a review whose v1 draw
@@ -29,6 +29,8 @@ var SAVED_PUBLICATION_CARD_LAYOUTS = [AUTO_CARD_LAYOUT, ...CARD_LAYOUTS];
29
29
  var BROADCAST_STYLES = ["glass", "editorial", "poster", "cinematic", "blueprint", "focus"];
30
30
  var DEFAULT_BROADCAST_STYLE = "glass";
31
31
  var isBroadcastStyleId = (value) => typeof value === "string" && BROADCAST_STYLES.includes(value);
32
+ var REVIEW_PHOTO_PRESENTATIONS = ["background"];
33
+ var isReviewPhotoPresentation = (value) => typeof value === "string" && REVIEW_PHOTO_PRESENTATIONS.includes(value);
32
34
  var REVIEW_CARD_VARIATIONS = ["signature", "frame"];
33
35
  var DEFAULT_REVIEW_CARD_VARIATION = "signature";
34
36
  var RETIRED_REVIEW_CARD_VARIATIONS = {
@@ -71,12 +73,14 @@ exports.REVIEW_CARD_VARIATIONS = REVIEW_CARD_VARIATIONS;
71
73
  exports.REVIEW_CARD_VARIATION_LABELS = REVIEW_CARD_VARIATION_LABELS;
72
74
  exports.REVIEW_CARD_VARIATION_OPTIONS = REVIEW_CARD_VARIATION_OPTIONS;
73
75
  exports.REVIEW_CARD_VARIATION_V1_DRAW = REVIEW_CARD_VARIATION_V1_DRAW;
76
+ exports.REVIEW_PHOTO_PRESENTATIONS = REVIEW_PHOTO_PRESENTATIONS;
74
77
  exports.SAVED_PUBLICATION_CARD_LAYOUTS = SAVED_PUBLICATION_CARD_LAYOUTS;
75
78
  exports.isArticleCoverLayoutId = isArticleCoverLayoutId;
76
79
  exports.isBroadcastStyleId = isBroadcastStyleId;
77
80
  exports.isCardLayoutId = isCardLayoutId;
78
81
  exports.isRetiredCardLayoutId = isRetiredCardLayoutId;
79
82
  exports.isReviewCardVariation = isReviewCardVariation;
83
+ exports.isReviewPhotoPresentation = isReviewPhotoPresentation;
80
84
  exports.normalizeArticleCoverLayout = normalizeArticleCoverLayout;
81
85
  exports.normalizeReviewCardVariation = normalizeReviewCardVariation;
82
86
  exports.reviewCardVariationFromDigest = reviewCardVariationFromDigest;
@@ -27,6 +27,8 @@ var SAVED_PUBLICATION_CARD_LAYOUTS = [AUTO_CARD_LAYOUT, ...CARD_LAYOUTS];
27
27
  var BROADCAST_STYLES = ["glass", "editorial", "poster", "cinematic", "blueprint", "focus"];
28
28
  var DEFAULT_BROADCAST_STYLE = "glass";
29
29
  var isBroadcastStyleId = (value) => typeof value === "string" && BROADCAST_STYLES.includes(value);
30
+ var REVIEW_PHOTO_PRESENTATIONS = ["background"];
31
+ var isReviewPhotoPresentation = (value) => typeof value === "string" && REVIEW_PHOTO_PRESENTATIONS.includes(value);
30
32
  var REVIEW_CARD_VARIATIONS = ["signature", "frame"];
31
33
  var DEFAULT_REVIEW_CARD_VARIATION = "signature";
32
34
  var RETIRED_REVIEW_CARD_VARIATIONS = {
@@ -53,4 +55,4 @@ function reviewCardVariationFromDigest(digest) {
53
55
  return isReviewCardVariation(drawn) ? drawn : REVIEW_CARD_VARIATIONS[uint32BE(digest, 4) % REVIEW_CARD_VARIATIONS.length];
54
56
  }
55
57
 
56
- export { ARTICLE_COVER_LAYOUTS, AUTO_CARD_LAYOUT, BROADCAST_LAYOUTS_CONTRACT_VERSION, BROADCAST_STYLES, CARD_LAYOUTS, CARD_LAYOUT_LABELS, CARD_LAYOUT_OPTIONS, DEFAULT_BROADCAST_STYLE, DEFAULT_REVIEW_CARD_VARIATION, PUBLICATION_CARD_LAYOUTS, RETIRED_CARD_LAYOUTS, RETIRED_REVIEW_CARD_VARIATIONS, REVIEW_CARD_VARIATIONS, REVIEW_CARD_VARIATION_LABELS, REVIEW_CARD_VARIATION_OPTIONS, REVIEW_CARD_VARIATION_V1_DRAW, SAVED_PUBLICATION_CARD_LAYOUTS, isArticleCoverLayoutId, isBroadcastStyleId, isCardLayoutId, isRetiredCardLayoutId, isReviewCardVariation, normalizeArticleCoverLayout, normalizeReviewCardVariation, reviewCardVariationFromDigest, reviewCompositionSeed };
58
+ export { ARTICLE_COVER_LAYOUTS, AUTO_CARD_LAYOUT, BROADCAST_LAYOUTS_CONTRACT_VERSION, BROADCAST_STYLES, CARD_LAYOUTS, CARD_LAYOUT_LABELS, CARD_LAYOUT_OPTIONS, DEFAULT_BROADCAST_STYLE, DEFAULT_REVIEW_CARD_VARIATION, PUBLICATION_CARD_LAYOUTS, RETIRED_CARD_LAYOUTS, RETIRED_REVIEW_CARD_VARIATIONS, REVIEW_CARD_VARIATIONS, REVIEW_CARD_VARIATION_LABELS, REVIEW_CARD_VARIATION_OPTIONS, REVIEW_CARD_VARIATION_V1_DRAW, REVIEW_PHOTO_PRESENTATIONS, SAVED_PUBLICATION_CARD_LAYOUTS, isArticleCoverLayoutId, isBroadcastStyleId, isCardLayoutId, isRetiredCardLayoutId, isReviewCardVariation, isReviewPhotoPresentation, normalizeArticleCoverLayout, normalizeReviewCardVariation, reviewCardVariationFromDigest, reviewCompositionSeed };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * What a job title says about a person's part in a buying decision. Single
3
+ * source of truth for the site-people scan: the Sonor API's deterministic scan
4
+ * (JSON-LD persons) and Signal's site-people scout both read this map, so the
5
+ * same person can't be an influencer on one side and an owner on the other.
6
+ *
7
+ * The role is never a model's opinion. A model finds the people; the title the
8
+ * page gives them decides the role, by the rules below, in this order (first
9
+ * match wins):
10
+ *
11
+ * gatekeeper someone who works for a person, whatever that person's
12
+ * title is: "Executive Assistant to the President",
13
+ * "Receptionist"
14
+ * decision_maker a vice president, matched before the president rule can
15
+ * claim the word
16
+ * owner owner, founder, president, CEO, a managing, senior, name
17
+ * or equity partner, a lone "Partner", and "Principal"
18
+ * unless it names a job ("Principal Engineer")
19
+ * decision_maker a chief of something that isn't the CEO, a general
20
+ * manager (or GM), a director, an operations or project
21
+ * manager, an estimator, a superintendent
22
+ * gatekeeper an office manager, an administrator
23
+ * influencer marketing, HR, recruiting, sales, business development
24
+ * gatekeeper a coordinator, last of all
25
+ *
26
+ * So "Vice President of Sales" is a decision maker (not an owner because it
27
+ * says president, not an influencer because it says sales), "Marketing
28
+ * Director" is a decision maker (director beats marketing) and "Marketing
29
+ * Coordinator" is an influencer. A title that names none of these
30
+ * ("Technician", a bare "Manager") has no role: no guess.
31
+ *
32
+ * Pure data and one small function, no dependencies. The four role values are
33
+ * stored and sent, so they never change.
34
+ */
35
+ /** The four parts a person can play in a buying decision, strongest first. */
36
+ export declare const DECISION_ROLES: readonly ['owner', 'decision_maker', 'influencer', 'gatekeeper'];
37
+ export type DecisionRole = (typeof DECISION_ROLES)[number];
38
+ /** The buying role a job title says, or null when it says none ("Technician", "Manager", no title). */
39
+ export declare function decisionRoleForTitle(title: string | null | undefined): DecisionRole | null;
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ // src/contracts/decision-roles.ts
4
+ var DECISION_ROLES = ["owner", "decision_maker", "influencer", "gatekeeper"];
5
+ var MAX_TITLE_LENGTH = 200;
6
+ var RULES = [
7
+ // Someone who works for a person, whatever that person's title is.
8
+ [/\b(?:executive|administrative|admin|office|personal)\s+assistant\b|\bassistant to\b|\bsecretary\b|\breceptionist\b|\bfront desk\b/, "gatekeeper"],
9
+ // A vice president before the president rule below can claim the word.
10
+ [/\b(?:vice[ -]president|vp|svp|evp|avp)\b/, "decision_maker"],
11
+ [
12
+ /\b(?:owner|co-?owner|founder|co-?founder|proprietor|president|ceo|chief executive(?: officer)?|managing partner|senior partner|name partner|equity partner)\b|\bprincipal\b(?!\s+(?:engineer|architect|consultant|designer|developer|analyst))|^partner$/,
13
+ "owner"
14
+ ],
15
+ // Chief of something else (not the CEO, matched above).
16
+ [/\bchief\b.*\bofficer\b|\b(?:coo|cfo|cto|cmo)\b/, "decision_maker"],
17
+ [/\b(?:general manager|gm|director|operations manager|estimator|estimating manager|project manager|superintendent)\b/, "decision_maker"],
18
+ [/\boffice manager\b|\badministrat(?:or|ive)\b|\badmin\b/, "gatekeeper"],
19
+ [/\b(?:marketing|hr|human resources|recruit(?:er|ing)|talent acquisition|sales|business development)\b/, "influencer"],
20
+ [/\bcoordinator\b/, "gatekeeper"]
21
+ ];
22
+ function decisionRoleForTitle(title) {
23
+ if (typeof title !== "string") return null;
24
+ const t = title.slice(0, MAX_TITLE_LENGTH).toLowerCase().replace(/&/g, " and ").replace(/\./g, "").replace(/\s+/g, " ").trim();
25
+ if (!t) return null;
26
+ for (const [pattern, role] of RULES) if (pattern.test(t)) return role;
27
+ return null;
28
+ }
29
+
30
+ exports.DECISION_ROLES = DECISION_ROLES;
31
+ exports.decisionRoleForTitle = decisionRoleForTitle;
@@ -0,0 +1,28 @@
1
+ // src/contracts/decision-roles.ts
2
+ var DECISION_ROLES = ["owner", "decision_maker", "influencer", "gatekeeper"];
3
+ var MAX_TITLE_LENGTH = 200;
4
+ var RULES = [
5
+ // Someone who works for a person, whatever that person's title is.
6
+ [/\b(?:executive|administrative|admin|office|personal)\s+assistant\b|\bassistant to\b|\bsecretary\b|\breceptionist\b|\bfront desk\b/, "gatekeeper"],
7
+ // A vice president before the president rule below can claim the word.
8
+ [/\b(?:vice[ -]president|vp|svp|evp|avp)\b/, "decision_maker"],
9
+ [
10
+ /\b(?:owner|co-?owner|founder|co-?founder|proprietor|president|ceo|chief executive(?: officer)?|managing partner|senior partner|name partner|equity partner)\b|\bprincipal\b(?!\s+(?:engineer|architect|consultant|designer|developer|analyst))|^partner$/,
11
+ "owner"
12
+ ],
13
+ // Chief of something else (not the CEO, matched above).
14
+ [/\bchief\b.*\bofficer\b|\b(?:coo|cfo|cto|cmo)\b/, "decision_maker"],
15
+ [/\b(?:general manager|gm|director|operations manager|estimator|estimating manager|project manager|superintendent)\b/, "decision_maker"],
16
+ [/\boffice manager\b|\badministrat(?:or|ive)\b|\badmin\b/, "gatekeeper"],
17
+ [/\b(?:marketing|hr|human resources|recruit(?:er|ing)|talent acquisition|sales|business development)\b/, "influencer"],
18
+ [/\bcoordinator\b/, "gatekeeper"]
19
+ ];
20
+ function decisionRoleForTitle(title) {
21
+ if (typeof title !== "string") return null;
22
+ const t = title.slice(0, MAX_TITLE_LENGTH).toLowerCase().replace(/&/g, " and ").replace(/\./g, "").replace(/\s+/g, " ").trim();
23
+ if (!t) return null;
24
+ for (const [pattern, role] of RULES) if (pattern.test(t)) return role;
25
+ return null;
26
+ }
27
+
28
+ export { DECISION_ROLES, decisionRoleForTitle };
package/dist/funnels.d.ts CHANGED
@@ -152,6 +152,27 @@ export interface PublishedFunnelPage {
152
152
  tracking: {
153
153
  campaignSlug: string;
154
154
  };
155
+ /** Optional reviewed content only. Requires a compatible installed renderer;
156
+ * older strict page parsers reject this field. No assignment/publication grant. */
157
+ experimentContent?: FunnelExperimentContentManifest;
158
+ }
159
+ export interface FunnelExperimentContentManifest {
160
+ schemaVersion: 1;
161
+ kind: 'funnel_experiment_content_v1';
162
+ experimentId: string;
163
+ experimentRevision: number;
164
+ goalRevision: string;
165
+ controlVariantKey: string;
166
+ /** contentDigest covers effective FULL content, including the fixed base offer.
167
+ * Native funnelHash owns canonical hashing; this parser checks digest shape only. */
168
+ variants: Array<{
169
+ variantKey: string;
170
+ content: {
171
+ headline: string;
172
+ body: FunnelBodyBlock[];
173
+ };
174
+ contentDigest: string;
175
+ }>;
155
176
  }
156
177
  export interface FunnelOperationReceipt {
157
178
  operationId: string;
@@ -276,6 +297,24 @@ export declare const FUNNEL_DEFINITION_JSON_SCHEMA: {
276
297
  readonly oneOf?: readonly Shape[];
277
298
  readonly $schema: 'http://json-schema.org/draft-07/schema#';
278
299
  };
300
+ export declare const FUNNEL_EXPERIMENT_CONTENT_JSON_SCHEMA: {
301
+ readonly type?: 'object' | 'array' | 'string' | 'integer' | 'boolean' | 'null';
302
+ readonly properties?: Record<string, Shape>;
303
+ readonly required?: readonly string[];
304
+ readonly additionalProperties?: false;
305
+ readonly items?: Shape;
306
+ readonly minItems?: number;
307
+ readonly maxItems?: number;
308
+ readonly minLength?: number;
309
+ readonly maxLength?: number;
310
+ readonly pattern?: string;
311
+ readonly minimum?: number;
312
+ readonly maximum?: number;
313
+ readonly enum?: readonly unknown[];
314
+ readonly const?: unknown;
315
+ readonly oneOf?: readonly Shape[];
316
+ readonly $schema: 'http://json-schema.org/draft-07/schema#';
317
+ };
279
318
  export declare const PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA: {
280
319
  readonly type?: 'object' | 'array' | 'string' | 'integer' | 'boolean' | 'null';
281
320
  readonly properties?: Record<string, Shape>;
@@ -306,6 +345,13 @@ export declare function isSafeFunnelPublicUrl(value: string): boolean;
306
345
  export declare function managedFunnelPath(value: string): string;
307
346
  export declare function parseFunnelDefinition(value: unknown): FunnelDefinition;
308
347
  export declare function parsePublishedFunnelPage(value: unknown): PublishedFunnelPage;
348
+ /** Shape/content checks only. Native owners verify the complete original Signal
349
+ * variant set and hash each effective FULL content with existing funnelHash. */
350
+ export declare function parseFunnelExperimentContent(value: unknown): FunnelExperimentContentManifest;
351
+ /** One effective-content composition for native hashing and existing rendering.
352
+ * Offer and all page identity/form/brand/tracking fields stay on the base page;
353
+ * a client-provided digest or key grants no native permission. */
354
+ export declare function resolveFunnelExperimentContent(value: unknown, variantKey: string): PublishedFunnelPage['content'];
309
355
  export declare function parseFunnelLaunchArgs(value: unknown): FunnelLaunchArgs;
310
356
  export declare function parseFunnelLifecycleArgs(value: unknown, operation?: 'pause' | 'resume' | 'rollback'): FunnelLifecycleArgs;
311
357
  /** Apply presentation-only overrides to an already resolved canonical brand. Null means inherit. */
package/dist/funnels.js CHANGED
@@ -82,6 +82,7 @@ var integer = (minimum, maximum = Number.MAX_SAFE_INTEGER) => ({ type: "integer"
82
82
  var nullable = (shape) => ({ oneOf: [shape, { type: "null" }] });
83
83
  var isoDate = { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$" };
84
84
  var uuid = { type: "string", pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" };
85
+ var fingerprint = { type: "string", pattern: "^[a-f0-9]{64}$" };
85
86
  var slug = { ...string(80), pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" };
86
87
  var fieldSlug = { ...string(80), pattern: "^[a-zA-Z0-9_-]+$" };
87
88
  var path = { ...string(512), pattern: "^/(?!/)[a-zA-Z0-9/_-]*$" };
@@ -99,6 +100,16 @@ var override = object({
99
100
  }, []);
100
101
  var bodyBlock = { oneOf: [object({ type: literal("paragraph"), text: string(2e3) }), object({ type: literal("bullets"), items: array(string(500), 8, 1) })] };
101
102
  var body = array(bodyBlock, 12);
103
+ var publishedContentFields = { headline: string(160), body };
104
+ var experimentContentShape = object({
105
+ schemaVersion: literal(1),
106
+ kind: literal("funnel_experiment_content_v1"),
107
+ experimentId: uuid,
108
+ experimentRevision: integer(1),
109
+ goalRevision: { ...string(128), pattern: "^[a-zA-Z0-9._:-]+$" },
110
+ controlVariantKey: string(80),
111
+ variants: array(object({ variantKey: string(80), content: object(publishedContentFields), contentDigest: fingerprint }), 50, 2)
112
+ });
102
113
  var publicBrand = object({ displayName: string(120), tagline: string(300, 0), fullLogoUrl: nullable(string(2048)), squareLogoUrl: nullable(string(2048)), colors, typography, scheme: enumeration("light", "dark") });
103
114
  var step = { oneOf: [
104
115
  object({ id: uuid, type: literal("wait"), durationSeconds: integer(60, 2592e3) }),
@@ -178,11 +189,13 @@ var publishedShape = object({
178
189
  revision: integer(1),
179
190
  site,
180
191
  path,
181
- content: object({ headline: string(160), body, offer: object({ label: string(120), href: string(2048) }) }, ["headline", "body"]),
192
+ content: object({ ...publishedContentFields, offer: object({ label: string(120), href: string(2048) }) }, ["headline", "body"]),
182
193
  brand: publicBrand,
183
194
  conversion: object({ formSlug: fieldSlug }),
184
- tracking: object({ campaignSlug: slug })
185
- });
195
+ tracking: object({ campaignSlug: slug }),
196
+ experimentContent: experimentContentShape
197
+ }, ["schemaVersion", "id", "versionId", "revision", "site", "path", "content", "brand", "conversion", "tracking"]);
198
+ var FUNNEL_EXPERIMENT_CONTENT_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", ...experimentContentShape };
186
199
  var PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", ...publishedShape };
187
200
  var FUNNEL_LAUNCH_JSON_SCHEMA = object({ expectedRevision: integer(1), previewId: uuid, idempotencyKey: string(200), confirm: literal(true) });
188
201
  var FUNNEL_LIFECYCLE_JSON_SCHEMA = object({ expectedActiveVersionId: uuid, idempotencyKey: string(200), confirm: literal(true) });
@@ -285,8 +298,24 @@ function parsePublishedFunnelPage(value) {
285
298
  checkFonts(result.brand, "page.brand");
286
299
  checkBody(result.content.body, "page.content.body");
287
300
  if (result.content.offer && !isSafeFunnelPublicUrl(result.content.offer.href)) fail("page.content.offer.href", "unsafe public URL");
301
+ if (result.experimentContent) result.experimentContent = parseFunnelExperimentContent(result.experimentContent);
288
302
  return result;
289
303
  }
304
+ function parseFunnelExperimentContent(value) {
305
+ check(value, experimentContentShape, "experimentContent");
306
+ const result = JSON.parse(JSON.stringify(value));
307
+ const keys = result.variants.map((v) => v.variantKey);
308
+ if (new Set(keys).size !== keys.length) fail("experimentContent.variants", "variant keys must be unique");
309
+ if (!keys.includes(result.controlVariantKey)) fail("experimentContent.controlVariantKey", "must identify an existing reviewed variant");
310
+ result.variants.forEach((variant, i) => checkBody(variant.content.body, `experimentContent.variants[${i}].content.body`));
311
+ return result;
312
+ }
313
+ function resolveFunnelExperimentContent(value, variantKey) {
314
+ const page = parsePublishedFunnelPage(value);
315
+ const variant = page.experimentContent?.variants.find((v) => v.variantKey === variantKey);
316
+ if (!variant) fail("page.experimentContent.variants", "an existing reviewed variant is required");
317
+ return { ...page.content, headline: variant.content.headline, body: variant.content.body };
318
+ }
290
319
  function parseFunnelLaunchArgs(value) {
291
320
  check(value, FUNNEL_LAUNCH_JSON_SCHEMA, "launch");
292
321
  return JSON.parse(JSON.stringify(value));
@@ -351,7 +380,6 @@ function parseFunnelRetryArgs(value) {
351
380
  check(value, FUNNEL_RETRY_JSON_SCHEMA, "retry");
352
381
  return JSON.parse(JSON.stringify(value));
353
382
  }
354
- var fingerprint = { type: "string", pattern: "^[a-f0-9]{64}$" };
355
383
  var issue = object({ code: { ...string(80), pattern: "^[A-Z][A-Z0-9_]*$" }, path: string(300), message: string(500), fixRef: object({ section: string(80), field: string(80), resourceId: uuid }, ["section"]) }, ["code", "path", "message"]);
356
384
  var FUNNEL_READINESS_JSON_SCHEMA = object({ definitionRevision: integer(1), brandFingerprint: fingerprint, resourceFingerprint: fingerprint, ready: { type: "boolean" }, blockers: array(issue, 50), warnings: array(issue, 50), capabilities: object({ managedPage: { type: "boolean" } }) });
357
385
  function parseFunnelReadiness(value) {
@@ -567,6 +595,7 @@ exports.FUNNEL_DETAIL_JSON_SCHEMA = FUNNEL_DETAIL_JSON_SCHEMA;
567
595
  exports.FUNNEL_DRAFT_DEFINITION_JSON_SCHEMA = FUNNEL_DRAFT_DEFINITION_JSON_SCHEMA;
568
596
  exports.FUNNEL_DRAFT_JSON_SCHEMAS = FUNNEL_DRAFT_JSON_SCHEMAS;
569
597
  exports.FUNNEL_ENROLLMENT_JSON_SCHEMA = FUNNEL_ENROLLMENT_JSON_SCHEMA;
598
+ exports.FUNNEL_EXPERIMENT_CONTENT_JSON_SCHEMA = FUNNEL_EXPERIMENT_CONTENT_JSON_SCHEMA;
570
599
  exports.FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA = FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA;
571
600
  exports.FUNNEL_LAUNCH_JSON_SCHEMA = FUNNEL_LAUNCH_JSON_SCHEMA;
572
601
  exports.FUNNEL_LIFECYCLE_JSON_SCHEMA = FUNNEL_LIFECYCLE_JSON_SCHEMA;
@@ -600,6 +629,7 @@ exports.parseFunnelDetail = parseFunnelDetail;
600
629
  exports.parseFunnelDraftArgs = parseFunnelDraftArgs;
601
630
  exports.parseFunnelDraftDefinition = parseFunnelDraftDefinition;
602
631
  exports.parseFunnelEnrollmentSummary = parseFunnelEnrollmentSummary;
632
+ exports.parseFunnelExperimentContent = parseFunnelExperimentContent;
603
633
  exports.parseFunnelLaunchArgs = parseFunnelLaunchArgs;
604
634
  exports.parseFunnelLaunchEvidence = parseFunnelLaunchEvidence;
605
635
  exports.parseFunnelLifecycleArgs = parseFunnelLifecycleArgs;
@@ -615,3 +645,4 @@ exports.parseFunnelSummary = parseFunnelSummary;
615
645
  exports.parseFunnelTimeline = parseFunnelTimeline;
616
646
  exports.parseFunnelUpdateArgs = parseFunnelUpdateArgs;
617
647
  exports.parsePublishedFunnelPage = parsePublishedFunnelPage;
648
+ exports.resolveFunnelExperimentContent = resolveFunnelExperimentContent;
package/dist/funnels.mjs CHANGED
@@ -80,6 +80,7 @@ var integer = (minimum, maximum = Number.MAX_SAFE_INTEGER) => ({ type: "integer"
80
80
  var nullable = (shape) => ({ oneOf: [shape, { type: "null" }] });
81
81
  var isoDate = { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$" };
82
82
  var uuid = { type: "string", pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" };
83
+ var fingerprint = { type: "string", pattern: "^[a-f0-9]{64}$" };
83
84
  var slug = { ...string(80), pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" };
84
85
  var fieldSlug = { ...string(80), pattern: "^[a-zA-Z0-9_-]+$" };
85
86
  var path = { ...string(512), pattern: "^/(?!/)[a-zA-Z0-9/_-]*$" };
@@ -97,6 +98,16 @@ var override = object({
97
98
  }, []);
98
99
  var bodyBlock = { oneOf: [object({ type: literal("paragraph"), text: string(2e3) }), object({ type: literal("bullets"), items: array(string(500), 8, 1) })] };
99
100
  var body = array(bodyBlock, 12);
101
+ var publishedContentFields = { headline: string(160), body };
102
+ var experimentContentShape = object({
103
+ schemaVersion: literal(1),
104
+ kind: literal("funnel_experiment_content_v1"),
105
+ experimentId: uuid,
106
+ experimentRevision: integer(1),
107
+ goalRevision: { ...string(128), pattern: "^[a-zA-Z0-9._:-]+$" },
108
+ controlVariantKey: string(80),
109
+ variants: array(object({ variantKey: string(80), content: object(publishedContentFields), contentDigest: fingerprint }), 50, 2)
110
+ });
100
111
  var publicBrand = object({ displayName: string(120), tagline: string(300, 0), fullLogoUrl: nullable(string(2048)), squareLogoUrl: nullable(string(2048)), colors, typography, scheme: enumeration("light", "dark") });
101
112
  var step = { oneOf: [
102
113
  object({ id: uuid, type: literal("wait"), durationSeconds: integer(60, 2592e3) }),
@@ -176,11 +187,13 @@ var publishedShape = object({
176
187
  revision: integer(1),
177
188
  site,
178
189
  path,
179
- content: object({ headline: string(160), body, offer: object({ label: string(120), href: string(2048) }) }, ["headline", "body"]),
190
+ content: object({ ...publishedContentFields, offer: object({ label: string(120), href: string(2048) }) }, ["headline", "body"]),
180
191
  brand: publicBrand,
181
192
  conversion: object({ formSlug: fieldSlug }),
182
- tracking: object({ campaignSlug: slug })
183
- });
193
+ tracking: object({ campaignSlug: slug }),
194
+ experimentContent: experimentContentShape
195
+ }, ["schemaVersion", "id", "versionId", "revision", "site", "path", "content", "brand", "conversion", "tracking"]);
196
+ var FUNNEL_EXPERIMENT_CONTENT_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", ...experimentContentShape };
184
197
  var PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", ...publishedShape };
185
198
  var FUNNEL_LAUNCH_JSON_SCHEMA = object({ expectedRevision: integer(1), previewId: uuid, idempotencyKey: string(200), confirm: literal(true) });
186
199
  var FUNNEL_LIFECYCLE_JSON_SCHEMA = object({ expectedActiveVersionId: uuid, idempotencyKey: string(200), confirm: literal(true) });
@@ -283,8 +296,24 @@ function parsePublishedFunnelPage(value) {
283
296
  checkFonts(result.brand, "page.brand");
284
297
  checkBody(result.content.body, "page.content.body");
285
298
  if (result.content.offer && !isSafeFunnelPublicUrl(result.content.offer.href)) fail("page.content.offer.href", "unsafe public URL");
299
+ if (result.experimentContent) result.experimentContent = parseFunnelExperimentContent(result.experimentContent);
286
300
  return result;
287
301
  }
302
+ function parseFunnelExperimentContent(value) {
303
+ check(value, experimentContentShape, "experimentContent");
304
+ const result = JSON.parse(JSON.stringify(value));
305
+ const keys = result.variants.map((v) => v.variantKey);
306
+ if (new Set(keys).size !== keys.length) fail("experimentContent.variants", "variant keys must be unique");
307
+ if (!keys.includes(result.controlVariantKey)) fail("experimentContent.controlVariantKey", "must identify an existing reviewed variant");
308
+ result.variants.forEach((variant, i) => checkBody(variant.content.body, `experimentContent.variants[${i}].content.body`));
309
+ return result;
310
+ }
311
+ function resolveFunnelExperimentContent(value, variantKey) {
312
+ const page = parsePublishedFunnelPage(value);
313
+ const variant = page.experimentContent?.variants.find((v) => v.variantKey === variantKey);
314
+ if (!variant) fail("page.experimentContent.variants", "an existing reviewed variant is required");
315
+ return { ...page.content, headline: variant.content.headline, body: variant.content.body };
316
+ }
288
317
  function parseFunnelLaunchArgs(value) {
289
318
  check(value, FUNNEL_LAUNCH_JSON_SCHEMA, "launch");
290
319
  return JSON.parse(JSON.stringify(value));
@@ -349,7 +378,6 @@ function parseFunnelRetryArgs(value) {
349
378
  check(value, FUNNEL_RETRY_JSON_SCHEMA, "retry");
350
379
  return JSON.parse(JSON.stringify(value));
351
380
  }
352
- var fingerprint = { type: "string", pattern: "^[a-f0-9]{64}$" };
353
381
  var issue = object({ code: { ...string(80), pattern: "^[A-Z][A-Z0-9_]*$" }, path: string(300), message: string(500), fixRef: object({ section: string(80), field: string(80), resourceId: uuid }, ["section"]) }, ["code", "path", "message"]);
354
382
  var FUNNEL_READINESS_JSON_SCHEMA = object({ definitionRevision: integer(1), brandFingerprint: fingerprint, resourceFingerprint: fingerprint, ready: { type: "boolean" }, blockers: array(issue, 50), warnings: array(issue, 50), capabilities: object({ managedPage: { type: "boolean" } }) });
355
383
  function parseFunnelReadiness(value) {
@@ -557,4 +585,4 @@ function parseFunnelPublishedVersionDetail(value) {
557
585
  }
558
586
  for (const schema of [FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA, FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA, FUNNEL_VERSION_JSON_SCHEMA, FUNNEL_VERSION_DETAIL_JSON_SCHEMA]) freezeSchema(schema);
559
587
 
560
- export { FUNNEL_BRAND_SNAPSHOT_JSON_SCHEMA, FUNNEL_CATALOG_JSON_SCHEMA, FUNNEL_CREATE_JSON_SCHEMA, FUNNEL_DEFINITION_JSON_SCHEMA, FUNNEL_DETAIL_JSON_SCHEMA, FUNNEL_DRAFT_DEFINITION_JSON_SCHEMA, FUNNEL_DRAFT_JSON_SCHEMAS, FUNNEL_ENROLLMENT_JSON_SCHEMA, FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA, FUNNEL_LAUNCH_JSON_SCHEMA, FUNNEL_LIFECYCLE_JSON_SCHEMA, FUNNEL_OPERATION_RECEIPT_JSON_SCHEMA, FUNNEL_PREVIEW_JSON_SCHEMA, FUNNEL_READINESS_JSON_SCHEMA, FUNNEL_RETRY_JSON_SCHEMA, FUNNEL_REVISION_JSON_SCHEMA, FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA, FUNNEL_ROLLBACK_JSON_SCHEMA, FUNNEL_SCHEMA_VERSION, FUNNEL_SUMMARY_JSON_SCHEMA, FUNNEL_TEMPLATES, FUNNEL_TIMELINE_JSON_SCHEMA, FUNNEL_UPDATE_JSON_SCHEMA, FUNNEL_VERSION_DETAIL_JSON_SCHEMA, FUNNEL_VERSION_JSON_SCHEMA, FunnelValidationError, MANAGED_FUNNEL_PATH_PREFIX, PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA, applyFunnelBrandOverride, createFunnelDraft, isSafeFunnelPublicUrl, managedFunnelPath, normalizeCampaignSlug, parseFunnelBrandSnapshot, parseFunnelCatalog, parseFunnelCreateArgs, parseFunnelDefinition, parseFunnelDetail, parseFunnelDraftArgs, parseFunnelDraftDefinition, parseFunnelEnrollmentSummary, parseFunnelLaunchArgs, parseFunnelLaunchEvidence, parseFunnelLifecycleArgs, parseFunnelOperationReceipt, parseFunnelPreview, parseFunnelPublishedVersion, parseFunnelPublishedVersionDetail, parseFunnelReadiness, parseFunnelRetryArgs, parseFunnelRevisionArgs, parseFunnelRollbackEvidence, parseFunnelSummary, parseFunnelTimeline, parseFunnelUpdateArgs, parsePublishedFunnelPage };
588
+ export { FUNNEL_BRAND_SNAPSHOT_JSON_SCHEMA, FUNNEL_CATALOG_JSON_SCHEMA, FUNNEL_CREATE_JSON_SCHEMA, FUNNEL_DEFINITION_JSON_SCHEMA, FUNNEL_DETAIL_JSON_SCHEMA, FUNNEL_DRAFT_DEFINITION_JSON_SCHEMA, FUNNEL_DRAFT_JSON_SCHEMAS, FUNNEL_ENROLLMENT_JSON_SCHEMA, FUNNEL_EXPERIMENT_CONTENT_JSON_SCHEMA, FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA, FUNNEL_LAUNCH_JSON_SCHEMA, FUNNEL_LIFECYCLE_JSON_SCHEMA, FUNNEL_OPERATION_RECEIPT_JSON_SCHEMA, FUNNEL_PREVIEW_JSON_SCHEMA, FUNNEL_READINESS_JSON_SCHEMA, FUNNEL_RETRY_JSON_SCHEMA, FUNNEL_REVISION_JSON_SCHEMA, FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA, FUNNEL_ROLLBACK_JSON_SCHEMA, FUNNEL_SCHEMA_VERSION, FUNNEL_SUMMARY_JSON_SCHEMA, FUNNEL_TEMPLATES, FUNNEL_TIMELINE_JSON_SCHEMA, FUNNEL_UPDATE_JSON_SCHEMA, FUNNEL_VERSION_DETAIL_JSON_SCHEMA, FUNNEL_VERSION_JSON_SCHEMA, FunnelValidationError, MANAGED_FUNNEL_PATH_PREFIX, PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA, applyFunnelBrandOverride, createFunnelDraft, isSafeFunnelPublicUrl, managedFunnelPath, normalizeCampaignSlug, parseFunnelBrandSnapshot, parseFunnelCatalog, parseFunnelCreateArgs, parseFunnelDefinition, parseFunnelDetail, parseFunnelDraftArgs, parseFunnelDraftDefinition, parseFunnelEnrollmentSummary, parseFunnelExperimentContent, parseFunnelLaunchArgs, parseFunnelLaunchEvidence, parseFunnelLifecycleArgs, parseFunnelOperationReceipt, parseFunnelPreview, parseFunnelPublishedVersion, parseFunnelPublishedVersionDetail, parseFunnelReadiness, parseFunnelRetryArgs, parseFunnelRevisionArgs, parseFunnelRollbackEvidence, parseFunnelSummary, parseFunnelTimeline, parseFunnelUpdateArgs, parsePublishedFunnelPage, resolveFunnelExperimentContent };
@@ -0,0 +1,4 @@
1
+ /** Single source of truth for inert Resonance wire literals; safe in browser entry graphs. */
2
+ export declare const RESONANCE_ATTRIBUTION_MODEL: 'first_party_last_touch_marker_v1';
3
+ export declare const RESONANCE_SOURCE_MARKER_PARAMETER: 'sonor_source';
4
+ export declare const RESONANCE_SUBJECT_HEADER: 'X-Sonor-Resonance-Subject';
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ // src/contracts/resonance-constants.ts
4
+ var RESONANCE_ATTRIBUTION_MODEL = "first_party_last_touch_marker_v1";
5
+ var RESONANCE_SOURCE_MARKER_PARAMETER = "sonor_source";
6
+ var RESONANCE_SUBJECT_HEADER = "X-Sonor-Resonance-Subject";
7
+
8
+ exports.RESONANCE_ATTRIBUTION_MODEL = RESONANCE_ATTRIBUTION_MODEL;
9
+ exports.RESONANCE_SOURCE_MARKER_PARAMETER = RESONANCE_SOURCE_MARKER_PARAMETER;
10
+ exports.RESONANCE_SUBJECT_HEADER = RESONANCE_SUBJECT_HEADER;
@@ -0,0 +1,6 @@
1
+ // src/contracts/resonance-constants.ts
2
+ var RESONANCE_ATTRIBUTION_MODEL = "first_party_last_touch_marker_v1";
3
+ var RESONANCE_SOURCE_MARKER_PARAMETER = "sonor_source";
4
+ var RESONANCE_SUBJECT_HEADER = "X-Sonor-Resonance-Subject";
5
+
6
+ export { RESONANCE_ATTRIBUTION_MODEL, RESONANCE_SOURCE_MARKER_PARAMETER, RESONANCE_SUBJECT_HEADER };