@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,406 @@
1
+ // Cart entry for the typed-card runner (campaigns-os#206, runner half).
2
+ //
3
+ // A checkout page renders its customer form whether or not the SDK cart holds
4
+ // anything, so a runner that opens the checkout URL directly cannot tell a
5
+ // "select here" checkout (Demeter, Apollo, Olympus: the bundle cards live on
6
+ // the checkout page) from a "cart was filled upstream" checkout (the
7
+ // shop-single-step family: the landing page adds to the cart and hands off).
8
+ // On the second shape the ladder used to run every fill step green and then sit
9
+ // in `order_submitted` for the full step budget, because the SDK never posts an
10
+ // order for an empty cart and the captured request log had nothing to say.
11
+ //
12
+ // This module holds the browser-free half of the fix: resolving which page the
13
+ // funnel enters the cart from, the DOM probes (as evaluate() bodies) that
14
+ // decide whether a checkout carries its own selection surface, the SDK cart
15
+ // read used by the pre-submit guard, and the named failure codes the ladder
16
+ // reports instead of a timeout. The design half of #206 — authoritative entry
17
+ // metadata on the spec — is deliberately NOT decided here: entry is still
18
+ // inferred from topology, and the inference is recorded on the ladder step so a
19
+ // reader can see which page the runner chose and why.
20
+
21
+ import { OFFER_PAGE_TYPES, RECEIPT_PAGE_TYPES, canonicalHttpUrl } from "./qa-test-order-topology.mjs";
22
+
23
+ // Ladder step recorded before `opened_checkout`.
24
+ export const CART_ENTRY_STEP = "entered_via_landing";
25
+
26
+ // Named failure codes. Each one is a pre-submit failure by construction: the
27
+ // runner authored the refusal before any reservation and before any click, so
28
+ // the creation classifier reads them as `not_created`.
29
+ export const CART_ENTRY_CODES = Object.freeze({
30
+ // Checkout carries no selection surface, and no landing/entry page could be
31
+ // resolved from the funnel topology to fill the cart from.
32
+ ENTRY_UNRESOLVED: "cart_entry_unresolved",
33
+ // The resolved entry page rendered no add-to-cart control (or none matching
34
+ // the requested --select-package ref).
35
+ ENTRY_CONTROL_MISSING: "cart_entry_control_missing",
36
+ // The control was clicked but the page never reached the checkout URL.
37
+ ENTRY_NO_NAVIGATION: "cart_entry_no_navigation",
38
+ // The SDK cart held zero items at submit time. Fired before the budget
39
+ // reservation and before the submit click.
40
+ CART_EMPTY_BEFORE_SUBMIT: "cart_empty_before_submit",
41
+ });
42
+
43
+ // The SDK's own activation selector for its add-to-cart feature, and nothing
44
+ // else: the SDK instantiates the feature on `data-next-action="add-to-cart"`
45
+ // only, so any other attribute spelling is a control the SDK never wires. A
46
+ // page carrying one of those must read as "no cart-entry control", not as an
47
+ // entry the runner can click, or the ladder claims an activation the SDK
48
+ // does not have.
49
+ export const CART_ENTRY_CONTROL_SELECTOR = '[data-next-action="add-to-cart"]';
50
+
51
+ // The attribute an SDK cart-entry control (one matching
52
+ // CART_ENTRY_CONTROL_SELECTOR) navigates by: the SDK adds the package and then
53
+ // goes to `data-next-url` itself, so such a control carries no href. Only SDK
54
+ // controls get this reading — on any other element `data-next-url` has no
55
+ // navigation semantics and the href-shaped attributes stay authoritative
56
+ // (campaigns-os#321: the primary-CTA recogniser used to read href-shaped
57
+ // attributes only, so the SDK's own button on a landing page counted as "no
58
+ // CTA to the next route" while a plain cart-bypassing <a href> passed). The
59
+ // ladder's entry step and the primary-CTA assertion share this one rule.
60
+ export const CART_ENTRY_ROUTE_ATTRIBUTE = "data-next-url";
61
+
62
+ // The route a candidate CTA leads to, as one pure rule shared by the
63
+ // primary-CTA recogniser (which runs it inside the page) and the unit tests
64
+ // (which run it against element-shaped objects). Self-contained on purpose:
65
+ // the browser half serialises this function's source into the page, so it
66
+ // may reference nothing outside its own body.
67
+ //
68
+ // An SDK cart-entry control (one matching `cartEntrySelector`) navigates by
69
+ // `cartEntryRouteAttribute` and nothing else: the SDK's click handler calls
70
+ // preventDefault() unconditionally, so its href never navigates, and it
71
+ // resolves the attribute against the origin (campaign-cart url-utils), not the
72
+ // document base. Without the attribute such a control adds to the cart and
73
+ // stays put — no route. Anywhere else the SDK attribute has no navigation
74
+ // semantics: an HTML anchor's own resolved href (native, so a <base href> is
75
+ // honoured), then an href attribute, then `data-href`, then a wrapping form's
76
+ // action. Only spellings with real navigation semantics count — an attribute
77
+ // the SDK does not declare is not a route.
78
+ export function cartEntryHrefFor(element, { cartEntrySelector, cartEntryRouteAttribute, origin, baseHref }) {
79
+ if (!element || typeof element.getAttribute !== "function") return null;
80
+ if (typeof element.matches === "function" && element.matches(cartEntrySelector)) {
81
+ const sdkRoute = String(element.getAttribute(cartEntryRouteAttribute) || "").trim();
82
+ if (!sdkRoute) return null;
83
+ try {
84
+ return new URL(sdkRoute, origin).href;
85
+ } catch {
86
+ // An unparseable route attribute is no route either; do not leak the
87
+ // raw value into evidence as if it were one.
88
+ return null;
89
+ }
90
+ }
91
+ // An HTML anchor resolves its own href (an SVG <a> exposes an object, not a
92
+ // string, and falls through to the attribute reading).
93
+ if (String(element.tagName || "").toUpperCase() === "A" && typeof element.href === "string" && element.href) return element.href;
94
+ const attr = element.getAttribute("href")
95
+ || element.getAttribute("data-href")
96
+ || (typeof element.closest === "function" ? element.closest("form")?.getAttribute("action") : null);
97
+ if (!attr) return null;
98
+ try {
99
+ return new URL(attr, baseHref).href;
100
+ } catch {
101
+ // Same rule as the SDK branch: an unparseable attribute is not a route,
102
+ // and evidence `href` fields stay parseable URLs or null.
103
+ return null;
104
+ }
105
+ }
106
+
107
+ // Route-shaped spellings the runner used to consult and no longer does,
108
+ // because the SDK never declares them. They are not routes, but a page that
109
+ // carries one is reported so an operator can tell "the runner narrowed its
110
+ // vocabulary" from "the CTA was removed" when a missing-route verdict flips.
111
+ export const UNDECLARED_ROUTE_ATTRIBUTES = Object.freeze(["data-next-href", "data-next-checkout-action"]);
112
+
113
+ // Page types that, when they route into checkout, are preferred as the cart
114
+ // entry. `select` is a real page type since #228; `product` is what the
115
+ // shop-single-step landing declares; the rest are the entry-like types
116
+ // `deriveEntryUrls` already recognises.
117
+ // Pages that can only follow the checkout: the checkout itself plus the
118
+ // topology module's own offer and receipt sets, so the two never drift.
119
+ const POST_CHECKOUT_PAGE_TYPES = new Set(["checkout", ...OFFER_PAGE_TYPES, ...RECEIPT_PAGE_TYPES]);
120
+ const PREFERRED_ENTRY_PAGE_TYPES = ["select", "landing", "product", "presell", "entry", "lander", "advertorial", "listicle", "review", "opt-in", "optin"];
121
+
122
+ export function codedError(code, message) {
123
+ const error = new Error(`${code}: ${message}`);
124
+ error.code = code;
125
+ return error;
126
+ }
127
+
128
+ export function isCartEntryCode(code) {
129
+ return Object.values(CART_ENTRY_CODES).includes(code);
130
+ }
131
+
132
+ function pageTypeOf(page) {
133
+ return String(page?.page_type || "").toLowerCase().trim();
134
+ }
135
+
136
+ function sameUrl(left, right) {
137
+ const a = canonicalHttpUrl(left);
138
+ const b = canonicalHttpUrl(right);
139
+ return Boolean(a && b && a === b);
140
+ }
141
+
142
+ // Which page fills the cart before checkout. Resolution order:
143
+ // 1. a page (other than checkout) whose `expected_next_url` is the checkout,
144
+ // preferring the entry-like page types above, then the lowest `order`;
145
+ // 2. the topology's first entry-like page, else its first page — the same
146
+ // choice `deriveEntryUrls` makes for `entry_urls[0]` — restricted to
147
+ // pages that sit BEFORE the checkout. A receipt or an offer page can
148
+ // never be the cart entry, whatever the fallback order says.
149
+ // Returns null when nothing resolves; the caller turns that into a named
150
+ // failure only when the checkout also carries no selection surface.
151
+ export function resolveCartEntryPage(topologies = [], checkoutPage = null) {
152
+ if (!checkoutPage?.url) return null;
153
+ const list = Array.isArray(topologies) ? topologies : [];
154
+ const owning = list.find((topology) => (topology?.pages || []).some((page) => page === checkoutPage || sameUrl(page?.url, checkoutPage.url)))
155
+ || list[0]
156
+ || null;
157
+ const checkoutOrder = Number(checkoutPage.order);
158
+ const pages = (owning?.pages || []).filter((page) => (
159
+ page?.url
160
+ && !sameUrl(page.url, checkoutPage.url)
161
+ && !POST_CHECKOUT_PAGE_TYPES.has(pageTypeOf(page))
162
+ && (!Number.isFinite(checkoutOrder) || !Number.isFinite(Number(page.order)) || Number(page.order) < checkoutOrder)
163
+ ));
164
+ if (!pages.length) return null;
165
+
166
+ const rank = (page) => {
167
+ const index = PREFERRED_ENTRY_PAGE_TYPES.indexOf(pageTypeOf(page));
168
+ return index === -1 ? PREFERRED_ENTRY_PAGE_TYPES.length : index;
169
+ };
170
+ const byPreference = (left, right) => rank(left) - rank(right) || (Number(left.order) || 0) - (Number(right.order) || 0);
171
+
172
+ const routesIntoCheckout = pages.filter((page) => sameUrl(page.expected_next_url, checkoutPage.url)).sort(byPreference);
173
+ if (routesIntoCheckout.length) return describe(routesIntoCheckout[0], "routes_into_checkout");
174
+
175
+ const entryLike = pages.filter((page) => PREFERRED_ENTRY_PAGE_TYPES.includes(pageTypeOf(page))).sort(byPreference);
176
+ if (entryLike.length) return describe(entryLike[0], "entry_page_fallback");
177
+ return describe(pages[0], "first_page_fallback");
178
+ }
179
+
180
+ function describe(page, resolution) {
181
+ return {
182
+ page_id: page.page_id || null,
183
+ page_type: page.page_type || null,
184
+ url: page.url,
185
+ resolution,
186
+ };
187
+ }
188
+
189
+ // evaluate() body: does this checkout carry a main-cart selection surface?
190
+ // Counted: bundle/cart selectors and package cards. Not counted: anything
191
+ // inside the rendered cart summary (it displays `cart.*`, it does not select),
192
+ // order-bump toggles (an add-on is not the main cart), upsell-context
193
+ // selectors, and <template> content the SDK has not rendered.
194
+ export function checkoutSelectionSurfaceScript() {
195
+ return () => {
196
+ const EXCLUDED_ANCESTORS = "[data-next-cart-summary], [data-next-toggle-card], [data-next-package-toggle], [data-next-bump], [data-next-upsell-context], [data-next-upsell], template";
197
+ const candidates = Array.from(document.querySelectorAll("[data-next-bundle-selector], [data-next-cart-selector], [data-next-package-id]"));
198
+ const counted = candidates.filter((element) => !element.closest(EXCLUDED_ANCESTORS));
199
+ const kinds = {
200
+ bundle_selector: counted.filter((element) => element.hasAttribute("data-next-bundle-selector")).length,
201
+ cart_selector: counted.filter((element) => element.hasAttribute("data-next-cart-selector")).length,
202
+ package_card: counted.filter((element) => element.hasAttribute("data-next-package-id")).length,
203
+ };
204
+ return { count: counted.length, kinds, excluded: candidates.length - counted.length };
205
+ };
206
+ }
207
+
208
+ export function summarizeSelectionSurface(surface) {
209
+ const kinds = surface?.kinds || {};
210
+ const parts = Object.entries(kinds).filter(([, count]) => count > 0).map(([kind, count]) => `${count} ${kind}`);
211
+ return parts.length ? parts.join(", ") : "none";
212
+ }
213
+
214
+ // evaluate() body: the cart-entry controls the entry page renders. Two kinds:
215
+ // add_to_cart the SDK action controls (CART_ENTRY_CONTROL_SELECTOR); the
216
+ // SDK adds the package and navigates via data-next-url;
217
+ // checkout_link a plain link into the checkout URL carrying
218
+ // `?forcePackageId=`, which the SDK reads on the checkout page
219
+ // to pre-load the cart. This is what the certified
220
+ // shop-single-step landing renders today.
221
+ // Each control reports its visibility, text, and the package id it carries
222
+ // (own attribute, nearest card, or the forcePackageId ref), so the caller can
223
+ // prefer a visible SDK control and honour --select-package in one round trip.
224
+ export function cartEntryControlsScript() {
225
+ return ({ selector, checkoutUrl }) => {
226
+ const clean = (value) => String(value || "").replace(/\s+/g, " ").trim();
227
+ const isVisible = (element) => {
228
+ const rect = element.getBoundingClientRect();
229
+ const style = getComputedStyle(element);
230
+ return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
231
+ };
232
+ const canonical = (value) => {
233
+ try {
234
+ const url = new URL(value, document.baseURI);
235
+ url.search = "";
236
+ url.hash = "";
237
+ url.pathname = url.pathname.replace(/\/index\.html$/i, "/").replace(/\/+$/, "") || "/";
238
+ return url.toString();
239
+ } catch {
240
+ return null;
241
+ }
242
+ };
243
+ const label = (element) => clean(element.textContent).slice(0, 80) || clean(element.getAttribute("aria-label")) || clean(element.getAttribute("value")) || null;
244
+ const readPackageId = (element) => {
245
+ const own = element.getAttribute("data-next-package-id");
246
+ if (own && own.trim()) return own.trim();
247
+ const card = element.closest("[data-next-package-id]");
248
+ return card ? clean(card.getAttribute("data-next-package-id")) || null : null;
249
+ };
250
+ const readQuantity = (value) => {
251
+ const parsed = Number.parseInt(String(value || "").trim(), 10);
252
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
253
+ };
254
+ // `index` is the control's position among the elements its own locator
255
+ // matches (the SDK-control selector, or every `a[href]`), so the caller
256
+ // can replay it with nth() instead of rebuilding a selector from the
257
+ // attribute value.
258
+ const actions = Array.from(document.querySelectorAll(selector)).map((element, index) => ({
259
+ kind: "add_to_cart",
260
+ index,
261
+ visible: isVisible(element),
262
+ text: label(element),
263
+ package_id: readPackageId(element),
264
+ quantity: readQuantity(element.getAttribute("data-next-quantity")),
265
+ next_url: clean(element.getAttribute("data-next-url")) || null,
266
+ tag: element.tagName.toLowerCase(),
267
+ }));
268
+ const checkout = canonical(checkoutUrl);
269
+ const links = checkout
270
+ ? Array.from(document.querySelectorAll("a[href]")).flatMap((element, index) => {
271
+ const href = element.getAttribute("href");
272
+ if (canonical(href) !== checkout) return [];
273
+ let forced = null;
274
+ try {
275
+ forced = new URL(href, document.baseURI).searchParams.get("forcePackageId");
276
+ } catch {
277
+ forced = null;
278
+ }
279
+ if (!forced) return [];
280
+ const [ref, qty] = forced.split(",")[0].split(":");
281
+ return [{
282
+ kind: "checkout_link",
283
+ index,
284
+ visible: isVisible(element),
285
+ text: label(element),
286
+ package_id: clean(ref) || null,
287
+ quantity: readQuantity(qty),
288
+ next_url: href,
289
+ tag: "a",
290
+ }];
291
+ })
292
+ : [];
293
+ return [...actions, ...links];
294
+ };
295
+ }
296
+
297
+ // Pick the control to click. A requested --select-package ref must match the
298
+ // control's package id (strict, like selectPackageCard); otherwise the first
299
+ // visible SDK control wins, then the first visible checkout link, and a hidden
300
+ // control is only used when nothing is visible.
301
+ export function chooseCartEntryControl(controls = [], requested = []) {
302
+ const list = Array.isArray(controls) ? controls : [];
303
+ if (!list.length) return { control: null, reason: "no add-to-cart control or forcePackageId checkout link rendered on the entry page" };
304
+ const wanted = Array.isArray(requested) ? requested.filter((item) => item?.packageId) : [];
305
+ if (wanted.length > 1) {
306
+ return {
307
+ control: null,
308
+ reason: `--select-package requested ${wanted.length} refs, but a landing-page entry can select at most one package before the SDK navigates to checkout`,
309
+ };
310
+ }
311
+ const byRef = wanted.length
312
+ ? list.filter((control) => String(control.package_id) === String(wanted[0].packageId))
313
+ : list;
314
+ if (!byRef.length) {
315
+ return {
316
+ control: null,
317
+ reason: `--select-package ${wanted[0].packageId}: no cart-entry control on the entry page carries package ${wanted[0].packageId} (rendered: ${list.map((control) => control.package_id || "(none)").join(", ")})`,
318
+ };
319
+ }
320
+ // An explicit quantity is a claim about what the click adds. The control
321
+ // states its own quantity (data-next-quantity, or the forcePackageId
322
+ // `ref:qty`), so a request the control cannot satisfy is refused rather
323
+ // than silently downgraded to whatever the control adds.
324
+ const pool = wanted.length && wanted[0].quantityExplicit
325
+ ? byRef.filter((control) => Number(control.quantity ?? 1) === Number(wanted[0].quantity))
326
+ : byRef;
327
+ if (!pool.length) {
328
+ return {
329
+ control: null,
330
+ reason: `--select-package ${wanted[0].packageId}:${wanted[0].quantity}: the entry-page control(s) for package ${wanted[0].packageId} add quantity ${[...new Set(byRef.map((control) => control.quantity ?? 1))].join("/")}, not ${wanted[0].quantity}`,
331
+ };
332
+ }
333
+ const byKind = (kind) => pool.filter((control) => control.kind === kind);
334
+ const ordered = [...byKind("add_to_cart"), ...byKind("checkout_link")];
335
+ return { control: ordered.find((control) => control.visible) || ordered[0], reason: null };
336
+ }
337
+
338
+ // evaluate() body: the SDK cart as the page sees it. Reads the public API
339
+ // first — `window.next.getCartCount()` is installed on every SDK page and
340
+ // returns the store's `totalQuantity` — and the debugger's cart store second
341
+ // (`window.nextDebug.stores.cart`, present with `?debugger=true`), which is
342
+ // the only public place the line items and their package ids are readable.
343
+ // The enriched line list on `getCartData()` is deliberately NOT read: it is
344
+ // always empty on the shipped SDK (campaign-cart#36; see the cart-state
345
+ // verification section of docs/qa-and-test-orders.md), so it would call every
346
+ // cart empty. Reports `readable: false` when neither source is present so the
347
+ // guard can say "could not read" instead of guessing.
348
+ export function sdkCartSnapshotScript() {
349
+ return () => {
350
+ const asArray = (value) => (Array.isArray(value) ? value : []);
351
+ const packageIds = (items) => asArray(items).map((item) => item?.packageId ?? item?.package_id ?? null).filter((id) => id !== null && id !== undefined).map(String);
352
+ let debugItems = null;
353
+ try {
354
+ const store = window.nextDebug?.stores?.cart;
355
+ const state = typeof store?.getState === "function" ? store.getState() : store;
356
+ if (state && Array.isArray(state.items)) debugItems = state.items;
357
+ } catch {
358
+ debugItems = null;
359
+ }
360
+ try {
361
+ const sdk = window.next;
362
+ if (sdk && typeof sdk.getCartCount === "function") {
363
+ const count = Number(sdk.getCartCount());
364
+ return {
365
+ readable: true,
366
+ source: "window.next",
367
+ count: Number.isFinite(count) ? count : 0,
368
+ line_count: debugItems ? debugItems.length : null,
369
+ package_ids: packageIds(debugItems),
370
+ };
371
+ }
372
+ } catch {
373
+ // Fall through to the debug store alone.
374
+ }
375
+ if (debugItems) {
376
+ return {
377
+ readable: true,
378
+ source: "nextDebug.stores.cart",
379
+ count: debugItems.reduce((sum, item) => sum + (Number(item?.quantity) || 0), 0),
380
+ line_count: debugItems.length,
381
+ package_ids: packageIds(debugItems),
382
+ };
383
+ }
384
+ return { readable: false, source: null, count: null, line_count: null, package_ids: [] };
385
+ };
386
+ }
387
+
388
+ // The guard's decision. `cartApi` is the cart-create observation
389
+ // (`cartCreationEvidence`) — the third source the guard may fall back to when
390
+ // the page exposes no SDK global at all.
391
+ export function assessCartBeforeSubmit(snapshot, cartApi = null) {
392
+ if (snapshot?.readable) {
393
+ // Decided on the unit count alone: `totalQuantity` is the store's own
394
+ // number, and the line list is only present under the debugger.
395
+ const empty = Number(snapshot.count) === 0;
396
+ return { empty, source: snapshot.source, count: snapshot.count, line_count: snapshot.line_count ?? null, package_ids: snapshot.package_ids || [] };
397
+ }
398
+ if (cartApi && typeof cartApi.line_count === "number") {
399
+ return { empty: cartApi.line_count === 0, source: "cart_api_response", count: null, line_count: cartApi.line_count, package_ids: [] };
400
+ }
401
+ return { empty: false, source: null, count: null, line_count: null, package_ids: [], unreadable: true };
402
+ }
403
+
404
+ export function cartEmptyMessage(assessment) {
405
+ return `SDK cart holds 0 item(s) at submit time (read from ${assessment.source}); the checkout would never post an order, so the submit click was not made`;
406
+ }