@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,381 @@
1
+ // Order-bump state evidence for browser QA (campaigns-os#323).
2
+ //
3
+ // The `browser-order-bump-state` assertion asks one question: does what the
4
+ // buyer can see on a bump toggle agree with what the SDK thinks is in the cart?
5
+ // It reads three things off each toggle — the SDK's own active/in-cart state,
6
+ // the toggle's checkbox input if it has one, and the rendered state marker —
7
+ // and fails when they disagree.
8
+ //
9
+ // The marker half used to be a single `querySelector` over a selector list that
10
+ // ended in a bare `[aria-hidden]`. Two things went wrong with that:
11
+ //
12
+ // 1. `aria-hidden` is decoration vocabulary, not state vocabulary. A bump
13
+ // toggle that carries its own visually-hidden `<input type="checkbox"
14
+ // aria-hidden="true">` has that input match the list, and `querySelector`
15
+ // returns document order, not selector order — so on every toggle whose
16
+ // input precedes its tick, the "marker" resolved to the input. An input is
17
+ // not a rendered marker: it has no `::after`, no glyph, no fill, so
18
+ // `markerChecked` could never read true and an accepted bump reported
19
+ // misaligned on every single run. That is the whole of #323.
20
+ // 2. The same clause matched purely decorative nodes — an arrow icon, a
21
+ // switch slider — whose rendering says nothing about the toggle's state,
22
+ // so the check would fail a correctly rendered toggle for the same reason.
23
+ //
24
+ // So the marker vocabulary here is only the four families that mean "this is
25
+ // the tick", the families are tried in order (rather than being collapsed into
26
+ // one document-order query), and form controls and `[hidden]` subtrees can
27
+ // never be a marker.
28
+ //
29
+ // A rendered marker is then read for a positive state signal, and the signals
30
+ // are alternatives rather than one replacing another, because the families
31
+ // express state in genuinely different ways:
32
+ //
33
+ // - `pseudo` — the marker is a persistent box and its `::after` carries the
34
+ // tick. The box renders in both states, so its visibility says nothing;
35
+ // the state is whether the pseudo-element is rendered. A marker whose
36
+ // `::after` has non-empty content belongs to this family by definition,
37
+ // and its checked reading comes from that pseudo-element alone.
38
+ // - `glyph` — the tick is literal text in the marker.
39
+ // - `fill` — the marker is filled with the accepted colour.
40
+ // - `display_toggled` — the marker *is* the tick and the CSS shows and hides
41
+ // it: `[data-next-toggle-card] [os-component="check"] { display: none }`
42
+ // with the active or in-cart card restoring `display: flex`. Only here is
43
+ // the marker's own rendering the state affordance.
44
+ //
45
+ // The last one is a claim about the page's CSS, so it is tested against the
46
+ // page's CSS (`hiddenByAMatchingRule`) rather than assumed from the family
47
+ // name. That is what keeps a persistent box out of it: a box nothing hides is
48
+ // not display-toggled, and reading its visibility as "checked" would fail a
49
+ // correctly declined bump. For the same reason that walk counts only rules
50
+ // that hide outright: a rule dimming a marker to `opacity: 0.4` leaves it on
51
+ // screen, so it is a style and not a state affordance. The rendered read still
52
+ // treats a faded marker as too faint to read a tick off; the two questions
53
+ // differ, and only the rule walk demands an exact `opacity: 0`.
54
+ //
55
+ // A rendered marker carrying no positive signal is reported as unresolved, not
56
+ // as checked and not as unchecked. The harness cannot read that page's state
57
+ // vocabulary, and saying so is honest; claiming a disagreement is not.
58
+
59
+ // Bump toggle roots. Matches the SDK's toggle card and the older bump root.
60
+ export const ORDER_BUMP_TOGGLE_SELECTOR = "[data-next-toggle-card], [data-next-bump]";
61
+
62
+ // State-marker families, most specific first. Tried in order, so a generic
63
+ // match can never outrank a dedicated one just by appearing earlier in the
64
+ // document. Deliberately does NOT include a bare `[aria-hidden]`.
65
+ export const ORDER_BUMP_MARKER_FAMILIES = Object.freeze([
66
+ ".bump-check",
67
+ "[data-next-toggle-check]",
68
+ "[os-component='check']",
69
+ ".checkbox__icon",
70
+ ]);
71
+
72
+ // Containers a marker may be nested in; the container must render too, so a tick
73
+ // inside a collapsed wrapper is not read as shown. A marker's wrapper is drawn
74
+ // from the same vocabulary as the marker itself — a `.checkbox__icon` box
75
+ // holding an `[os-component="check"]` tick is the shape this exists for — so
76
+ // this is the family list, not a second copy of it: two copies drifted apart
77
+ // once already, and a family added to one but not the other would resolve a
78
+ // nested tick and then judge it by the wrong box. Order is immaterial here,
79
+ // unlike in the family list: the selectors are joined into one `closest()`
80
+ // query, which resolves by tree position rather than by selector order.
81
+ export const ORDER_BUMP_MARKER_CONTAINERS = ORDER_BUMP_MARKER_FAMILIES;
82
+
83
+ // Elements that can never be a state marker, whatever they match: the toggle's
84
+ // own form control (read separately as `inputChecked`) and anything the author
85
+ // removed from rendering outright.
86
+ export const ORDER_BUMP_MARKER_EXCLUDED = "input, select, textarea, option";
87
+
88
+ // The accepted-state fill used by the shared checkout CSS, as a computed
89
+ // `background-color` string — `getComputedStyle` always reports an opaque
90
+ // colour in this `rgb(r, g, b)` form, so the comparison is a string equality
91
+ // and the constant has to be written the way the browser serialises it. It is
92
+ // the `fill` family's whole vocabulary: a marker painted this colour is a
93
+ // ticked marker. Deliberately a constant of this module rather than a value
94
+ // read from a brand contract — the check is asking what the checkout CSS in
95
+ // front of the buyer actually paints, and sourcing it from a theme would make
96
+ // the answer move whenever the theme does.
97
+ export const ORDER_BUMP_ACCEPTED_FILL_COLOR = "rgb(45, 148, 127)";
98
+
99
+ export const ORDER_BUMP_PROBE_INPUT = Object.freeze({
100
+ toggleSelector: ORDER_BUMP_TOGGLE_SELECTOR,
101
+ markerFamilies: ORDER_BUMP_MARKER_FAMILIES,
102
+ markerContainers: ORDER_BUMP_MARKER_CONTAINERS,
103
+ markerExcluded: ORDER_BUMP_MARKER_EXCLUDED,
104
+ acceptedFillColor: ORDER_BUMP_ACCEPTED_FILL_COLOR,
105
+ });
106
+
107
+ // Returns the `page.evaluate` body. Exported as a factory so the browser proof
108
+ // can drive the same function the QA runner does, instead of a copy of it.
109
+ export function orderBumpEvidenceScript() {
110
+ return ({ toggleSelector, markerFamilies, markerContainers, markerExcluded, acceptedFillColor }) => {
111
+ // The evaluate body is serialised into the page, so it cannot read the
112
+ // module constant; the colour arrives through the probe input. A caller
113
+ // that omits it would compare every fill against undefined and read each
114
+ // fill-family marker as unresolved with no signal, so refuse instead.
115
+ if (typeof acceptedFillColor !== "string" || !acceptedFillColor) {
116
+ throw new Error("order-bump evidence script needs acceptedFillColor in its input (use ORDER_BUMP_PROBE_INPUT)");
117
+ }
118
+ const hasContent = (value) => Boolean(value) && !["none", "normal", '""', "''"].includes(value);
119
+
120
+ // The one list of declarations that remove an element from view. Both the
121
+ // computed-style test and the stylesheet-rule walk ask through this, so a
122
+ // way of hiding a tick can never be understood by one and missed by the
123
+ // other — `visibility: collapse` was rejected by the computed-style test
124
+ // and invisible to the rule walk, which left a tick hidden that way
125
+ // unreadable instead of state-toggled. Reads through getPropertyValue
126
+ // because a rule's own style leaves unset properties empty, which every
127
+ // test below treats as "not hidden".
128
+ //
129
+ // `opacity` is the one declaration the two callers must weigh differently,
130
+ // so it is passed in rather than fixed here. `display` and `visibility`
131
+ // have no middle ground; opacity does, and the two callers are asking
132
+ // different questions about it (see the two wrappers below).
133
+ const hiddenBy = (style, opacityHides) => style.getPropertyValue("display") === "none"
134
+ || ["hidden", "collapse"].includes(style.getPropertyValue("visibility"))
135
+ || opacityHides(Number.parseFloat(style.getPropertyValue("opacity") || "1"));
136
+
137
+ // What the buyer can see. An element faded to half its colour or less is
138
+ // not something a buyer reads a tick off, so the rendered test treats
139
+ // anything at or below half opacity as hidden. The threshold is a judgement about
140
+ // legibility, which is why it belongs only here.
141
+ const FADED_OUT_AT_OR_BELOW = 0.5;
142
+ const hiddenFromView = (style) => hiddenBy(style, (value) => value <= FADED_OUT_AT_OR_BELOW);
143
+
144
+ // What a stylesheet rule *declares*. The rule walk is not asking whether
145
+ // the buyer can see the element; it is asking whether a rule removes the
146
+ // element from rendering, because that is the display-toggled family's
147
+ // signature. `opacity: 0.4` is a dimmed style, not a hiding one — a
148
+ // greyed-out but perfectly visible marker would otherwise be read as a
149
+ // state-toggled tick and report a correctly declined bump as misaligned.
150
+ // Only `opacity: 0` removes a tick the way `display: none` does, so only
151
+ // an exact zero counts here.
152
+ const hiddenByDeclaration = (style) => hiddenBy(style, (value) => value === 0);
153
+
154
+ // The marker's `::after`, read once for both jobs it does here. A tick that
155
+ // is absolutely positioned can render while its host box measures zero, so
156
+ // a zero-sized host is not on its own proof that the tick is hidden — the
157
+ // pseudo-element has to be looked at before the size test disqualifies the
158
+ // marker. Returns null when the element has no generated content at all.
159
+ const pseudoTick = (element) => {
160
+ const after = getComputedStyle(element, "::after");
161
+ if (!hasContent(after.content)) return null;
162
+ // An unstyled tick is sized by its own content, and its computed width
163
+ // and height come back as `auto` rather than a length. Only an explicit
164
+ // zero means the tick occupies nothing; `auto` means the content decides,
165
+ // and the content is non-empty or we would not be here.
166
+ const boxed = (value) => {
167
+ const raw = String(value ?? "").trim();
168
+ if (raw === "" || raw === "auto") return true;
169
+ const parsed = Number.parseFloat(raw);
170
+ return Number.isFinite(parsed) ? parsed > 0 : true;
171
+ };
172
+ return {
173
+ shown: !hiddenFromView(after),
174
+ boxed: boxed(after.width) && boxed(after.height),
175
+ };
176
+ };
177
+
178
+ // The one visibility test, in two halves that every call site shares: is the
179
+ // element styled to be seen, and does it occupy a box. Both the toggle
180
+ // filter and the marker read go through these, so a toggle and its own
181
+ // marker can never be judged by different rules — `visibility: collapse`
182
+ // was rejected for a marker and accepted for the toggle containing it.
183
+ // Returns the computed style so a caller that needs it does not read twice.
184
+ const visibleStyle = (element) => {
185
+ if (!(element instanceof Element) || element.hidden) return null;
186
+ if (element.closest("[hidden]")) return null;
187
+ const style = getComputedStyle(element);
188
+ return hiddenFromView(style) ? null : style;
189
+ };
190
+
191
+ const hasBox = (element) => {
192
+ const rect = element.getBoundingClientRect();
193
+ return rect.width > 0 && rect.height > 0 && element.getClientRects().length > 0;
194
+ };
195
+
196
+ const displayed = (element) => Boolean(visibleStyle(element)) && hasBox(element);
197
+
198
+ // A marker is held to the same visibility rules, with one addition: a
199
+ // zero-sized host still renders when its own tick does.
200
+ const rendered = (element) => {
201
+ if (!visibleStyle(element)) return false;
202
+ if (hasBox(element)) return true;
203
+ const tick = pseudoTick(element);
204
+ return Boolean(tick && tick.shown && tick.boxed);
205
+ };
206
+
207
+ // A candidate must be a plausible marker at all: not the toggle's own form
208
+ // control, not inside a subtree the author removed from rendering.
209
+ const eligible = (element) => !element.matches(markerExcluded) && !element.closest("[hidden]");
210
+
211
+ // Does any stylesheet rule that *currently applies* and matches this element
212
+ // remove it from rendering? That is the display-toggled family's signature:
213
+ // a base rule hides the tick and a state-scoped rule restores it, so the
214
+ // element matches the hiding rule in both states. A persistent box that
215
+ // nothing hides matches nothing here, which is exactly why its visibility
216
+ // must not be read as its state.
217
+ //
218
+ // "Currently applies" is load-bearing. A rule inside `@media print`, or
219
+ // inside an `@supports` block for a feature this browser lacks, says
220
+ // nothing about what the buyer sees on screen; counting it would read a
221
+ // visible, unchecked box as a hidden tick and fail a correctly declined
222
+ // bump. So a conditional group is descended into only while its condition
223
+ // holds, a stylesheet whose own media attribute does not match is skipped,
224
+ // and so is a disabled one. A stylesheet the page cannot read
225
+ // (cross-origin, no CORS) is not evidence either way and is skipped too.
226
+ const hiddenByAMatchingRule = (element) => {
227
+ const mediaApplies = (query) => {
228
+ if (!query || query === "all") return true;
229
+ try {
230
+ return window.matchMedia(query).matches;
231
+ } catch {
232
+ return false;
233
+ }
234
+ };
235
+ const groupApplies = (rule) => {
236
+ // A container query is evaluated against a specific element's nearest
237
+ // container, and there is no API that answers that question for an
238
+ // arbitrary element — `CSS.supports` rejects a container condition
239
+ // outright. So an `@container` block is skipped deliberately rather
240
+ // than by accident: a tick hidden only inside one reads as unresolved,
241
+ // which is quiet, instead of being guessed at. Recorded as a known
242
+ // limit in fixtures/qa-order-bump/README.md.
243
+ if (typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule) return false;
244
+ // CSSMediaRule carries `.media`; CSSSupportsRule carries only a
245
+ // condition. Anything else grouping (a layer, a nested style rule) has
246
+ // no condition to fail and applies.
247
+ if (rule.media && typeof rule.media.mediaText === "string") {
248
+ return mediaApplies(rule.conditionText || rule.media.mediaText);
249
+ }
250
+ if (typeof rule.conditionText === "string") {
251
+ try {
252
+ return CSS.supports(rule.conditionText);
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+ return true;
258
+ };
259
+ const walk = (rules) => {
260
+ for (const rule of rules) {
261
+ if (rule.cssRules) {
262
+ if (!groupApplies(rule)) continue;
263
+ if (walk(Array.from(rule.cssRules))) return true;
264
+ }
265
+ if (!rule.selectorText || !rule.style || !hiddenByDeclaration(rule.style)) continue;
266
+ try {
267
+ // Pseudo-element selectors throw here; they are not this family.
268
+ if (element.matches(rule.selectorText)) return true;
269
+ } catch {
270
+ // unsupported selector: no evidence
271
+ }
272
+ }
273
+ return false;
274
+ };
275
+ for (const sheet of Array.from(document.styleSheets)) {
276
+ if (sheet.disabled) continue;
277
+ if (!mediaApplies(sheet.media?.mediaText)) continue;
278
+ let rules;
279
+ try {
280
+ rules = Array.from(sheet.cssRules || []);
281
+ } catch {
282
+ continue;
283
+ }
284
+ if (walk(rules)) return true;
285
+ }
286
+ return false;
287
+ };
288
+
289
+
290
+ // The positive signals, in the order that settles which vocabulary the
291
+ // marker speaks. Returns null when a rendered marker carries none.
292
+ const checkedSignal = (marker) => {
293
+ const style = getComputedStyle(marker);
294
+ // A marker with pseudo-element content belongs to the pseudo family
295
+ // whatever else is true of it, and its state is that pseudo-element's
296
+ // rendering — never the box's.
297
+ const tick = pseudoTick(marker);
298
+ if (tick) return { signal: "pseudo", checked: tick.shown };
299
+ if (/check|\u2713/.test(marker.textContent || "")) return { signal: "glyph", checked: true };
300
+ if (style.backgroundColor === acceptedFillColor) return { signal: "fill", checked: true };
301
+ if (hiddenByAMatchingRule(marker)) return { signal: "display_toggled", checked: true };
302
+ return null;
303
+ };
304
+
305
+ // Families in order. A rendered candidate wins outright; otherwise the
306
+ // first eligible candidate is kept so an off toggle still reports a marker
307
+ // (which then correctly reads unchecked) rather than reporting none.
308
+ const resolveMarker = (toggle) => {
309
+ let fallback = null;
310
+ for (const family of markerFamilies) {
311
+ const candidates = Array.from(toggle.querySelectorAll(family)).filter(eligible);
312
+ if (!candidates.length) continue;
313
+ const shown = candidates.find(rendered);
314
+ if (shown) return { element: shown, family };
315
+ if (!fallback) fallback = { element: candidates[0], family };
316
+ }
317
+ return fallback;
318
+ };
319
+
320
+ const toggles = Array.from(document.querySelectorAll(toggleSelector)).filter(displayed).map((toggle, index) => {
321
+ const input = toggle.querySelector('input[type="checkbox"]');
322
+ const resolved = resolveMarker(toggle);
323
+ const marker = resolved?.element || null;
324
+ const markerContainer = marker?.closest(markerContainers.join(", ")) || marker;
325
+ const markerVisible = Boolean(marker)
326
+ && rendered(marker)
327
+ && Boolean(markerContainer)
328
+ && rendered(markerContainer);
329
+ // A hidden marker is unchecked in every family, so it needs no signal.
330
+ // A rendered one is read for a positive signal, and a rendered marker
331
+ // with none is `unresolved`: it exists and renders, but nothing on it
332
+ // says which state it is in. That is distinct from finding no marker at
333
+ // all, which is a null signal, so the two are never conflated by a reader
334
+ // of the evidence.
335
+ const signal = marker && markerVisible ? checkedSignal(marker) : null;
336
+ const markerSignal = !marker
337
+ ? null
338
+ : (markerVisible ? signal?.signal || "unresolved" : "not_rendered");
339
+ const markerChecked = Boolean(markerVisible && signal?.checked);
340
+ // Readable: a marker was found AND its state can be read. An unreadable
341
+ // marker is read like an absent one, never as a disagreement.
342
+ const markerReadable = markerSignal !== null && markerSignal !== "unresolved";
343
+ const active = toggle.classList.contains("next-active")
344
+ || toggle.classList.contains("next-in-cart")
345
+ || toggle.classList.contains("next-selected")
346
+ || toggle.getAttribute("aria-pressed") === "true";
347
+ const inputChecked = input ? input.checked : null;
348
+ const inputAgrees = inputChecked === null || inputChecked === active;
349
+ const markerAgrees = !markerReadable || markerChecked === active;
350
+ return {
351
+ index,
352
+ packageId: toggle.getAttribute("data-next-package-id") || null,
353
+ active,
354
+ inputChecked,
355
+ // Was a marker element found at all. Unchanged in meaning: whether the
356
+ // harness located something to read.
357
+ markerResolved: Boolean(marker),
358
+ // Whether that marker's state could actually be read. A found-but-
359
+ // unreadable marker is resolved and not readable, and only `markerAgrees`
360
+ // depends on this one.
361
+ markerReadable,
362
+ // Which family matched and what it resolved to, so an operator reading
363
+ // a misaligned verdict can see whether the harness found the right
364
+ // element before concluding the page is wrong.
365
+ markerFamily: resolved?.family || null,
366
+ markerTag: marker ? marker.tagName.toLowerCase() : null,
367
+ // How the marker was read: "pseudo", "glyph", "fill" or
368
+ // "display_toggled" when a state vocabulary was recognised,
369
+ // "not_rendered" when the marker is on the page but hidden,
370
+ // "unresolved" when it renders but carries no state signal, and null
371
+ // when no marker was found at all.
372
+ markerSignal,
373
+ markerChecked,
374
+ inputAgrees,
375
+ markerAgrees,
376
+ statesAgree: inputAgrees && markerAgrees,
377
+ };
378
+ });
379
+ return { toggles };
380
+ };
381
+ }