@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,190 @@
1
+ /**
2
+ * routing — the single source of truth for a page's outgoing edges.
3
+ *
4
+ * A campaign is a free-form headless shopping journey. It usually runs
5
+ * landing → checkout → upsells → receipt, but nothing requires that, and the
6
+ * routing fields a page declares are the author's statement of where the
7
+ * shopper goes next. Before this module those fields were interpreted by three
8
+ * independent page-type tables — source intake, cycle detection, and the QA
9
+ * topology extractor — which disagreed with each other and silently discarded
10
+ * any edge declared outside their own table.
11
+ *
12
+ * Everything that asks "where does this page go" now asks here, and every
13
+ * caller models the SAME traversable edges — the forward link the page resolves
14
+ * to, plus its decline branch. They differ only in how many of those they need:
15
+ *
16
+ * forwardRouteTarget() — the ONE forward link, for wiring (build output, QA
17
+ * expectations), which emits a single next URL.
18
+ * outgoingEdgeIds() — forward plus decline, for graph analysis (cycle
19
+ * detection), which must consider both branches.
20
+ *
21
+ * An earlier revision had outgoingEdgeIds return every DECLARED field on the
22
+ * theory that a safety analysis should over-approximate. That was wrong, and
23
+ * cost real correctness: a page whose `success_url` wins at runtime and
24
+ * terminates cleanly was reported as a release-blocking cycle through an unused,
25
+ * shadowed `next_page`. The runtime can only ever take the forward link or the
26
+ * decline branch, so following a field neither of them selects does not find
27
+ * extra bugs — it invents them. Accuracy is the safety property here, not
28
+ * breadth.
29
+ *
30
+ * One narrowing sits on top of that (#234): a field whose meaning the page's
31
+ * type cannot satisfy is not an edge either. `success_url` means "after
32
+ * payment" and `on_accept` means "after accepting this page's offer"; a page
33
+ * with no payment and no offer can satisfy neither, and honouring a
34
+ * copy-pasted one routed real shoppers past the checkout. This is NOT the
35
+ * page-type gate #230 removed — that dropped edges the author declared on
36
+ * tables that disagreed about which fields a type may use. `next_page` stays
37
+ * honoured on every page type. The carve-out is about what two fields MEAN.
38
+ */
39
+ import type { Page } from './types.ts';
40
+ /**
41
+ * Forward-route fields in specific-before-generic precedence. `on_accept` and
42
+ * `success_url` each name a particular branch (upsell accept, order success);
43
+ * `next_page` is the generic "wherever this page goes next", so it loses to
44
+ * either. Order is load-bearing — `forwardRouteTarget` returns the first match
45
+ * — and is pinned by test, not only by this comment.
46
+ *
47
+ * Precedence is not the whole answer. `on_accept` and `success_url` each carry
48
+ * a meaning only some page types can satisfy, and a field its page cannot
49
+ * satisfy is skipped before precedence is consulted at all. `next_page` is the
50
+ * generic one and stays honoured everywhere. See FORWARD_FIELD_APPLICABILITY.
51
+ */
52
+ export declare const FORWARD_ROUTE_FIELDS: readonly ["on_accept", "success_url", "next_page"];
53
+ /** The accept branch. Also a forward field, so it appears in both lists. */
54
+ export declare const ACCEPT_ROUTE_FIELD: "on_accept";
55
+ /** The decline branch. Separate because it is a second edge, not a fallback. */
56
+ export declare const DECLINE_ROUTE_FIELD: "on_decline";
57
+ /**
58
+ * Page types that take payment, and are therefore the only types that can
59
+ * satisfy `success_url`.
60
+ *
61
+ * `success_url` means "where the shopper goes after payment succeeds". Only a
62
+ * page that takes payment has a payment to succeed, so on any other type the
63
+ * field names an event that cannot happen there. In practice it is a
64
+ * copy-paste down from the checkout below it, and honouring it routes the
65
+ * shopper straight past the step they were meant to reach: a `select` page
66
+ * declaring `next_page: checkout` alongside `success_url: upsell` wired the
67
+ * upsell and skipped payment entirely.
68
+ *
69
+ * This is NOT the page-type routing gate #230 removed. That gate DROPPED edges
70
+ * an author had declared, on tables that disagreed about which fields a type
71
+ * was allowed to use. `next_page` and `on_accept` stay type-agnostic — every
72
+ * page may declare them and every declaration is honoured. The carve-out here
73
+ * is about one field's MEANING: reading a payment-shaped field on a page with
74
+ * no payment is not respecting the author's intent, it is inventing one.
75
+ *
76
+ * Membership is deliberately narrow. The three-step shop family types its
77
+ * `information` / `shipping` / `billing` pages as `checkout`, so they are
78
+ * already covered; no certified family carries a second payment-bearing type.
79
+ * An `upsell` takes a one-click payment but expresses its post-purchase branch
80
+ * through `on_accept`, which has its own applicability rule below.
81
+ * Ratified on campaigns-os#234, 2026-08-25.
82
+ */
83
+ export declare const PAYMENT_BEARING_PAGE_TYPES: readonly ["checkout"];
84
+ /**
85
+ * Page types that present an offer the shopper can accept or decline, and are
86
+ * therefore the only types that can satisfy `on_accept`.
87
+ *
88
+ * Same reasoning as PAYMENT_BEARING_PAGE_TYPES, one field over. `on_accept`
89
+ * means "where the shopper goes after accepting the offer on this page". A
90
+ * page that presents no offer has no acceptance to branch on, so the field
91
+ * names an event that cannot happen there.
92
+ *
93
+ * This is not a hypothetical tidy-up. `on_accept` sits at the TOP of
94
+ * FORWARD_ROUTE_FIELDS, so before this rule existed a `select` or `landing`
95
+ * page declaring `next_page: "checkout"` alongside a copy-pasted
96
+ * `on_accept: "upsell"` wired the upsell and routed the shopper straight past
97
+ * payment — the identical break #234 fixed for `success_url`, reachable
98
+ * through the sibling field at higher precedence. Gating one field closed the
99
+ * reported instance; gating both closes the class.
100
+ *
101
+ * `downsell` is included because UpsellRoutingComplete already requires
102
+ * `on_accept` and `on_decline` on exactly `upsell` and `downsell`; leaving
103
+ * `downsell` out would make routing reject a field another rule demands.
104
+ */
105
+ export declare const OFFER_BEARING_PAGE_TYPES: readonly ["upsell", "downsell"];
106
+ /**
107
+ * A forward field whose meaning only some page types can satisfy: the types
108
+ * that can, plus the plain-English meaning that explains why.
109
+ */
110
+ export interface ForwardFieldApplicability {
111
+ /** Page types that can satisfy the field. */
112
+ readonly requiredTypes: readonly string[];
113
+ /** What the field means, phrased to complete "it means ...". */
114
+ readonly meaning: string;
115
+ }
116
+ /**
117
+ * Every field that can carry a route, for consumers that need to enumerate them
118
+ * (validation, tooling). This is NOT the edge set: which of these a page can
119
+ * actually traverse is decided by forwardRouteTarget/declineRouteTarget, since
120
+ * a lower-precedence forward field is shadowed and never taken.
121
+ */
122
+ export declare const ROUTE_FIELDS: readonly ["on_accept", "success_url", "next_page", "on_decline"];
123
+ /**
124
+ * The single forward link, or null when the page declares none. Type-agnostic
125
+ * in the direction that matters: a journey that routes its checkout through
126
+ * `next_page`, or continues past a thank-you page into a second offer
127
+ * sequence, is unusual but entirely legal, and the author has said exactly
128
+ * what they meant. `next_page` is honoured on every page type without
129
+ * exception. `on_accept` and `success_url` are honoured only where they mean
130
+ * something — see FORWARD_FIELD_APPLICABILITY.
131
+ */
132
+ export declare function forwardRouteTarget(page: Page | null | undefined): string | null;
133
+ /**
134
+ * What `field` means and which page types can satisfy it, or null when the
135
+ * field applies everywhere. Exported so a diagnostic can explain WHY a field
136
+ * was skipped without hand-writing a second copy of the rule it reports on.
137
+ */
138
+ export declare function describeForwardField(field: string): ForwardFieldApplicability | null;
139
+ /**
140
+ * Forward fields this page's type CAN route from, declared or not. The
141
+ * complement of the applicability table rather than of what the author wrote,
142
+ * because its job is to answer "what should I have set instead" — a question
143
+ * about the page type, not about this spec.
144
+ *
145
+ * Kept here rather than derived at a call site: a consumer filtering
146
+ * FORWARD_ROUTE_FIELDS against `inapplicableForwardFields` would get the wrong
147
+ * answer, since that list only names fields the author actually declared. An
148
+ * upsell that declares no `success_url` would come back as though `success_url`
149
+ * were a legitimate option for it.
150
+ */
151
+ export declare function applicableForwardFields(page: Page | null | undefined): string[];
152
+ /**
153
+ * Forward fields this page declares with a real target but which its type
154
+ * cannot satisfy, so routing skips them. Empty for almost every page; the
155
+ * diagnostic that teaches authors about a stray `success_url` reads it, rather
156
+ * than re-deriving the type rule and drifting from the resolver.
157
+ */
158
+ export declare function inapplicableForwardFields(page: Page | null | undefined): string[];
159
+ /**
160
+ * The accept-branch target: `on_accept`, but only where the page can satisfy
161
+ * it. Distinct from `forwardRouteTarget`, which answers "the ONE forward link"
162
+ * and may resolve to a different field; this answers "where does accepting
163
+ * this page's offer go", which is a question only an offer page can be asked.
164
+ *
165
+ * Exists because reading `page.on_accept` raw is now wrong. The QA topology
166
+ * extractor did exactly that and, after #234 gated the field, emitted an
167
+ * `expected_accept_url` for a `select` page's inert `on_accept` — so QA looked
168
+ * for an accept link the built page correctly does not have, and flagged a
169
+ * correct build. Every consumer asks the resolver; that is the whole point of
170
+ * this module.
171
+ */
172
+ export declare function acceptRouteTarget(page: Page | null | undefined): string | null;
173
+ /** The decline-branch target, wherever it is declared. */
174
+ export declare function declineRouteTarget(page: Page | null | undefined): string | null;
175
+ /**
176
+ * True when the page RESOLVES to a forward link. Not the same as declaring a
177
+ * forward field: a field the page's type cannot satisfy does not count, so a
178
+ * `select` page carrying only `success_url` reports false. CheckoutHasSuccessUrl
179
+ * reads this, and "can the shopper continue" is the question it means to ask.
180
+ */
181
+ export declare function hasForwardRoute(page: Page | null | undefined): boolean;
182
+ /**
183
+ * The distinct edges this page can actually traverse: its forward link and its
184
+ * decline branch, deduplicated, forward first. Two kinds of declared field are
185
+ * excluded on purpose: one shadowed by a higher-precedence field (see the
186
+ * module note), and one whose page type cannot satisfy it (see
187
+ * PAYMENT_BEARING_PAGE_TYPES). Neither can be taken at runtime, so following
188
+ * either would invent a cycle rather than find one.
189
+ */
190
+ export declare function outgoingEdgeIds(page: Page | null | undefined): string[];
@@ -0,0 +1,263 @@
1
+ /**
2
+ * routing — the single source of truth for a page's outgoing edges.
3
+ *
4
+ * A campaign is a free-form headless shopping journey. It usually runs
5
+ * landing → checkout → upsells → receipt, but nothing requires that, and the
6
+ * routing fields a page declares are the author's statement of where the
7
+ * shopper goes next. Before this module those fields were interpreted by three
8
+ * independent page-type tables — source intake, cycle detection, and the QA
9
+ * topology extractor — which disagreed with each other and silently discarded
10
+ * any edge declared outside their own table.
11
+ *
12
+ * Everything that asks "where does this page go" now asks here, and every
13
+ * caller models the SAME traversable edges — the forward link the page resolves
14
+ * to, plus its decline branch. They differ only in how many of those they need:
15
+ *
16
+ * forwardRouteTarget() — the ONE forward link, for wiring (build output, QA
17
+ * expectations), which emits a single next URL.
18
+ * outgoingEdgeIds() — forward plus decline, for graph analysis (cycle
19
+ * detection), which must consider both branches.
20
+ *
21
+ * An earlier revision had outgoingEdgeIds return every DECLARED field on the
22
+ * theory that a safety analysis should over-approximate. That was wrong, and
23
+ * cost real correctness: a page whose `success_url` wins at runtime and
24
+ * terminates cleanly was reported as a release-blocking cycle through an unused,
25
+ * shadowed `next_page`. The runtime can only ever take the forward link or the
26
+ * decline branch, so following a field neither of them selects does not find
27
+ * extra bugs — it invents them. Accuracy is the safety property here, not
28
+ * breadth.
29
+ *
30
+ * One narrowing sits on top of that (#234): a field whose meaning the page's
31
+ * type cannot satisfy is not an edge either. `success_url` means "after
32
+ * payment" and `on_accept` means "after accepting this page's offer"; a page
33
+ * with no payment and no offer can satisfy neither, and honouring a
34
+ * copy-pasted one routed real shoppers past the checkout. This is NOT the
35
+ * page-type gate #230 removed — that dropped edges the author declared on
36
+ * tables that disagreed about which fields a type may use. `next_page` stays
37
+ * honoured on every page type. The carve-out is about what two fields MEAN.
38
+ */
39
+ /**
40
+ * Forward-route fields in specific-before-generic precedence. `on_accept` and
41
+ * `success_url` each name a particular branch (upsell accept, order success);
42
+ * `next_page` is the generic "wherever this page goes next", so it loses to
43
+ * either. Order is load-bearing — `forwardRouteTarget` returns the first match
44
+ * — and is pinned by test, not only by this comment.
45
+ *
46
+ * Precedence is not the whole answer. `on_accept` and `success_url` each carry
47
+ * a meaning only some page types can satisfy, and a field its page cannot
48
+ * satisfy is skipped before precedence is consulted at all. `next_page` is the
49
+ * generic one and stays honoured everywhere. See FORWARD_FIELD_APPLICABILITY.
50
+ */
51
+ export const FORWARD_ROUTE_FIELDS = Object.freeze([
52
+ 'on_accept',
53
+ 'success_url',
54
+ 'next_page',
55
+ ]);
56
+ /** The accept branch. Also a forward field, so it appears in both lists. */
57
+ export const ACCEPT_ROUTE_FIELD = 'on_accept';
58
+ /** The decline branch. Separate because it is a second edge, not a fallback. */
59
+ export const DECLINE_ROUTE_FIELD = 'on_decline';
60
+ /**
61
+ * Page types that take payment, and are therefore the only types that can
62
+ * satisfy `success_url`.
63
+ *
64
+ * `success_url` means "where the shopper goes after payment succeeds". Only a
65
+ * page that takes payment has a payment to succeed, so on any other type the
66
+ * field names an event that cannot happen there. In practice it is a
67
+ * copy-paste down from the checkout below it, and honouring it routes the
68
+ * shopper straight past the step they were meant to reach: a `select` page
69
+ * declaring `next_page: checkout` alongside `success_url: upsell` wired the
70
+ * upsell and skipped payment entirely.
71
+ *
72
+ * This is NOT the page-type routing gate #230 removed. That gate DROPPED edges
73
+ * an author had declared, on tables that disagreed about which fields a type
74
+ * was allowed to use. `next_page` and `on_accept` stay type-agnostic — every
75
+ * page may declare them and every declaration is honoured. The carve-out here
76
+ * is about one field's MEANING: reading a payment-shaped field on a page with
77
+ * no payment is not respecting the author's intent, it is inventing one.
78
+ *
79
+ * Membership is deliberately narrow. The three-step shop family types its
80
+ * `information` / `shipping` / `billing` pages as `checkout`, so they are
81
+ * already covered; no certified family carries a second payment-bearing type.
82
+ * An `upsell` takes a one-click payment but expresses its post-purchase branch
83
+ * through `on_accept`, which has its own applicability rule below.
84
+ * Ratified on campaigns-os#234, 2026-08-25.
85
+ */
86
+ export const PAYMENT_BEARING_PAGE_TYPES = Object.freeze(['checkout']);
87
+ /**
88
+ * Page types that present an offer the shopper can accept or decline, and are
89
+ * therefore the only types that can satisfy `on_accept`.
90
+ *
91
+ * Same reasoning as PAYMENT_BEARING_PAGE_TYPES, one field over. `on_accept`
92
+ * means "where the shopper goes after accepting the offer on this page". A
93
+ * page that presents no offer has no acceptance to branch on, so the field
94
+ * names an event that cannot happen there.
95
+ *
96
+ * This is not a hypothetical tidy-up. `on_accept` sits at the TOP of
97
+ * FORWARD_ROUTE_FIELDS, so before this rule existed a `select` or `landing`
98
+ * page declaring `next_page: "checkout"` alongside a copy-pasted
99
+ * `on_accept: "upsell"` wired the upsell and routed the shopper straight past
100
+ * payment — the identical break #234 fixed for `success_url`, reachable
101
+ * through the sibling field at higher precedence. Gating one field closed the
102
+ * reported instance; gating both closes the class.
103
+ *
104
+ * `downsell` is included because UpsellRoutingComplete already requires
105
+ * `on_accept` and `on_decline` on exactly `upsell` and `downsell`; leaving
106
+ * `downsell` out would make routing reject a field another rule demands.
107
+ */
108
+ export const OFFER_BEARING_PAGE_TYPES = Object.freeze(['upsell', 'downsell']);
109
+ /**
110
+ * Forward fields whose meaning depends on the page type. A field absent from
111
+ * this table applies everywhere.
112
+ *
113
+ * The `meaning` string lives HERE, beside the types, and not in the rule that
114
+ * reports an ignored field. A rule that hand-wrote its own explanation would
115
+ * be a second derivation of this table: correct today, and quietly wrong the
116
+ * first time a second field or a second payment-bearing type joins it.
117
+ */
118
+ const FORWARD_FIELD_APPLICABILITY = Object.freeze({
119
+ on_accept: Object.freeze({
120
+ requiredTypes: OFFER_BEARING_PAGE_TYPES,
121
+ meaning: 'where the shopper goes after accepting the offer on this page',
122
+ }),
123
+ success_url: Object.freeze({
124
+ requiredTypes: PAYMENT_BEARING_PAGE_TYPES,
125
+ meaning: 'where the shopper goes after payment succeeds',
126
+ }),
127
+ });
128
+ /**
129
+ * Every field that can carry a route, for consumers that need to enumerate them
130
+ * (validation, tooling). This is NOT the edge set: which of these a page can
131
+ * actually traverse is decided by forwardRouteTarget/declineRouteTarget, since
132
+ * a lower-precedence forward field is shadowed and never taken.
133
+ */
134
+ export const ROUTE_FIELDS = Object.freeze([
135
+ ...FORWARD_ROUTE_FIELDS,
136
+ DECLINE_ROUTE_FIELD,
137
+ ]);
138
+ /**
139
+ * Whether `field` carries a meaning this page can satisfy. Type-agnostic for
140
+ * every field except the ones in FORWARD_FIELD_APPLICABILITY.
141
+ */
142
+ function fieldAppliesTo(page, field) {
143
+ const rule = FORWARD_FIELD_APPLICABILITY[field];
144
+ if (!rule)
145
+ return true;
146
+ const type = page?.type;
147
+ if (typeof type !== 'string')
148
+ return false;
149
+ // Trimmed and case-folded on purpose. This is the first thing that ever let
150
+ // `page.type` decide an edge, and normalize() does not touch that field, so
151
+ // an exact match would let `type: "Checkout"` silently drop a real checkout's
152
+ // success_url — the precise failure this module exists to prevent. Compare
153
+ // with RouteTargetResolves, which deliberately does NOT fold case: there
154
+ // folding would PASS a target the build then fails to resolve, so leniency
155
+ // hides a break. Here leniency prevents one.
156
+ return rule.requiredTypes.includes(type.trim().toLowerCase());
157
+ }
158
+ function declared(page, field) {
159
+ const value = page?.[field];
160
+ if (typeof value !== 'string')
161
+ return null;
162
+ // Return the NORMALIZED value. Returning the raw one tested trimmed but
163
+ // handed back untrimmed made `success_url: " landing "` wire a live link
164
+ // (intake trims downstream) while cycle detection's pageMap.get(" landing ")
165
+ // missed — reopening the exact blind spot this module exists to close.
166
+ return value.trim() || null;
167
+ }
168
+ /**
169
+ * The single forward link, or null when the page declares none. Type-agnostic
170
+ * in the direction that matters: a journey that routes its checkout through
171
+ * `next_page`, or continues past a thank-you page into a second offer
172
+ * sequence, is unusual but entirely legal, and the author has said exactly
173
+ * what they meant. `next_page` is honoured on every page type without
174
+ * exception. `on_accept` and `success_url` are honoured only where they mean
175
+ * something — see FORWARD_FIELD_APPLICABILITY.
176
+ */
177
+ export function forwardRouteTarget(page) {
178
+ for (const field of FORWARD_ROUTE_FIELDS) {
179
+ if (!fieldAppliesTo(page, field))
180
+ continue;
181
+ const value = declared(page, field);
182
+ if (value)
183
+ return value;
184
+ }
185
+ return null;
186
+ }
187
+ /**
188
+ * What `field` means and which page types can satisfy it, or null when the
189
+ * field applies everywhere. Exported so a diagnostic can explain WHY a field
190
+ * was skipped without hand-writing a second copy of the rule it reports on.
191
+ */
192
+ export function describeForwardField(field) {
193
+ return FORWARD_FIELD_APPLICABILITY[field] ?? null;
194
+ }
195
+ /**
196
+ * Forward fields this page's type CAN route from, declared or not. The
197
+ * complement of the applicability table rather than of what the author wrote,
198
+ * because its job is to answer "what should I have set instead" — a question
199
+ * about the page type, not about this spec.
200
+ *
201
+ * Kept here rather than derived at a call site: a consumer filtering
202
+ * FORWARD_ROUTE_FIELDS against `inapplicableForwardFields` would get the wrong
203
+ * answer, since that list only names fields the author actually declared. An
204
+ * upsell that declares no `success_url` would come back as though `success_url`
205
+ * were a legitimate option for it.
206
+ */
207
+ export function applicableForwardFields(page) {
208
+ return FORWARD_ROUTE_FIELDS.filter((field) => fieldAppliesTo(page, field));
209
+ }
210
+ /**
211
+ * Forward fields this page declares with a real target but which its type
212
+ * cannot satisfy, so routing skips them. Empty for almost every page; the
213
+ * diagnostic that teaches authors about a stray `success_url` reads it, rather
214
+ * than re-deriving the type rule and drifting from the resolver.
215
+ */
216
+ export function inapplicableForwardFields(page) {
217
+ return FORWARD_ROUTE_FIELDS.filter((field) => !fieldAppliesTo(page, field) && declared(page, field) !== null);
218
+ }
219
+ /**
220
+ * The accept-branch target: `on_accept`, but only where the page can satisfy
221
+ * it. Distinct from `forwardRouteTarget`, which answers "the ONE forward link"
222
+ * and may resolve to a different field; this answers "where does accepting
223
+ * this page's offer go", which is a question only an offer page can be asked.
224
+ *
225
+ * Exists because reading `page.on_accept` raw is now wrong. The QA topology
226
+ * extractor did exactly that and, after #234 gated the field, emitted an
227
+ * `expected_accept_url` for a `select` page's inert `on_accept` — so QA looked
228
+ * for an accept link the built page correctly does not have, and flagged a
229
+ * correct build. Every consumer asks the resolver; that is the whole point of
230
+ * this module.
231
+ */
232
+ export function acceptRouteTarget(page) {
233
+ return fieldAppliesTo(page, ACCEPT_ROUTE_FIELD) ? declared(page, ACCEPT_ROUTE_FIELD) : null;
234
+ }
235
+ /** The decline-branch target, wherever it is declared. */
236
+ export function declineRouteTarget(page) {
237
+ return declared(page, DECLINE_ROUTE_FIELD);
238
+ }
239
+ /**
240
+ * True when the page RESOLVES to a forward link. Not the same as declaring a
241
+ * forward field: a field the page's type cannot satisfy does not count, so a
242
+ * `select` page carrying only `success_url` reports false. CheckoutHasSuccessUrl
243
+ * reads this, and "can the shopper continue" is the question it means to ask.
244
+ */
245
+ export function hasForwardRoute(page) {
246
+ return forwardRouteTarget(page) !== null;
247
+ }
248
+ /**
249
+ * The distinct edges this page can actually traverse: its forward link and its
250
+ * decline branch, deduplicated, forward first. Two kinds of declared field are
251
+ * excluded on purpose: one shadowed by a higher-precedence field (see the
252
+ * module note), and one whose page type cannot satisfy it (see
253
+ * PAYMENT_BEARING_PAGE_TYPES). Neither can be taken at runtime, so following
254
+ * either would invent a cycle rather than find one.
255
+ */
256
+ export function outgoingEdgeIds(page) {
257
+ const ids = [];
258
+ for (const value of [forwardRouteTarget(page), declineRouteTarget(page)]) {
259
+ if (value && !ids.includes(value))
260
+ ids.push(value);
261
+ }
262
+ return ids;
263
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AnalyticsContractShape — validates the optional top-level `analytics` block
3
+ * when present. The block declares a campaign's analytics/attribution/param
4
+ * contract so doctor + QA can validate against intent (cf. the Chamelo Shield
5
+ * `?reviews=n`-has-no-handler QA finding and the Walla Sound Redtrack param
6
+ * conflict — both are gaps that had no declared contract to check against).
7
+ *
8
+ * The block is fully OPTIONAL — a spec without `analytics` is silent (SDK
9
+ * defaults apply, exactly as today). When `analytics` IS set, this rule catches
10
+ * authoring drift before doctor/QA see it. Every check is `warning` severity:
11
+ * authoring guidance, not a build blocker (matches DesignSourceShape).
12
+ *
13
+ * Checks:
14
+ * 0. If `analytics` is present but not a plain object (a non-plain-object
15
+ * value: string, array, number, boolean, boxed primitive, class instance,
16
+ * etc.), warn once and stop — there is no contract shape to inspect.
17
+ * Genuinely-absent `analytics` stays silent (optional, non-gating).
18
+ * 1. `mode`, if present, is one of auto | manual | disabled.
19
+ * 2. Each provider: `blockedEvents` (when present) is a string[], and each
20
+ * entry is a known SDK `dl_*` event (a misspelled/legacy name like
21
+ * "purchase" blocks nothing — the original drift bug this keystone closes);
22
+ * an enabled gtm provider should declare `containerId`, facebook `pixelId`,
23
+ * custom `endpoint` (warning — the id is what doctor/QA bind to).
24
+ * 3. Each `out_of_band_pixels[]` entry has a non-empty `vendor`.
25
+ * 4. Each `manual_events[]` entry has a non-empty `event`; if it names a
26
+ * `page`, that page id must exist; a purchase manual event SHOULD name a
27
+ * page (the first-upsell placement footgun — beacons lost in the
28
+ * checkout→upsell redirect when placed on checkout).
29
+ * 5. Each `params.content[]` entry has a non-empty `name`; referenced `pages`
30
+ * must exist (a content param pointing at a missing page is the
31
+ * `?reviews=n`-with-no-handler gap, inverted).
32
+ * 6. `params.tracking.click_id`, when present, declares both `inbound` and
33
+ * `maps_to` (half a mapping silently drops the affiliate click id).
34
+ * 7. `params.tracking.preserve` / `utmTransfer.paramsToCopy`, when present,
35
+ * are string[].
36
+ * 8. If analytics is active (the block is present and mode is not
37
+ * "disabled"; missing mode means SDK defaults apply), runtime/global_config
38
+ * sdk_version should be an exact released semver >= the SDK identity
39
+ * baseline so events carry campaign/session ids.
40
+ */
41
+ import type { Rule } from '../types.ts';
42
+ export declare const AnalyticsContractShape: Rule;