@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,299 @@
1
+ // Upsell selector scope (#270).
2
+ //
3
+ // A `data-next-bundle-selector` binds to ONE of two baskets. Which one is
4
+ // decided entirely by two container attributes, and getting it wrong on an
5
+ // upsell page charges the shopper for a package they never chose:
6
+ //
7
+ // data-next-upsell-context -> the post-purchase order. Correct after checkout.
8
+ // data-next-selection-mode -> "swap" (the DEFAULT) writes the selected
9
+ // bundle into the LIVE CART; "select" writes
10
+ // nothing anywhere.
11
+ //
12
+ // The failure this module gates is not a click-time mistake. In the SDK's
13
+ // bundle-selector enhancer, a selector without `data-next-upsell-context`
14
+ // subscribes to the cart store and runs a sync pass at init; that pass picks a
15
+ // default card (`data-next-selected="true"`, or the FIRST card when no card
16
+ // declares one) and, in swap mode, applies it to the cart. No click, no
17
+ // interaction, no visibility requirement — a `display:none` container does it
18
+ // just as well as a visible one. On a page whose funnel role is `upsell`, the
19
+ // order is already paid, so the package lands in the shopper's next cart and is
20
+ // charged at the next checkout WITHOUT appearing in that checkout's rendered
21
+ // order summary. Every layer is silent: the SDK binds it without complaint, the
22
+ // built markup looks deliberate, and QA has nothing to assert against.
23
+ //
24
+ // Hence a static blocker over built output. It needs no browser and no order.
25
+ //
26
+ // Two attributes clear the gate, and they are not interchangeable:
27
+ //
28
+ // - `data-next-upsell-context` is the correct answer. It forces select mode,
29
+ // disables cart writes entirely, and fetches prices with `upsell=true` so
30
+ // the rendered price is upsell pricing.
31
+ // - `data-next-selection-mode="select"` is *sufficient against the charge* —
32
+ // every cart write in the enhancer is gated on `mode === "swap" && !upsell
33
+ // context`, at init and on click alike — but it leaves the selector on
34
+ // non-upsell pricing. It clears the blocker and raises a warning, because a
35
+ // blocker whose message says "this sells into the cart" would be stating
36
+ // something false about markup that provably does not.
37
+ //
38
+ // Pure: callers read the built HTML and hand it in. Nothing here touches the
39
+ // filesystem, so both the packet doctor path and the built-site-only path
40
+ // (`doctor --built`) can drive it with the same evaluator.
41
+
42
+ import {
43
+ assessCheckpointWaivers,
44
+ checkpointStateFingerprint,
45
+ projectCheckpointWaiverAssessment,
46
+ } from "./checkpoint-waiver.mjs";
47
+
48
+ export const UPSELL_SELECTOR_SCOPE = "built_output.upsell_selector_scope";
49
+
50
+ // The funnel roles that put a page after checkout, where the cart is the wrong
51
+ // basket. `downsell` is included deliberately: it is the same post-purchase
52
+ // position with a different offer, and the SDK draws no distinction.
53
+ const POST_PURCHASE_PAGE_TYPES = new Set(["upsell", "downsell"]);
54
+
55
+ // Opening tags, with quoted attribute values consumed as units so a `>` inside
56
+ // an attribute (`data-next-show="param.qty>2"`) does not truncate the tag.
57
+ const OPEN_TAG = /<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^>"'])*)>/g;
58
+ const HTML_COMMENT = /<!--[\s\S]*?-->/g;
59
+
60
+ function hasBareAttribute(attrs, name) {
61
+ return new RegExp(`(?:^|\\s)${name}(?=[\\s=/]|$)`, "i").test(attrs);
62
+ }
63
+
64
+ function attributeValue(attrs, name) {
65
+ const match = new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s/>]+))`, "i").exec(attrs);
66
+ if (!match) return null;
67
+ return match[1] ?? match[2] ?? match[3] ?? "";
68
+ }
69
+
70
+ // Bound to a real declaration rather than matched as a substring, so a custom
71
+ // property or a longer property name that merely ends in "display" cannot be
72
+ // read as one (`--card-display:none`, `my-display:none`). Only affects how a
73
+ // finding is described, never whether it blocks.
74
+ function isDisplayNone(style) {
75
+ return /(?:^|;)\s*display\s*:\s*none\s*(?:!\s*important\s*)?(?:;|$)/i.test(String(style || ""));
76
+ }
77
+
78
+ /**
79
+ * Every `data-next-bundle-selector` container in a built page, with the two
80
+ * attributes that decide which basket it writes to.
81
+ *
82
+ * HTML comments are stripped first: a commented-out selector is inert, and the
83
+ * certified upsell templates carry long explanatory comments that quote the
84
+ * attribute names verbatim.
85
+ *
86
+ * Nothing else is excluded, and the two inclusions are deliberate:
87
+ *
88
+ * - `display:none` containers, because the cart write happens during the
89
+ * enhancer's init sync and never consults visibility.
90
+ * - `<template>` content, which is inert in the abstract — it lives in a
91
+ * DocumentFragment until something clones it — but is NOT inert here. This
92
+ * SDK clones it: a selector declares `data-next-bundle-slot-template-id`
93
+ * and the enhancer renders that template's content into the live DOM, one
94
+ * slot per configurable unit. `apollo-mv-single-step` ships exactly that
95
+ * shape today. So a selector authored inside a template on a post-purchase
96
+ * page reaches the live DOM by the SDK's own machinery, and skipping
97
+ * templates would be a blind spot precisely where the mechanism is least
98
+ * obvious to a reader.
99
+ *
100
+ * That is an assumption about THIS SDK, not about HTML in general. If a family
101
+ * ever ships a template that genuinely nothing clones, the finding is a false
102
+ * blocker — visible, named, and waivable, which is the direction this gate errs
103
+ * in on purpose. A false pass is a silent charge.
104
+ */
105
+ export function collectBundleSelectors(html) {
106
+ const source = String(html || "").replace(HTML_COMMENT, "");
107
+ const selectors = [];
108
+ let occurrence = 0;
109
+ for (const match of source.matchAll(OPEN_TAG)) {
110
+ const attrs = match[2] || "";
111
+ if (!hasBareAttribute(attrs, "data-next-bundle-selector")) continue;
112
+ const selectionMode = (attributeValue(attrs, "data-next-selection-mode") || "").trim().toLowerCase();
113
+ const upsellScoped = hasBareAttribute(attrs, "data-next-upsell-context");
114
+ selectors.push({
115
+ occurrence: occurrence++,
116
+ selector_id: attributeValue(attrs, "data-next-selector-id"),
117
+ upsell_scoped: upsellScoped,
118
+ // Absent selection-mode means "swap" — the SDK's default, and the reason
119
+ // this defect is so easy to introduce by omission.
120
+ selection_mode: selectionMode || "swap",
121
+ // The single property that decides whether loading the page moves money.
122
+ writes_to_cart: !upsellScoped && selectionMode !== "select",
123
+ hidden: isDisplayNone(attributeValue(attrs, "style"))
124
+ || hasBareAttribute(attrs, "hidden"),
125
+ });
126
+ }
127
+ return selectors;
128
+ }
129
+
130
+ export function isPostPurchasePageType(value) {
131
+ return POST_PURCHASE_PAGE_TYPES.has(String(value || "").toLowerCase().trim());
132
+ }
133
+
134
+ // A built page's funnel role, from either signal that carries it. The declared
135
+ // spec/scope type and the page's own `next-page-type` meta are both consulted
136
+ // and either one is enough: the meta is what the SDK actually reads, the
137
+ // declared type is what survives when a page ships without the meta, and
138
+ // disagreement between them is a reason to check MORE carefully, not less.
139
+ export function builtPageIsPostPurchase({ page_type = null, content = "" } = {}) {
140
+ if (isPostPurchasePageType(page_type)) return true;
141
+ const meta = /<meta\b(?=[^>]*\bname=["']next-page-type["'])[^>]*\bcontent=["']([^"']*)["'][^>]*>/i.exec(String(content || ""));
142
+ return meta ? isPostPurchasePageType(meta[1]) : false;
143
+ }
144
+
145
+ function describeSelector(finding) {
146
+ return finding.selector_id ? `"${finding.selector_id}"` : "(no data-next-selector-id)";
147
+ }
148
+
149
+ function emptyWaiverAssessment() {
150
+ return { active: null, inert_counts: { stale: 0, foreign: 0, malformed: 0, expired: 0 } };
151
+ }
152
+
153
+ /**
154
+ * Evaluate the upsell-selector-scope checkpoint over built pages.
155
+ *
156
+ * @param {{
157
+ * subject: object,
158
+ * pages: Array<{ page_id: string, page_type?: string|null, file?: string|null, content: string }>,
159
+ * waivers?: unknown,
160
+ * now?: string,
161
+ * }} input
162
+ */
163
+ export function evaluateUpsellSelectorScope({ subject, pages = [], waivers = null, now = new Date().toISOString() } = {}) {
164
+ const resolvedSubject = subject && typeof subject === "object" ? subject : {};
165
+ const postPurchasePages = (Array.isArray(pages) ? pages : []).filter((page) => builtPageIsPostPurchase(page));
166
+
167
+ if (postPurchasePages.length === 0) {
168
+ return {
169
+ id: UPSELL_SELECTOR_SCOPE,
170
+ scope: UPSELL_SELECTOR_SCOPE,
171
+ status: "not_applicable",
172
+ code: "built_output.upsell_selector_scope.not_applicable",
173
+ reason: "No built upsell or downsell page was available to scan; selector scope becomes mandatory once one is built.",
174
+ waivable: false,
175
+ subject: resolvedSubject,
176
+ state: { findings: [], warned: [] },
177
+ state_fingerprint: null,
178
+ findings: [],
179
+ warned: [],
180
+ pages_scanned: 0,
181
+ selectors_scanned: 0,
182
+ waiver: null,
183
+ waiver_assessment: emptyWaiverAssessment(),
184
+ required_actions: [],
185
+ };
186
+ }
187
+
188
+ const findings = [];
189
+ const warned = [];
190
+ let selectorsScanned = 0;
191
+ for (const page of postPurchasePages) {
192
+ for (const selector of collectBundleSelectors(page.content)) {
193
+ selectorsScanned += 1;
194
+ if (selector.upsell_scoped) continue;
195
+ const record = {
196
+ page_id: page.page_id,
197
+ file: page.file || null,
198
+ selector_id: selector.selector_id,
199
+ occurrence: selector.occurrence,
200
+ selection_mode: selector.selection_mode,
201
+ hidden: selector.hidden,
202
+ };
203
+ if (selector.writes_to_cart) findings.push(record);
204
+ else warned.push(record);
205
+ }
206
+ }
207
+
208
+ // State is the blocking set only. A warned selector must not shift the
209
+ // fingerprint, or correcting an unrelated pricing note would silently
210
+ // invalidate a waiver recorded against a blocker that has not changed.
211
+ const state = {
212
+ findings: findings
213
+ .map(({ page_id, selector_id, occurrence }) => ({ page_id, selector_id, occurrence }))
214
+ .sort((a, b) => a.page_id.localeCompare(b.page_id)
215
+ || String(a.selector_id).localeCompare(String(b.selector_id))
216
+ || a.occurrence - b.occurrence),
217
+ };
218
+
219
+ const warningNote = warned.length
220
+ ? ` ${warned.length} further selector(s) are cart-scoped but carry data-next-selection-mode="select", so they do not write to the cart; they still price without upsell pricing.`
221
+ : "";
222
+
223
+ if (findings.length === 0) {
224
+ return {
225
+ id: UPSELL_SELECTOR_SCOPE,
226
+ scope: UPSELL_SELECTOR_SCOPE,
227
+ status: "pass",
228
+ code: warned.length ? "built_output.upsell_selector_scope.cart_scoped_select_mode" : "built_output.upsell_selector_scope.pass",
229
+ reason: `Every bundle selector on ${postPurchasePages.length} built post-purchase page(s) is scoped away from the live cart.${warningNote}`,
230
+ waivable: false,
231
+ subject: resolvedSubject,
232
+ state,
233
+ state_fingerprint: null,
234
+ findings: [],
235
+ warned,
236
+ pages_scanned: postPurchasePages.length,
237
+ selectors_scanned: selectorsScanned,
238
+ waiver: null,
239
+ waiver_assessment: emptyWaiverAssessment(),
240
+ required_actions: [],
241
+ };
242
+ }
243
+
244
+ const state_fingerprint = checkpointStateFingerprint({
245
+ scope: UPSELL_SELECTOR_SCOPE,
246
+ subject: resolvedSubject,
247
+ state,
248
+ });
249
+ const checkpoint = { scope: UPSELL_SELECTOR_SCOPE, subject: resolvedSubject, state_fingerprint };
250
+ const waiver_assessment = projectCheckpointWaiverAssessment(
251
+ assessCheckpointWaivers(waivers, checkpoint, { now }),
252
+ checkpoint,
253
+ );
254
+ const waiver = waiver_assessment.active;
255
+
256
+ // The message has to carry the whole finding, because the defect's defining
257
+ // property is that nothing else shows it: the selector id (there are usually
258
+ // two selectors on the page and only one is wrong), the page, and what the
259
+ // selector will actually do to a shopper.
260
+ const detail = findings
261
+ .map((finding) => `${describeSelector(finding)} on page "${finding.page_id}"${finding.hidden ? " (hidden)" : ""}`)
262
+ .join("; ");
263
+ const symptom = "Loading the page adds that selector's package to the shopper's LIVE CART with no click, and it is charged at the next checkout without appearing in that checkout's rendered order summary.";
264
+ const remedy = 'Add data-next-upsell-context to each selector listed (the post-purchase basket, and upsell pricing), or delete the selector if it exists only to display a price.';
265
+
266
+ return {
267
+ id: UPSELL_SELECTOR_SCOPE,
268
+ scope: UPSELL_SELECTOR_SCOPE,
269
+ status: waiver ? "waived" : "blocked",
270
+ code: waiver ? "built_output.upsell_selector_scope.waived" : UPSELL_SELECTOR_SCOPE,
271
+ reason: waiver
272
+ ? `${findings.length} cart-writing bundle selector(s) on built post-purchase page(s) are accepted under an exact named-human decision: ${detail}.`
273
+ : `${findings.length} bundle selector(s) on built post-purchase page(s) sell into the live cart: ${detail}. Each lacks data-next-upsell-context and is in the default "swap" selection mode. ${symptom} ${remedy}${warningNote}`,
274
+ waivable: true,
275
+ subject: resolvedSubject,
276
+ state,
277
+ state_fingerprint,
278
+ findings,
279
+ warned,
280
+ pages_scanned: postPurchasePages.length,
281
+ selectors_scanned: selectorsScanned,
282
+ waiver,
283
+ waiver_assessment,
284
+ required_actions: waiver ? [] : [
285
+ {
286
+ id: "repair_selectors",
287
+ kind: "edit",
288
+ command: null,
289
+ description: `Add data-next-upsell-context to ${detail}, or remove the selector, then rebuild and re-run doctor.`,
290
+ },
291
+ {
292
+ id: "waive_checkpoint",
293
+ kind: "command",
294
+ command: `campaigns-os checkpoint waive --packet <packet> --gate ${UPSELL_SELECTOR_SCOPE} --reason "<reason>" --waived-by "<named human>" --review-condition "<re-evaluation trigger>"`,
295
+ description: "Record a bounded named-human decision that these exact cart-scoped selectors are intended on a post-purchase page.",
296
+ },
297
+ ],
298
+ };
299
+ }