@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.
- package/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- 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
|
+
}
|