@nextcommerce/campaigns-os 1.33.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.
Files changed (310) hide show
  1. package/AGENTS.md +204 -0
  2. package/CHANGELOG.md +5002 -0
  3. package/CONTEXT.md +685 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +4 -0
  6. package/README.md +368 -0
  7. package/agents/claude/CLAUDE.md +32 -0
  8. package/agents/codex/AGENTS.md +27 -0
  9. package/agents/copilot/copilot-instructions.md +14 -0
  10. package/agents/cursor/campaigns-os.mdc +13 -0
  11. package/bin/campaigns-os.mjs +38 -0
  12. package/campaign-spec/README.md +138 -0
  13. package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
  14. package/campaign-spec/dist/analytics-vocabulary.js +74 -0
  15. package/campaign-spec/dist/index.d.ts +40 -0
  16. package/campaign-spec/dist/index.js +77 -0
  17. package/campaign-spec/dist/normalize.d.ts +22 -0
  18. package/campaign-spec/dist/normalize.js +41 -0
  19. package/campaign-spec/dist/routing.d.ts +190 -0
  20. package/campaign-spec/dist/routing.js +263 -0
  21. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
  22. package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
  23. package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
  24. package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
  25. package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
  26. package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
  27. package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
  28. package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
  29. package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
  30. package/campaign-spec/dist/rules/cycle-detection.js +141 -0
  31. package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
  32. package/campaign-spec/dist/rules/design-source-shape.js +142 -0
  33. package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
  34. package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
  35. package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
  36. package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
  37. package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
  38. package/campaign-spec/dist/rules/funnel-count.js +37 -0
  39. package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
  40. package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
  41. package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
  42. package/campaign-spec/dist/rules/funnel-identity.js +57 -0
  43. package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
  44. package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
  45. package/campaign-spec/dist/rules/index.d.ts +69 -0
  46. package/campaign-spec/dist/rules/index.js +131 -0
  47. package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
  48. package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
  49. package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
  50. package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
  51. package/campaign-spec/dist/rules/page-count.d.ts +11 -0
  52. package/campaign-spec/dist/rules/page-count.js +31 -0
  53. package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
  54. package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
  55. package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
  56. package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
  57. package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
  58. package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
  59. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
  60. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
  61. package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
  62. package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
  63. package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
  64. package/campaign-spec/dist/rules/schema-version.js +53 -0
  65. package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
  66. package/campaign-spec/dist/rules/sdk-version.js +96 -0
  67. package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
  68. package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
  69. package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
  70. package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
  71. package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
  72. package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
  73. package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
  74. package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
  75. package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
  76. package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
  77. package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
  78. package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
  79. package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
  80. package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
  81. package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
  82. package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
  83. package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
  84. package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
  85. package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
  86. package/campaign-spec/dist/sdk-version-parse.js +62 -0
  87. package/campaign-spec/dist/types.d.ts +674 -0
  88. package/campaign-spec/dist/types.js +40 -0
  89. package/campaign-spec/package.json +12 -0
  90. package/compatibility.json +25 -0
  91. package/contracts/agent-relevant-change-policy.v1.json +111 -0
  92. package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
  93. package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
  94. package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
  95. package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
  96. package/contracts/commerce-surface-catalog.json +2452 -0
  97. package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
  98. package/contracts/fixtures/orientation/envelope/current.json +95 -0
  99. package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
  100. package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
  101. package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
  102. package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
  103. package/contracts/fixtures/orientation/envelope/refused.json +100 -0
  104. package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
  105. package/contracts/fixtures/orientation/envelope/updated.json +135 -0
  106. package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
  107. package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
  108. package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
  109. package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
  110. package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
  111. package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
  112. package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
  113. package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
  114. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
  115. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
  116. package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
  117. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
  118. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
  119. package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
  120. package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
  121. package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
  122. package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
  123. package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
  124. package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
  125. package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
  126. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
  127. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
  128. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
  129. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
  130. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
  131. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
  132. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
  133. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
  134. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
  135. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
  136. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
  137. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
  138. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
  139. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
  140. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
  141. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
  142. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
  143. package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
  144. package/contracts/migration-sidecar-bundle.v0.json +149 -0
  145. package/contracts/orientation-limits.v1.json +41 -0
  146. package/contracts/orientation-reason-codes.v1.json +196 -0
  147. package/contracts/private-template-sources.json +8 -0
  148. package/contracts/release-ledger.json +4598 -0
  149. package/contracts/reserved-skill-names.json +13 -0
  150. package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
  151. package/contracts/supported-surface.json +180 -0
  152. package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
  153. package/contracts/template-brand-contract.apollo.v0.json +27 -0
  154. package/contracts/template-brand-contract.demeter.v0.json +27 -0
  155. package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
  156. package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
  157. package/contracts/template-brand-contract.olympus.v0.json +27 -0
  158. package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
  159. package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
  160. package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
  161. package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
  162. package/contracts/template-slot-manifest.apollo.v0.json +12 -0
  163. package/contracts/template-slot-manifest.demeter.v0.json +30 -0
  164. package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
  165. package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
  166. package/contracts/template-slot-manifest.olympus.v0.json +12 -0
  167. package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
  168. package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
  169. package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
  170. package/docs/brand-theme-bridge.md +159 -0
  171. package/docs/build-packet.md +1300 -0
  172. package/docs/campaign-build-brief.md +145 -0
  173. package/docs/campaign-standardization-report.md +329 -0
  174. package/docs/campaigns-os-build-flow.md +117 -0
  175. package/docs/design-source-package.md +784 -0
  176. package/docs/legacy-migration.md +58 -0
  177. package/docs/migration-sidecar-bundle.md +139 -0
  178. package/docs/orientation-contract-reference.md +1220 -0
  179. package/docs/polish-evidence.md +502 -0
  180. package/docs/qa-and-test-orders.md +1691 -0
  181. package/docs/release-ledger-authoring-guide.md +274 -0
  182. package/docs/runtime-readiness.md +211 -0
  183. package/docs/supported-surface.md +83 -0
  184. package/docs/versioning.md +55 -0
  185. package/docs/workflow-findings-sidecar.md +588 -0
  186. package/package.json +135 -0
  187. package/prompts/first-build.md +27 -0
  188. package/prompts/friction-log.md +30 -0
  189. package/schemas/campaign-build-brief.v1.schema.json +149 -0
  190. package/schemas/campaign-design-source-package.v0.schema.json +697 -0
  191. package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
  192. package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
  193. package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
  194. package/schemas/campaign-spec.v4.schema.json +582 -0
  195. package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
  196. package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
  197. package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
  198. package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
  199. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
  200. package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
  201. package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
  202. package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
  203. package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
  204. package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
  205. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
  206. package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
  207. package/schemas/source-html-manifest.v0.schema.json +252 -0
  208. package/skills/next-campaigns-build/SKILL.md +73 -0
  209. package/skills/next-campaigns-os/SKILL.md +101 -0
  210. package/skills/next-campaigns-os/references/session-intake.md +160 -0
  211. package/skills/next-campaigns-os-setup/SKILL.md +22 -0
  212. package/skills/next-campaigns-polish/SKILL.md +145 -0
  213. package/skills/next-campaigns-qa/SKILL.md +92 -0
  214. package/skills.json +56 -0
  215. package/skills.sh +64 -0
  216. package/src/adapter-decision-contract.mjs +333 -0
  217. package/src/brand-theme.mjs +1151 -0
  218. package/src/browser-launch.mjs +79 -0
  219. package/src/build-brief.mjs +781 -0
  220. package/src/built-site-scope.mjs +312 -0
  221. package/src/campaign-ecosystem.mjs +734 -0
  222. package/src/campaign-identity.mjs +405 -0
  223. package/src/campaign-workspace.mjs +127 -0
  224. package/src/checkpoint-waiver.mjs +302 -0
  225. package/src/cli.mjs +13442 -0
  226. package/src/commercial-journey.mjs +1119 -0
  227. package/src/commercial-parity.mjs +965 -0
  228. package/src/consent.mjs +347 -0
  229. package/src/content-residue.mjs +322 -0
  230. package/src/deadline.mjs +81 -0
  231. package/src/design-source-package.mjs +2604 -0
  232. package/src/deviation.mjs +107 -0
  233. package/src/doctor-check-registry.mjs +49 -0
  234. package/src/doctor-sidecar.mjs +106 -0
  235. package/src/finding-cause.mjs +557 -0
  236. package/src/findings.mjs +326 -0
  237. package/src/fs-identity.mjs +68 -0
  238. package/src/gate-actions.mjs +105 -0
  239. package/src/html-scan.mjs +53 -0
  240. package/src/install-mode.mjs +272 -0
  241. package/src/legacy-migration.d.ts +128 -0
  242. package/src/legacy-migration.mjs +510 -0
  243. package/src/lifecycle.mjs +338 -0
  244. package/src/local-proof.mjs +401 -0
  245. package/src/map-pin-writeback.mjs +210 -0
  246. package/src/orchestration-stage-contract.mjs +81 -0
  247. package/src/package-install-fixture.mjs +33 -0
  248. package/src/page-kit-build-summary.mjs +175 -0
  249. package/src/page-kit-campaign-config.mjs +57 -0
  250. package/src/page-kit-sdk-version.mjs +392 -0
  251. package/src/page-kit-store-profile.mjs +369 -0
  252. package/src/page-kit-sync.mjs +162 -0
  253. package/src/polish-browser.mjs +867 -0
  254. package/src/polish-capture.mjs +1094 -0
  255. package/src/polish-deadline.mjs +51 -0
  256. package/src/polish-gate.mjs +739 -0
  257. package/src/polish-node.mjs +639 -0
  258. package/src/polish-page-load.mjs +1100 -0
  259. package/src/private-template-source.mjs +237 -0
  260. package/src/proof-policy.mjs +82 -0
  261. package/src/qa-analytics-correctness.mjs +307 -0
  262. package/src/qa-analytics-errors.mjs +38 -0
  263. package/src/qa-analytics-parity.mjs +699 -0
  264. package/src/qa-binding-evidence.mjs +140 -0
  265. package/src/qa-browser.mjs +6608 -0
  266. package/src/qa-cart-entry.mjs +406 -0
  267. package/src/qa-commercial-parity.mjs +641 -0
  268. package/src/qa-node.mjs +3620 -0
  269. package/src/qa-order-bump.mjs +381 -0
  270. package/src/qa-parity-capture.mjs +428 -0
  271. package/src/qa-parity-fixture.mjs +359 -0
  272. package/src/qa-publish.mjs +362 -0
  273. package/src/qa-purchase-data-layer.mjs +263 -0
  274. package/src/qa-route-probe.mjs +272 -0
  275. package/src/qa-sidecar.mjs +188 -0
  276. package/src/qa-test-order-topology.mjs +207 -0
  277. package/src/qa-url-privacy.mjs +13 -0
  278. package/src/qa-verdict-discovery.mjs +192 -0
  279. package/src/qa-verdict-publish.mjs +105 -0
  280. package/src/qa-verdict.mjs +287 -0
  281. package/src/remit.mjs +388 -0
  282. package/src/repo-scan.mjs +83 -0
  283. package/src/route-identity.mjs +133 -0
  284. package/src/run-record-closeout.mjs +229 -0
  285. package/src/run-record.mjs +839 -0
  286. package/src/run-session.mjs +226 -0
  287. package/src/runtime-state-ignore.mjs +113 -0
  288. package/src/sdk-attribute-index.mjs +212 -0
  289. package/src/sdk-markup.mjs +358 -0
  290. package/src/sdk-meta-tags.mjs +52 -0
  291. package/src/shell-token.mjs +7 -0
  292. package/src/sidecar-bundle.mjs +399 -0
  293. package/src/source-asset-crawl.mjs +469 -0
  294. package/src/source-html-intake.mjs +627 -0
  295. package/src/source-html-manifest.mjs +276 -0
  296. package/src/source-prep.mjs +284 -0
  297. package/src/spec-derive-store.mjs +431 -0
  298. package/src/spec-derive.mjs +514 -0
  299. package/src/spec-fetch.mjs +66 -0
  300. package/src/spec-hash.mjs +48 -0
  301. package/src/spec-identity.mjs +27 -0
  302. package/src/stage-ledger.mjs +509 -0
  303. package/src/standardization-report.mjs +1297 -0
  304. package/src/template-brand-contract.mjs +473 -0
  305. package/src/template-freshness.mjs +196 -0
  306. package/src/template-reference.mjs +81 -0
  307. package/src/template-slot-manifest.mjs +150 -0
  308. package/src/text-safety.mjs +62 -0
  309. package/src/theme-gate.mjs +185 -0
  310. package/src/upsell-selector-scope.mjs +299 -0
