@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,358 @@
1
+ // Static SDK markup checks (#303).
2
+ //
3
+ // `built_output.upsell_selector_scope` catches one shape of built markup that
4
+ // the Campaign Cart SDK binds without complaint and that then does the wrong
5
+ // thing to a shopper. This module is the rest of that family: six more
6
+ // shapes, each a static read of built HTML, each producing either a silent
7
+ // no-op (the shopper fills a field that never reaches the order; a button
8
+ // that never enables) or a double cart write. A partner agency's Campaign
9
+ // Cart skill kit listed them as stable lint codes; the codes are kept so the
10
+ // two vocabularies line up.
11
+ //
12
+ // Blockers (not waivable — the markup provably does not do what it says)
13
+ // SWAP_WITH_ADD_TO_CART a bundle selector in swap mode (explicit, or
14
+ // the SDK default when the attribute is absent)
15
+ // AND an add-to-cart button linked to it by
16
+ // data-next-selector-id. Both write the cart: the
17
+ // card click swaps the bundle in, the button adds
18
+ // it again.
19
+ // CHECKOUT_NOT_FORM data-next-checkout on anything but <form>. The
20
+ // checkout enhancer binds to a form element;
21
+ // anything else never submits.
22
+ // WRONG_FIELD_NAME data-next-checkout-field with a value the SDK
23
+ // does not map (firstName, lastName, zip …). The
24
+ // input renders, the value never reaches the
25
+ // order.
26
+ // MISSING_SELECTOR_ID_MATCH an add-to-cart button whose data-next-selector-id
27
+ // names no selector on the page. The button waits
28
+ // for a selection that can never arrive.
29
+ //
30
+ // Warnings (advisory)
31
+ // DOUBLE_SELECTED more than one data-next-selected="true" card in
32
+ // one selector; the SDK picks one and the page
33
+ // shows two.
34
+ // TEMPLATE_DOUBLE_BRACE `{{` inside an SDK-owned <template>. SDK tokens
35
+ // are single-brace and conditions are no-brace;
36
+ // a double brace renders literally.
37
+ //
38
+ // Info (advisory, one note per campaign, no code)
39
+ // unknown_attributes[] a data-next-* name the pinned SDK's attribute
40
+ // index does not list. Catches an invented
41
+ // attribute (data-next-coupon-input) — and, on the
42
+ // certified templates, a handful of the templates'
43
+ // own data-next-* hooks, which is why it informs
44
+ // rather than warns.
45
+ //
46
+ // Parsed with parse5 rather than regex because four of the six turn on
47
+ // containment (a card inside a selector, a field inside a form, a template's
48
+ // content), and HTML nesting is not a regular language. Template content is
49
+ // walked too: the SDK clones it into the live DOM.
50
+ //
51
+ // Pure: callers hand in built HTML. Both doctor entry points drive it.
52
+
53
+ import { parse } from "parse5";
54
+
55
+ import {
56
+ SDK_ATTRIBUTE_INDEX_VERSION,
57
+ isIndexedSdkAttribute,
58
+ isKnownCheckoutFieldName,
59
+ } from "./sdk-attribute-index.mjs";
60
+
61
+ export const SDK_MARKUP = "built_output.sdk_markup";
62
+
63
+ export const SDK_MARKUP_CODES = Object.freeze({
64
+ SWAP_WITH_ADD_TO_CART: { code: `${SDK_MARKUP}.swap_with_add_to_cart`, severity: "error" },
65
+ CHECKOUT_NOT_FORM: { code: `${SDK_MARKUP}.checkout_not_form`, severity: "error" },
66
+ WRONG_FIELD_NAME: { code: `${SDK_MARKUP}.wrong_field_name`, severity: "error" },
67
+ MISSING_SELECTOR_ID_MATCH: { code: `${SDK_MARKUP}.missing_selector_id_match`, severity: "error" },
68
+ DOUBLE_SELECTED: { code: `${SDK_MARKUP}.double_selected`, severity: "warning" },
69
+ TEMPLATE_DOUBLE_BRACE: { code: `${SDK_MARKUP}.template_double_brace`, severity: "warning" },
70
+ // Unknown data-next-* names are not a finding and carry no code: they are
71
+ // information on the gate (unknown_attributes[]) and one advisory ready line.
72
+ });
73
+
74
+ // Attributes whose value names a <template> by id. The SDK reads the template
75
+ // they point at, so that template is SDK-owned wherever it sits.
76
+ const TEMPLATE_ID_ATTRIBUTES = /^data-(?:next-)?[a-z0-9-]*template-id$/;
77
+
78
+ // Containers whose DIRECT <template> child the SDK clones (each does a
79
+ // `:scope > template` lookup at v0.4.38: cart-summary and its
80
+ // data-summary-lines / data-next-discounts sub-containers; bundle-selector,
81
+ // the card-internal data-next-bundle-slots placeholder
82
+ // (`[data-next-bundle-slots] > template`, template-state.ts) and the external
83
+ // data-next-bundle-slots-for container the enhancer resolves by selector id
84
+ // (`externalSlotsEl.querySelector(':scope > template')`); package-selector;
85
+ // package-toggle).
86
+ // Only the direct child: a vendor template nested deeper inside SDK chrome is
87
+ // never read, so it may use any syntax.
88
+ const TEMPLATE_CONTAINER_ATTRIBUTES = [
89
+ "data-next-cart-summary",
90
+ "data-summary-lines",
91
+ "data-next-discounts",
92
+ "data-next-bundle-selector",
93
+ "data-next-bundle-slots",
94
+ "data-next-bundle-slots-for",
95
+ "data-next-package-selector",
96
+ "data-next-package-toggle",
97
+ ];
98
+
99
+ // Elements that ARE a selector an add-to-cart button can link to by id.
100
+ const SELECTOR_ATTRIBUTES = ["data-next-bundle-selector", "data-next-package-selector", "data-next-cart-selector", "data-next-upsell-selector"];
101
+
102
+ function attrs(node) {
103
+ const map = new Map();
104
+ for (const attr of node.attrs || []) map.set(attr.name.toLowerCase(), attr.value);
105
+ return map;
106
+ }
107
+
108
+ // Depth-first over element nodes, descending into <template> content. The
109
+ // callback receives the element, its attribute map, and the ancestor chain
110
+ // (nearest first) so containment questions are a lookup, not a second walk.
111
+ function walkElements(root, visit, ancestors = []) {
112
+ const children = root.content ? root.content.childNodes : root.childNodes;
113
+ for (const node of children || []) {
114
+ if (!node.tagName) continue;
115
+ const entry = { node, tag: node.tagName.toLowerCase(), attrs: attrs(node), ancestors };
116
+ visit(entry);
117
+ walkElements(node, visit, [entry, ...ancestors]);
118
+ }
119
+ }
120
+
121
+ function serializeTemplateText(node) {
122
+ // Text content of a template's fragment, attributes included: `{{` can sit
123
+ // in an attribute value (`data-next-format="{{price}}"`) as easily as in a
124
+ // text node.
125
+ const out = [];
126
+ const visit = (n) => {
127
+ if (n.value != null) out.push(n.value);
128
+ for (const attr of n.attrs || []) out.push(attr.value);
129
+ const kids = n.content ? n.content.childNodes : n.childNodes;
130
+ for (const child of kids || []) visit(child);
131
+ };
132
+ visit(node);
133
+ return out.join("\n");
134
+ }
135
+
136
+ function describe(entry) {
137
+ const id = entry.attrs.get("id");
138
+ const selectorId = entry.attrs.get("data-next-selector-id");
139
+ const bits = [`<${entry.tag}`];
140
+ if (id) bits.push(` id="${id}"`);
141
+ if (selectorId) bits.push(` data-next-selector-id="${selectorId}"`);
142
+ return `${bits.join("")}>`;
143
+ }
144
+
145
+ /**
146
+ * Scan one built page. Returns findings with { code_name, code, severity,
147
+ * page_id, file, message, detail } and the set of unknown data-next-* names.
148
+ */
149
+ export function scanPageMarkup({ page_id, file = null, content = "" }) {
150
+ const document = parse(String(content || ""));
151
+ const where = file || page_id;
152
+ const findings = [];
153
+ const unknown = new Set();
154
+
155
+ const selectors = []; // { entry, id, mode, upsell, selectedCount }
156
+ const addToCartButtons = []; // { entry, selectorId }
157
+ const selectorIds = new Set(); // ids of elements that are themselves a selector
158
+ const templates = []; // { entry, sdkOwned }
159
+ const referencedTemplateIds = new Set();
160
+
161
+ walkElements(document, (entry) => {
162
+ const { tag, attrs: a, ancestors } = entry;
163
+
164
+ for (const [name] of a) {
165
+ if (name.startsWith("data-next-") && !isIndexedSdkAttribute(name)) unknown.add(name);
166
+ if (TEMPLATE_ID_ATTRIBUTES.test(name) && a.get(name)) referencedTemplateIds.add(a.get(name).trim());
167
+ }
168
+
169
+ if (a.has("data-next-checkout") && tag !== "form") {
170
+ findings.push(finding("CHECKOUT_NOT_FORM", page_id, where,
171
+ `${describe(entry)} on ${where} carries data-next-checkout but is not a <form>. The checkout enhancer binds to a form element; this one never submits. Move the attribute onto the <form> that wraps the fields.`,
172
+ { tag }));
173
+ }
174
+
175
+ if (a.has("data-next-checkout-field")) {
176
+ const value = a.get("data-next-checkout-field") ?? "";
177
+ if (!isKnownCheckoutFieldName(value)) {
178
+ findings.push(finding("WRONG_FIELD_NAME", page_id, where,
179
+ `data-next-checkout-field="${value}" on ${where} is not a field name the SDK ${SDK_ATTRIBUTE_INDEX_VERSION} maps (${suggestFieldName(value)}). The input renders and the value never reaches the order.`,
180
+ { value, suggestion: suggestFieldName(value, true) }));
181
+ }
182
+ }
183
+
184
+ const action = (a.get("data-next-action") || "").trim().toLowerCase();
185
+ if (action === "add-to-cart") {
186
+ addToCartButtons.push({ entry, selectorId: (a.get("data-next-selector-id") || "").trim() || null });
187
+ } else if (SELECTOR_ATTRIBUTES.some((name) => a.has(name)) && (a.get("data-next-selector-id") || "").trim()) {
188
+ // Only an element that IS a selector satisfies the link. Another element
189
+ // merely echoing the id (a price display, a quantity control) does not
190
+ // drive the button, and counting it would hide the missing selector.
191
+ selectorIds.add(a.get("data-next-selector-id").trim());
192
+ }
193
+
194
+ if (a.has("data-next-bundle-selector") || a.has("data-next-package-selector")) {
195
+ const mode = (a.get("data-next-selection-mode") || "").trim().toLowerCase();
196
+ selectors.push({
197
+ entry,
198
+ id: (a.get("data-next-selector-id") || "").trim() || null,
199
+ mode: mode || "swap",
200
+ modeExplicit: Boolean(mode),
201
+ upsell: a.has("data-next-upsell-context"),
202
+ selectedCount: 0,
203
+ });
204
+ }
205
+
206
+ if (a.has("data-next-selected") && (a.get("data-next-selected") || "").trim().toLowerCase() === "true") {
207
+ const owner = ancestors.find((anc) => anc.attrs.has("data-next-bundle-selector") || anc.attrs.has("data-next-package-selector"));
208
+ if (owner) {
209
+ const selector = selectors.find((s) => s.entry === owner);
210
+ if (selector) selector.selectedCount += 1;
211
+ }
212
+ }
213
+
214
+ if (tag === "template") {
215
+ const parent = ancestors[0];
216
+ templates.push({ entry, sdkOwned: Boolean(parent && TEMPLATE_CONTAINER_ATTRIBUTES.some((name) => parent.attrs.has(name))) });
217
+ }
218
+ });
219
+
220
+ // SWAP_WITH_ADD_TO_CART and MISSING_SELECTOR_ID_MATCH need the whole page.
221
+ // One finding per dead id, however many buttons link to it: the root cause
222
+ // is the selector, not each button.
223
+ const missingReported = new Set();
224
+ for (const button of addToCartButtons) {
225
+ if (!button.selectorId) continue;
226
+ const linked = selectors.filter((s) => s.id === button.selectorId);
227
+ if (linked.length === 0 && !selectorIds.has(button.selectorId)) {
228
+ if (missingReported.has(button.selectorId)) continue;
229
+ missingReported.add(button.selectorId);
230
+ const count = addToCartButtons.filter((other) => other.selectorId === button.selectorId).length;
231
+ findings.push(finding("MISSING_SELECTOR_ID_MATCH", page_id, where,
232
+ `${count > 1 ? `${count} add-to-cart buttons` : 'data-next-action="add-to-cart"'} on ${where} ${count > 1 ? "are" : "is"} linked to data-next-selector-id="${button.selectorId}" but no selector on the page carries that id. The button${count > 1 ? "s wait" : " waits"} for a selection that can never arrive and never enable${count > 1 ? "" : "s"}. Give the selector that id, or set data-next-package-id on the button and drop the link.`,
233
+ { selector_id: button.selectorId, buttons: count }));
234
+ continue;
235
+ }
236
+ for (const selector of linked) {
237
+ if (selector.upsell || selector.mode !== "swap") continue;
238
+ findings.push(finding("SWAP_WITH_ADD_TO_CART", page_id, where,
239
+ `Selector data-next-selector-id="${selector.id}" on ${where} is in swap mode (${selector.modeExplicit ? 'data-next-selection-mode="swap"' : "the SDK default; no data-next-selection-mode set"}) and an add-to-cart button is linked to it. Both write the cart: the card click swaps the bundle in, the button adds it again. Set data-next-selection-mode="select" on the selector, or remove the button.`,
240
+ { selector_id: selector.id, selection_mode_explicit: selector.modeExplicit }));
241
+ }
242
+ }
243
+
244
+ for (const selector of selectors) {
245
+ if (selector.selectedCount > 1) {
246
+ findings.push(finding("DOUBLE_SELECTED", page_id, where,
247
+ `Selector ${selector.id ? `data-next-selector-id="${selector.id}"` : "(no data-next-selector-id)"} on ${where} has ${selector.selectedCount} cards with data-next-selected="true". The SDK selects one; the page shows ${selector.selectedCount}. Leave exactly one default card selected.`,
248
+ { selector_id: selector.id, selected_count: selector.selectedCount }));
249
+ }
250
+ }
251
+
252
+ for (const template of templates) {
253
+ const id = template.entry.attrs.get("id");
254
+ const owned = template.sdkOwned || (id && referencedTemplateIds.has(id.trim()));
255
+ if (!owned) continue;
256
+ const text = serializeTemplateText(template.entry.node);
257
+ const at = text.indexOf("{{");
258
+ if (at === -1) continue;
259
+ const snippet = text.slice(at, at + 40).split("\n")[0];
260
+ const inner = /^\{\{\s*([^}\s]*)/.exec(snippet)?.[1] || "token";
261
+ findings.push(finding("TEMPLATE_DOUBLE_BRACE", page_id, where,
262
+ `SDK template ${id ? `id="${id}"` : "(no id)"} on ${where} contains "${snippet}". SDK template tokens are single-brace ({${inner}}) and conditions are no-brace; a double brace renders literally to the shopper.`,
263
+ { template_id: id || null, snippet }));
264
+ }
265
+
266
+ return { page_id, file, findings, unknown_attributes: [...unknown].sort() };
267
+ }
268
+
269
+ function finding(codeName, page_id, file, message, detail = {}) {
270
+ const { code, severity } = SDK_MARKUP_CODES[codeName];
271
+ return { code_name: codeName, code, severity, page_id, file, message: `${codeName}: ${message}`, detail };
272
+ }
273
+
274
+ const FIELD_NAME_HINTS = {
275
+ firstname: "fname", first_name: "fname", "first-name": "fname",
276
+ lastname: "lname", last_name: "lname", "last-name": "lname",
277
+ zip: "postal", zipcode: "postal", postcode: "postal", postal_code: "postal",
278
+ state: "province", region: "province",
279
+ address: "address1", street: "address1", apartment: "address2", apt: "address2", suite: "address2",
280
+ tel: "phone", telephone: "phone", mobile: "phone",
281
+ emailaddress: "email", "email-address": "email",
282
+ cardnumber: "cc-number", "card-number": "cc-number", ccnumber: "cc-number",
283
+ expmonth: "exp-month", expyear: "exp-year", cvc: "cvv", "cc-cvv": "cvv",
284
+ };
285
+
286
+ function suggestFieldName(value, bare = false) {
287
+ const key = String(value || "").toLowerCase();
288
+ const hint = FIELD_NAME_HINTS[key] || (isKnownCheckoutFieldName(key) ? key : null);
289
+ if (bare) return hint;
290
+ return hint ? `the SDK spelling is "${hint}"` : "see the SDK checkout-form field names";
291
+ }
292
+
293
+ /**
294
+ * Evaluate the SDK markup family over built pages.
295
+ *
296
+ * @param {{ subject: object, pages: Array<{ page_id: string, file?: string|null, content: string }> }} input
297
+ */
298
+ export function evaluateSdkMarkup({ subject, pages = [] } = {}) {
299
+ const resolvedSubject = subject && typeof subject === "object" ? subject : {};
300
+ const base = { id: SDK_MARKUP, scope: SDK_MARKUP, waivable: false, subject: resolvedSubject, waiver: null, sdk_attribute_index_version: SDK_ATTRIBUTE_INDEX_VERSION };
301
+ const list = Array.isArray(pages) ? pages : [];
302
+
303
+ if (list.length === 0) {
304
+ return { ...base, status: "not_applicable", code: `${SDK_MARKUP}.not_applicable`, reason: "No built page was available to scan; SDK markup checks become mandatory once pages are built.", findings: [], warned: [], unknown_attributes: [], pages_scanned: 0, required_actions: [] };
305
+ }
306
+
307
+ const findings = [];
308
+ const warned = [];
309
+ const unknown = new Map();
310
+ for (const page of list) {
311
+ const scan = scanPageMarkup(page);
312
+ for (const item of scan.findings) (item.severity === "error" ? findings : warned).push(item);
313
+ for (const name of scan.unknown_attributes) {
314
+ if (!unknown.has(name)) unknown.set(name, []);
315
+ unknown.get(name).push(scan.file || scan.page_id);
316
+ }
317
+ }
318
+ const unknown_attributes = [...unknown.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([name, files]) => ({ name, pages: files }));
319
+
320
+ if (findings.length === 0) {
321
+ return {
322
+ ...base,
323
+ status: "pass",
324
+ code: warned.length ? `${SDK_MARKUP}.advisories` : `${SDK_MARKUP}.pass`,
325
+ reason: `SDK markup checks passed on ${list.length} built page(s)${warned.length ? ` with ${warned.length} advisory finding(s)` : ""}.`,
326
+ findings: [],
327
+ warned,
328
+ unknown_attributes,
329
+ pages_scanned: list.length,
330
+ required_actions: [],
331
+ };
332
+ }
333
+
334
+ // Bounded summaries: a checkout with a dozen misspelled fields is a dozen
335
+ // WRONG_FIELD_NAME findings, each already a doctor error with its full
336
+ // message. The gate names the shape and the first few, not all of them.
337
+ const SHOWN = 5;
338
+ const headline = findings.slice(0, SHOWN).map((item) => `${item.code_name} on ${item.file || item.page_id}`);
339
+ const more = findings.length > SHOWN ? `; plus ${findings.length - SHOWN} more` : "";
340
+ return {
341
+ ...base,
342
+ status: "blocked",
343
+ code: SDK_MARKUP,
344
+ reason: `${findings.length} SDK markup blocker(s) on ${list.length} built page(s) (${headline.join("; ")}${more})${warned.length ? `, and ${warned.length} advisory finding(s) held in warned[] until the blockers clear` : ""}; each blocker is a doctor error with the repair.`,
345
+ findings,
346
+ warned,
347
+ unknown_attributes,
348
+ pages_scanned: list.length,
349
+ required_actions: [
350
+ {
351
+ id: "repair_sdk_markup",
352
+ kind: "edit",
353
+ command: null,
354
+ description: `Repair the markup each finding names (${headline.join("; ")}${more}), rebuild, and re-run doctor. Not waivable: the markup provably does not do what it says.`,
355
+ },
356
+ ],
357
+ };
358
+ }
@@ -0,0 +1,52 @@
1
+ // The one list of CampaignSpec `sdk_hints.meta_tags` keys the Campaign Cart
2
+ // SDK does not read. Doctor (validateBuiltSdkMetaTags) and QA
3
+ // (unsupportedSdkMetaHint) both consume it, so a tag can never be "required"
4
+ // by one and "ignored" by the other: the SDK is the authority, and a spec that
5
+ // still lists one of these keys carries a stale Map page hint, not a missing
6
+ // rendered tag.
7
+ //
8
+ // Verified against the SDK source: it reads next-api-key, next-page-type,
9
+ // next-success-url, next-upsell-accept-url, next-upsell-decline-url,
10
+ // next-failure-url, next-clear-cart, next-debug and the like. It reads
11
+ // neither key below. The Map Builder export stopped emitting both; older
12
+ // specs still carry them.
13
+
14
+ export const SDK_IGNORED_META_TAGS = Object.freeze({
15
+ "next-currency": Object.freeze({
16
+ expected: "Campaign Cart currency from the currency URL parameter, remembered session choice, or SDK default",
17
+ actual: "No page-level currency override to verify",
18
+ note: "Campaign Cart does not read a next-currency meta tag; remove it from the Map's page hints. Currency behavior is optional and must be verified through the documented URL/session/default flow.",
19
+ }),
20
+ "next-predictive-address": Object.freeze({
21
+ expected: "window.nextConfig.addressConfig.enableAutocomplete",
22
+ actual: "Autocomplete config requires browser/config review",
23
+ note: "Campaign Cart does not read a next-predictive-address meta tag; remove it from the Map's page hints. Predictive address is optional and configured through window.nextConfig.addressConfig.enableAutocomplete.",
24
+ }),
25
+ });
26
+
27
+ // Meta names reach the consumers from spec-declared keys and from parsed HTML
28
+ // whose `name` value is not trimmed; normalize case and surrounding
29
+ // whitespace so a stray-space tag matches the intended entry.
30
+ export function normalizeSdkMetaName(name) {
31
+ return String(name || "").trim().toLowerCase();
32
+ }
33
+
34
+ // The map entry for an SDK-ignored meta tag, or null for a tag the SDK reads.
35
+ export function lookupSdkIgnoredMetaTag(name) {
36
+ const normalized = normalizeSdkMetaName(name);
37
+ return Object.hasOwn(SDK_IGNORED_META_TAGS, normalized) ? SDK_IGNORED_META_TAGS[normalized] : null;
38
+ }
39
+
40
+ // Boolean form for the filter/branch sites that only need to know.
41
+ export function isSdkIgnoredMetaTag(name) {
42
+ return lookupSdkIgnoredMetaTag(name) !== null;
43
+ }
44
+
45
+ // One doctor-facing line for a set of ignored tags listed on a page: names each
46
+ // tag with its note so the operator reads the reason without opening QA.
47
+ export function describeSdkIgnoredMetaTags(names) {
48
+ return [...new Set(names.map(normalizeSdkMetaName))]
49
+ .filter((name) => Object.hasOwn(SDK_IGNORED_META_TAGS, name))
50
+ .map((name) => `"${name}" (${SDK_IGNORED_META_TAGS[name].note})`)
51
+ .join("; ");
52
+ }
@@ -0,0 +1,7 @@
1
+ export function shellToken(value) {
2
+ // null and undefined are "no value"; 0, false and NaN are values and print as themselves.
3
+ const text = String(value ?? "");
4
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) return text;
5
+ return `'${text.replace(/'/g, "'\\''")}'`;
6
+ }
7
+