@@ -0,0 +1,674 @@
1
+ /**
2
+ * Type surface for the CampaignSpec contract layer.
3
+ *
4
+ * The CampaignSpec is the central authoring contract of this repo (see
5
+ * ../CONTEXT.md). v4.3 is the authoring shape; v4.2 funnels[] is the canonical
6
+ * internal shape that rules operate on (after normalize()).
7
+ *
8
+ * Types here are intentionally permissive on optional fields — the rule
9
+ * registry catches missing/malformed fields, rather than the type system
10
+ * refusing to compile against in-progress Map Builder drafts.
11
+ */
12
+ export type Severity = 'error' | 'warning';
13
+ export type Tag = 'fast' | 'structure' | 'references' | 'meta-tags' | 'tracking' | 'spec-only' | 'requires-complete-spec';
14
+ /**
15
+ * A rule's structured output. One shape across every consumer (Map Builder
16
+ * field-level UI, QA verdict, CLI output, compiler exceptions).
17
+ *
18
+ * `path` is a JSON Pointer locator into the normalized spec, e.g.
19
+ * "/funnels/0/pages/2/route". Enables field-level UI without rule-specific
20
+ * wiring.
21
+ *
22
+ * `data` carries rule-specific structured detail (e.g. CycleDetection
23
+ * includes the offending cycle's page IDs).
24
+ */
25
+ export interface Violation {
26
+ ruleId: string;
27
+ severity: Severity;
28
+ message: string;
29
+ path: string;
30
+ data?: Record<string, unknown>;
31
+ }
32
+ /**
33
+ * The unit of composition for spec validation.
34
+ *
35
+ * Pure: takes only a normalized CampaignSpec, returns Violations. No context
36
+ * bag, no live data dependency. Mode flags become tag filters at the call
37
+ * site; rule parameters bind at registration time.
38
+ *
39
+ * `severity` is the rule's default. Individual violations can override
40
+ * (e.g. CycleDetection emits warnings for self-loops, errors for multi-page
41
+ * cycles).
42
+ */
43
+ export interface Rule {
44
+ id: string;
45
+ severity: Severity;
46
+ tags: Tag[];
47
+ check(spec: CampaignSpec): Violation[];
48
+ }
49
+ /** A `Rule[]`. Compose with plain array operations. */
50
+ export type RuleSet = Rule[];
51
+ export type PageType = 'presell' | 'landing'
52
+ /**
53
+ * The bundle-selection step of a two-step family: a template-owned commerce
54
+ * page where the shopper picks a package before checkout. Distinct from
55
+ * 'landing' — a landing page is design-source-owned and carries no SDK cart
56
+ * selection, so commerce gates (brand-theme load order, logo and
57
+ * computed-style residue, bump pricing) apply to 'select' and not to
58
+ * 'landing'. Routes forward like a landing page, through `next_page`: it is
59
+ * the first step of a two-step checkout flow, and it neither takes payment
60
+ * nor presents an accept/decline offer, so a `success_url` or `on_accept`
61
+ * here is ignored for routing (campaigns-os#234).
62
+ *
63
+ * Not to be confused with the page ID "select", which several family
64
+ * contracts use for this page and which is a separate namespace.
65
+ */
66
+ | 'select' | 'checkout' | 'upsell' | 'downsell' | 'thankyou';
67
+ /**
68
+ * Offer condition/benefit blocks, as exported by the Map Builder. Both share
69
+ * the { type, value, description } shape (e.g. condition
70
+ * `{ type: 'count', value: 2 }`, benefit
71
+ * `{ type: 'package_percentage', value: '20.00' }`).
72
+ */
73
+ export interface OfferCondition {
74
+ type?: string;
75
+ value?: number | string | null;
76
+ description?: string | null;
77
+ [key: string]: unknown;
78
+ }
79
+ export interface OfferBenefit {
80
+ type?: string;
81
+ value?: number | string | null;
82
+ description?: string | null;
83
+ [key: string]: unknown;
84
+ }
85
+ export interface PageOffer {
86
+ ref_id: string | number;
87
+ code?: string | null;
88
+ name?: string | null;
89
+ type?: string;
90
+ condition?: OfferCondition;
91
+ benefit?: OfferBenefit;
92
+ [key: string]: unknown;
93
+ }
94
+ export interface PagePackage {
95
+ ref_id?: string | number;
96
+ /**
97
+ * Package quantity. The real export key is `qty` (206/211 real package
98
+ * entries) — NOT `quantity`, which never appears in exports.
99
+ */
100
+ qty?: number;
101
+ name?: string | null;
102
+ price?: number | string | null;
103
+ price_retail?: number | string | null;
104
+ product_name?: string | null;
105
+ product_sku?: string | null;
106
+ product_variant_name?: string | null;
107
+ product_purchase_availability?: string | null;
108
+ product_inventory_availability?: string | null;
109
+ image?: string | null;
110
+ /**
111
+ * Boolean role trio. There is no `role` field in real exports — a
112
+ * package's role is expressed by is_upsell / is_order_bump /
113
+ * default_selected flags (absent means a main package).
114
+ */
115
+ is_upsell?: boolean;
116
+ is_order_bump?: boolean;
117
+ default_selected?: boolean;
118
+ variant_attributes?: Array<{
119
+ code?: string;
120
+ name?: string;
121
+ value?: string;
122
+ [key: string]: unknown;
123
+ }>;
124
+ is_recurring?: boolean;
125
+ price_recurring?: number | string | null;
126
+ interval?: string | null;
127
+ interval_count?: number | null;
128
+ [key: string]: unknown;
129
+ }
130
+ export interface ExitIntent {
131
+ enabled?: boolean;
132
+ offer_ref_id?: string | number;
133
+ offer_code?: string;
134
+ [key: string]: unknown;
135
+ }
136
+ export interface PromoCodeInput {
137
+ enabled?: boolean;
138
+ mode?: 'mapped_offer' | string;
139
+ offer_ref_id?: string | number;
140
+ offer_code?: string;
141
+ [key: string]: unknown;
142
+ }
143
+ /**
144
+ * Pointer to the design artifact that supplies the prepared HTML for this
145
+ * page. Read by figma-sections-export (and future design-tool exporters) to
146
+ * locate the source frames; read by campaigns-os doctor to decide whether a
147
+ * missing source-html manifest is "designer hasn't exported yet" (blocker
148
+ * with run-export-first guidance) vs. plain `collect-inputs`.
149
+ *
150
+ * Today `figma` is the only supported `type`; the field is structured this
151
+ * way so future tools (Penpot, Sketch, hand-authored HTML, AI-generated)
152
+ * slot in without a schema break.
153
+ *
154
+ * `file_url` is the design-tool file URL (canonical identity of the file).
155
+ * `breakpoints` carry per-viewport pointers — for Figma, these are selection
156
+ * URLs that already encode node IDs, which is what designers copy via
157
+ * "Copy link to selection". Empty/missing breakpoints are valid during
158
+ * draft authoring; rules surface incompleteness as warnings, not errors.
159
+ */
160
+ export interface DesignSourceBreakpoints {
161
+ desktop?: string;
162
+ tablet?: string;
163
+ mobile?: string;
164
+ [key: string]: unknown;
165
+ }
166
+ export interface DesignSource {
167
+ type: 'figma' | string;
168
+ file_url: string;
169
+ breakpoints?: DesignSourceBreakpoints;
170
+ notes?: string;
171
+ [key: string]: unknown;
172
+ }
173
+ /**
174
+ * Per-page hint declaring which UI variant the build should render this
175
+ * page as. Today only upsell pages have meaningful variants: the
176
+ * olympus-mv-single-step family ships with `mv` (multi-quantity tier
177
+ * pills), `bundle_tier_pills`, `bundle_tier_cards`, and a `single`
178
+ * fallback, but the spec previously carried no way to declare which one
179
+ * each OTO should use, forcing per-page decisions at build time.
180
+ *
181
+ * Like preferred_template_family, this is a HINT — the build agent
182
+ * uses it as the default when no per-page override is given. CLI args
183
+ * and operator overrides win.
184
+ *
185
+ * Open-string at the type level so future template patterns slot in
186
+ * without a schema break; validation rule narrows to the known set.
187
+ */
188
+ export type UpsellTemplatePattern = 'mv' | 'bundle_tier_pills' | 'bundle_tier_cards' | 'single' | (string & {});
189
+ /**
190
+ * Per-page MV upsell tier range. Pairs with `upsell_template_pattern: 'mv'`
191
+ * (or any tier-based variant) to declare the inclusive `{min, max}`
192
+ * quantity-tier range the page should render.
193
+ *
194
+ * Slice 4b context: the olympus-mv-single-step family ships with a fixed
195
+ * pill ladder but the spec previously carried no way to declare which
196
+ * subset each OTO should render.
197
+ * Author-time: "Upsell 1 ranges 1-5, Upsell 2 ranges 2-4". Build agent
198
+ * reads the range to scope the pill set; the source HTML's static
199
+ * markup is overridden when the hint disagrees.
200
+ *
201
+ * HINT semantics match the other authoring-time fields: validation
202
+ * warns when shape is malformed or `min > max`, but never blocks a
203
+ * build. Hand-authored or operator-supplied tier counts at build time
204
+ * still win.
205
+ */
206
+ export interface UpsellMvTiers {
207
+ min: number;
208
+ max: number;
209
+ [key: string]: unknown;
210
+ }
211
+ /**
212
+ * Per-page MV upsell variant column labels. Used by template families
213
+ * that render multi-attribute variant tables (olympus-mv-single-step
214
+ * tier-cards) where columns map to product attributes like size,
215
+ * color, or flavor. The starter HTML often assumes two columns —
216
+ * single-attribute products (size-only, color-only) end up with an
217
+ * empty second column.
218
+ *
219
+ * Slice 4e context: declare `{primary: "Size"}` and the build drops
220
+ * the second column; declare `{primary: "Size", secondary: "Color"}`
221
+ * and both columns render with the spec-declared labels. HINT
222
+ * semantics: warning-severity validation, never blocks a build,
223
+ * CLI/operator overrides at build time win.
224
+ */
225
+ export interface VariantLabels {
226
+ primary: string;
227
+ secondary?: string;
228
+ [key: string]: unknown;
229
+ }
230
+ /**
231
+ * Per-funnel promo-code roster. Replaces the hardcoded `sales` array
232
+ * in the starter templates' promo-banner.js / promo-timer.js so each
233
+ * merchant carries their own seasonal calendar in the spec rather
234
+ * than inheriting demo defaults.
235
+ *
236
+ * Slice 4c context: every campaign ships with the demo SUMMER26 /
237
+ * BF26 / etc. codes burned into promo-banner.js source. The
238
+ * build-side replacement step (next-campaigns-build skill addendum)
239
+ * reads `funnels[].promo_codes` and regenerates the sales array in
240
+ * the assembled JS.
241
+ *
242
+ * Per-funnel scope: A/B funnels can run different rosters. `id` and
243
+ * `code` are required identity; visual presentation fields are
244
+ * optional and mirror the existing template shape so the build can
245
+ * do a clean array replace. `starts_at` / `ends_at` are ISO date
246
+ * strings — missing means "active whenever selected." Array order
247
+ * decides priority (first matching date range wins).
248
+ */
249
+ export interface PromoCode {
250
+ id: string;
251
+ code: string;
252
+ starts_at?: string;
253
+ ends_at?: string;
254
+ title?: string;
255
+ emoji?: string;
256
+ offer1?: string;
257
+ offer2?: string;
258
+ top_bar_bg?: string;
259
+ highlight_color?: string;
260
+ banner_text?: string;
261
+ banner_text_sec?: string;
262
+ limited_time?: string;
263
+ [key: string]: unknown;
264
+ }
265
+ /**
266
+ * SDK-layer page type projection. `thankyou` is the canonical terminal
267
+ * page.type; `receipt` is its SDK projection and lives HERE (and in
268
+ * meta_tags["next-page-type"]), never in page.type. Observed values across
269
+ * the real corpus: product, checkout, upsell, receipt.
270
+ */
271
+ export type SdkPageType = 'product' | 'checkout' | 'upsell' | 'receipt';
272
+ /**
273
+ * Per-page SDK hints. Real export subkeys are `sdk_page_type` + `meta_tags`
274
+ * ONLY. `frontmatter` and `template_family` are template-handoff extensions
275
+ * carried by the contracts/fixtures/campaign-specs agent-contract fixtures
276
+ * (and blessed as optional extensions by schemas/campaign-spec.v4.schema.json);
277
+ * they are not Map Builder export fields.
278
+ */
279
+ export interface SdkHints {
280
+ sdk_page_type?: SdkPageType;
281
+ meta_tags?: Record<string, string>;
282
+ frontmatter?: Record<string, unknown>;
283
+ template_family?: string;
284
+ [key: string]: unknown;
285
+ }
286
+ /**
287
+ * DERIVED routing projection (present on 135/195 real pages). The declared
288
+ * routing fields (next_page / on_accept / on_decline / success_url) hold
289
+ * PAGE IDS in every real occurrence; this object is where the corresponding
290
+ * route paths (`*_route`, trailing-slash) and filenames (`*_filename`,
291
+ * route path or legacy .html) live.
292
+ */
293
+ export interface ResolvedRouting {
294
+ accept?: string;
295
+ decline?: string;
296
+ success?: string;
297
+ next_page?: string;
298
+ accept_route?: string;
299
+ decline_route?: string;
300
+ success_route?: string;
301
+ next_page_route?: string;
302
+ accept_filename?: string;
303
+ decline_filename?: string;
304
+ success_filename?: string;
305
+ next_page_filename?: string;
306
+ [key: string]: unknown;
307
+ }
308
+ /** Per-page design pointers (93/195 real pages). */
309
+ export interface DesignHooks {
310
+ figma_frame_url?: string | null;
311
+ component_slots?: unknown;
312
+ [key: string]: unknown;
313
+ }
314
+ export interface Page {
315
+ id: string;
316
+ type: PageType;
317
+ label?: string;
318
+ order?: number;
319
+ /** True on funnel entry pages (178/195 real pages carry the flag). */
320
+ is_entry?: boolean;
321
+ /**
322
+ * Authored route field: usually a trailing-slash route path
323
+ * ("checkout/"), occasionally empty, a bare token, or a legacy .html
324
+ * filename. This — not the declared routing fields — is where authored
325
+ * route paths live.
326
+ */
327
+ page_url?: string;
328
+ next_page?: string;
329
+ success_url?: string;
330
+ on_accept?: string;
331
+ on_decline?: string;
332
+ resolved_routing?: ResolvedRouting;
333
+ packages?: PagePackage[];
334
+ offers?: PageOffer[];
335
+ exit_intent?: ExitIntent;
336
+ promo_code_input?: PromoCodeInput;
337
+ sdk_hints?: SdkHints;
338
+ design_hooks?: DesignHooks;
339
+ design_source?: DesignSource;
340
+ /**
341
+ * Per-page UI variant hint. Meaningful only on upsell-type pages
342
+ * today; on non-upsell pages the validation rule warns.
343
+ */
344
+ upsell_template_pattern?: UpsellTemplatePattern;
345
+ /**
346
+ * Per-page MV upsell tier range. Pairs with the `mv` UI pattern (or any
347
+ * tier-based variant) to declare the inclusive `{min, max}` quantity-tier
348
+ * range the build should render. Meaningful on upsell pages; validation
349
+ * warns when set elsewhere or when shape is malformed.
350
+ */
351
+ upsell_mv_tiers?: UpsellMvTiers;
352
+ /**
353
+ * Per-page MV upsell variant column labels (Slice 4e). Meaningful
354
+ * only on upsell pages today; validation warns when set elsewhere
355
+ * or when primary is missing/empty.
356
+ */
357
+ variant_labels?: VariantLabels;
358
+ [key: string]: unknown;
359
+ }
360
+ export interface Funnel {
361
+ id: string;
362
+ name?: string;
363
+ hypothesis?: string;
364
+ weight?: number;
365
+ pages?: Page[];
366
+ /**
367
+ * Per-funnel promo-code roster (Slice 4c). The build-side
368
+ * replacement step regenerates promo-banner.js / promo-timer.js
369
+ * sales arrays from this list when present.
370
+ */
371
+ promo_codes?: PromoCode[];
372
+ [key: string]: unknown;
373
+ }
374
+ /** A package nested under a root offer (package_id identity). */
375
+ export interface OfferPackage {
376
+ package_id?: number | string;
377
+ package_name?: string | null;
378
+ package_image?: string | null;
379
+ product_name?: string | null;
380
+ product_variant_name?: string | null;
381
+ unit_price?: number | string | null;
382
+ unit_price_before_discount?: number | string | null;
383
+ package_price?: number | string | null;
384
+ package_price_before_discount?: number | string | null;
385
+ package_unit_qty?: number | null;
386
+ [key: string]: unknown;
387
+ }
388
+ /**
389
+ * Root offer catalog entry. `ref_id` is the SOLE offer identity — all 236
390
+ * real offer occurrences use ref_id (+ code, often null, + name). A
391
+ * `package_ref_id` key never appears in real exports and is not part of
392
+ * the contract; offer→package links are the nested `packages[]` with
393
+ * package_id identity.
394
+ */
395
+ export interface Offer {
396
+ ref_id: string | number;
397
+ code?: string | null;
398
+ name?: string | null;
399
+ type?: string;
400
+ condition?: OfferCondition;
401
+ benefit?: OfferBenefit;
402
+ packages?: OfferPackage[];
403
+ /** Offer-scoped shipping method pricing. */
404
+ shipping_methods?: unknown[];
405
+ [key: string]: unknown;
406
+ }
407
+ /**
408
+ * Optional hint declaring which starter template family the campaign was
409
+ * authored against. Doctrine: template family is a build-time decision,
410
+ * not a spec-time decision; this field is a HINT that the build agent
411
+ * uses as the default when no `--template-family` CLI override is given.
412
+ *
413
+ * Pre-Slice 4a, this field already existed as an undocumented convention
414
+ * read by campaigns-os/src/cli.mjs preferredTemplateFamily(); Slice 4a
415
+ * blesses it in the schema, adds Map Builder UI to author it, and adds
416
+ * a validation rule that warns if the value isn't a recognized family.
417
+ *
418
+ * Operators / agents always retain veto power via `--template-family`;
419
+ * the hint never silently locks the build.
420
+ *
421
+ * Known families track the certified Campaigns OS commerce-surface catalog.
422
+ * The AssemblyHintsShape test pins this list to the vendored catalog so a
423
+ * newly certified family cannot start producing a false unknown-family
424
+ * warning.
425
+ */
426
+ export declare const KNOWN_TEMPLATE_FAMILY_HINTS: readonly ["apollo", "apollo-mv-single-step", "demeter", "olympus", "olympus-mv-single-step", "olympus-mv-two-step", "shop-single-step", "shop-three-step"];
427
+ export type TemplateFamilyHint = typeof KNOWN_TEMPLATE_FAMILY_HINTS[number] | (string & {});
428
+ /**
429
+ * One entry of the object-shaped available_shipping_countries variant
430
+ * (`{ code, label }` pairs — 5/33 real specs).
431
+ */
432
+ export interface ShippingCountry {
433
+ code?: string;
434
+ label?: string;
435
+ [key: string]: unknown;
436
+ }
437
+ export interface Campaign {
438
+ ref_id?: number | string;
439
+ /** Campaign slug-style identifier (28/33 real specs), distinct from ref_id. */
440
+ id?: string;
441
+ name?: string;
442
+ slug?: string;
443
+ currency?: string;
444
+ language?: string;
445
+ /** Public-by-design, domain-allowlisted Campaigns API key. */
446
+ campaigns_api_key?: string | null;
447
+ available_payment_methods?: unknown[];
448
+ available_express_payment_methods?: unknown[];
449
+ available_currencies?: string[];
450
+ /**
451
+ * Public route root the campaign is served under. `'/'` declares a
452
+ * ROOT-SERVED campaign: the whole funnel lives at site-root paths
453
+ * (`/checkout-v2`, `/receipt`) with no slug prefix — e.g. a single-campaign
454
+ * site whose deploy publishes the funnel at the domain root. When absent,
455
+ * consumers default to `'/<slug>/'`. `slug` stays required identity either
456
+ * way; route_root only changes how public routes and SDK routing metas are
457
+ * composed and validated (campaigns-os doctor honors it).
458
+ */
459
+ route_root?: string;
460
+ payment_env_key?: string;
461
+ /**
462
+ * Arrives in THREE incompatible shapes in real exports: bare string
463
+ * "all", an array of country-code strings (incl. []), and an array of
464
+ * { code, label } objects. All three are accepted; normalization to one
465
+ * canonical shape is deliberately deferred.
466
+ */
467
+ available_shipping_countries?: 'all' | string[] | ShippingCountry[];
468
+ tracking?: Record<string, unknown>;
469
+ preferred_template_family?: TemplateFamilyHint;
470
+ store_url?: string | null;
471
+ store_name?: string | null;
472
+ store_contact?: string | null;
473
+ store_terms?: string | null;
474
+ store_shipping?: string | null;
475
+ store_privacy?: string | null;
476
+ store_returns?: string | null;
477
+ store_phone?: string | null;
478
+ /**
479
+ * Domain allowlist for the SDK / Campaigns API key (Slice 4f). The
480
+ * Campaigns API treats domain allowlisting as the access boundary for
481
+ * public-by-design keys; carrying the allowlist in the spec lets the
482
+ * build packet bind config.js to the same surface. Empty/missing
483
+ * value is a warning, not an error — pre-launch specs frequently
484
+ * lack a final domain.
485
+ */
486
+ allowed_domains?: string[];
487
+ /**
488
+ * `tel:`-prefixed phone URI for "Call us" CTAs (Slice 4f). Distinct
489
+ * from store_phone (the human-readable display string); store_phone_tel
490
+ * goes into <a href="tel:..."> attributes. Validation warns when the
491
+ * value is present but doesn't start with `tel:`.
492
+ */
493
+ store_phone_tel?: string;
494
+ [key: string]: unknown;
495
+ }
496
+ /**
497
+ * Analytics & attribution contract (Slice 4g) — what a campaign's analytics,
498
+ * tag-management, and querystring-param tracking are SUPPOSED to be, so doctor
499
+ * + QA can validate them instead of discovering gaps in QA (cf. the Chamelo
500
+ * Shield `?reviews=n`-has-no-handler finding and the Walla Sound Redtrack/
501
+ * campaign.js sub1-6 param conflict).
502
+ *
503
+ * Modeled on real production-funnel usage, NOT the idealized "SDK fires the
504
+ * canonical dl_* set" view. The field reality the block must express:
505
+ * - Events fire from three sources: SDK auto, SDK-blocked-then-manual
506
+ * (`blockedEvents` suppresses the SDK event, a side script re-fires it via
507
+ * raw fbq/gtag to control timing), and fully out-of-band pixels loaded via
508
+ * GTM (Everflow / TriplePixel / Northbeam / RudderStack) that never touch
509
+ * the dataLayer. So outbound pixel fires — not dataLayer events — are the
510
+ * QA source of truth.
511
+ * - A custom provider may carry an endpoint + transform (cookie injection).
512
+ * - Querystring params split into two classes: CONTENT (param.* → visibility
513
+ * via data-next-hide) and TRACKING (utm params, gclid, fbclid, subN, click-ids that
514
+ * must be preserved across funnel steps and not collide with ad trackers).
515
+ *
516
+ * The whole block is OPTIONAL — absent analytics means "use SDK defaults",
517
+ * exactly as today. When present it becomes the source of truth for the
518
+ * AnalyticsContractShape rule + downstream doctor/QA.
519
+ */
520
+ export type AnalyticsMode = 'auto' | 'manual' | 'disabled';
521
+ export interface AnalyticsProvider {
522
+ enabled?: boolean;
523
+ /** GTM container id (kind: gtm). */
524
+ containerId?: string;
525
+ /** Meta/Facebook pixel id (kind: facebook). */
526
+ pixelId?: string;
527
+ /** Custom-provider HTTP endpoint (kind: custom). */
528
+ endpoint?: string;
529
+ /** Custom-provider transform hint, e.g. a cookie name to inject (`cf_click_id`). */
530
+ transform?: string;
531
+ /** Events the SDK must NOT fire for this provider (a side script fires them). */
532
+ blockedEvents?: string[];
533
+ [key: string]: unknown;
534
+ }
535
+ /** A pixel/tag fired OUTSIDE the SDK (via GTM or a raw snippet) — QA must still
536
+ * expect it on a live run even though it never appears in the dataLayer. */
537
+ export interface OutOfBandPixel {
538
+ vendor: string;
539
+ loaded_via?: 'gtm' | 'script' | (string & {});
540
+ id?: string;
541
+ }
542
+ /** An event the SDK is configured NOT to fire (`blockedEvents`), declared with
543
+ * where + when a side script fires it instead. A `purchase`/`Purchase` manual
544
+ * event SHOULD name the page it lives on — the first-upsell placement footgun
545
+ * (async purchase beacons get lost in the checkout→upsell redirect). */
546
+ export interface ManualEvent {
547
+ event: string;
548
+ page?: string;
549
+ trigger?: string;
550
+ }
551
+ /** A content param that drives visibility via `data-next-hide="param.X=='n'"`. */
552
+ export interface ContentParam {
553
+ name: string;
554
+ hides?: string;
555
+ pages?: string[];
556
+ }
557
+ export interface TrackingParams {
558
+ /** Params captured + preserved across funnel steps (utm_*, gclid, fbclid, sub1..5). */
559
+ preserve?: string[];
560
+ /** Funnel step page-types/ids the params must survive across. */
561
+ across?: string[];
562
+ /** The affiliate click id: inbound querystring param → SDK attribution field. */
563
+ click_id?: {
564
+ inbound?: string;
565
+ maps_to?: string;
566
+ };
567
+ /** External ad trackers sharing the URL (Redtrack/Clickflare) — collision watch. */
568
+ external_trackers?: string[];
569
+ }
570
+ export interface AnalyticsParams {
571
+ content?: ContentParam[];
572
+ tracking?: TrackingParams;
573
+ }
574
+ export interface UtmTransfer {
575
+ enabled?: boolean;
576
+ applyToExternalLinks?: boolean;
577
+ paramsToCopy?: string[];
578
+ excludedDomains?: string[];
579
+ }
580
+ export interface AnalyticsContract {
581
+ mode?: AnalyticsMode;
582
+ /** Keyed by provider kind: gtm | facebook | rudderstack | custom | … */
583
+ providers?: Record<string, AnalyticsProvider>;
584
+ out_of_band_pixels?: OutOfBandPixel[];
585
+ manual_events?: ManualEvent[];
586
+ utmTransfer?: UtmTransfer;
587
+ params?: AnalyticsParams;
588
+ [key: string]: unknown;
589
+ }
590
+ /**
591
+ * Saved Map Builder identity block (26/33 real specs). `source` values
592
+ * observed: campaign-map-builder, hand-authored-simulation,
593
+ * local-experimental.
594
+ */
595
+ export interface SpecIdentity {
596
+ map_id?: string;
597
+ source?: string;
598
+ id?: string;
599
+ map_url?: string;
600
+ edit_url?: string;
601
+ spec_url?: string;
602
+ spec_hash?: string;
603
+ saved_at?: string;
604
+ public_route_slug?: string;
605
+ variant_slug?: string;
606
+ template_family?: string;
607
+ derived_from?: string;
608
+ authority?: string;
609
+ [key: string]: unknown;
610
+ }
611
+ /**
612
+ * funnel_pages[] mirror entry: a Page plus the _funnel_id/_funnel_name
613
+ * annotations linking it back to its funnel. The mirror is a flattened
614
+ * LEGACY projection of funnels[].pages[] (declared "legacy" by
615
+ * _provenance); funnels[] is authoritative.
616
+ */
617
+ export interface FunnelPageMirrorEntry extends Page {
618
+ _funnel_id?: string;
619
+ _funnel_name?: string;
620
+ }
621
+ /**
622
+ * Field-ownership declaration (27/33 real specs): which JSON paths are
623
+ * ops-authored vs api-owned vs derived vs legacy mirrors.
624
+ */
625
+ export interface SpecProvenance {
626
+ ops?: string[];
627
+ api?: string[];
628
+ derived?: string[];
629
+ legacy?: string[];
630
+ [key: string]: unknown;
631
+ }
632
+ export interface CampaignSpec {
633
+ schema_version?: string;
634
+ builder_version?: string;
635
+ generated_at?: string;
636
+ spec_identity?: SpecIdentity;
637
+ campaign?: Campaign;
638
+ funnels: Funnel[];
639
+ /** Flattened legacy mirror of funnels[].pages[]; funnels[] is authoritative. */
640
+ funnel_pages?: FunnelPageMirrorEntry[];
641
+ offers?: Offer[];
642
+ shipping_methods?: unknown[];
643
+ /**
644
+ * CANONICAL home of the SDK pin for the 4.x lineage (33/33 real specs
645
+ * declare global_config.sdk_version).
646
+ */
647
+ global_config?: {
648
+ sdk_version?: string;
649
+ [key: string]: unknown;
650
+ };
651
+ /** Accepted ALIAS location for sdk_version (local drafts only). */
652
+ runtime?: {
653
+ sdk_version?: string;
654
+ [key: string]: unknown;
655
+ };
656
+ build_scope?: {
657
+ mode?: 'partial' | 'full';
658
+ [key: string]: unknown;
659
+ };
660
+ /** Analytics & attribution contract (optional). See AnalyticsContract. */
661
+ analytics?: AnalyticsContract;
662
+ _provenance?: SpecProvenance;
663
+ /** Flat mirrors of the saved-map identity (7/33 real specs). */
664
+ slug?: string;
665
+ map_id?: string;
666
+ saved_at?: string;
667
+ [key: string]: unknown;
668
+ }
669
+ export interface Fixture {
670
+ spec: CampaignSpec;
671
+ expected: {
672
+ violations: Violation[];
673
+ };
674
+ }