@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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routing — the single source of truth for a page's outgoing edges.
|
|
3
|
+
*
|
|
4
|
+
* A campaign is a free-form headless shopping journey. It usually runs
|
|
5
|
+
* landing → checkout → upsells → receipt, but nothing requires that, and the
|
|
6
|
+
* routing fields a page declares are the author's statement of where the
|
|
7
|
+
* shopper goes next. Before this module those fields were interpreted by three
|
|
8
|
+
* independent page-type tables — source intake, cycle detection, and the QA
|
|
9
|
+
* topology extractor — which disagreed with each other and silently discarded
|
|
10
|
+
* any edge declared outside their own table.
|
|
11
|
+
*
|
|
12
|
+
* Everything that asks "where does this page go" now asks here, and every
|
|
13
|
+
* caller models the SAME traversable edges — the forward link the page resolves
|
|
14
|
+
* to, plus its decline branch. They differ only in how many of those they need:
|
|
15
|
+
*
|
|
16
|
+
* forwardRouteTarget() — the ONE forward link, for wiring (build output, QA
|
|
17
|
+
* expectations), which emits a single next URL.
|
|
18
|
+
* outgoingEdgeIds() — forward plus decline, for graph analysis (cycle
|
|
19
|
+
* detection), which must consider both branches.
|
|
20
|
+
*
|
|
21
|
+
* An earlier revision had outgoingEdgeIds return every DECLARED field on the
|
|
22
|
+
* theory that a safety analysis should over-approximate. That was wrong, and
|
|
23
|
+
* cost real correctness: a page whose `success_url` wins at runtime and
|
|
24
|
+
* terminates cleanly was reported as a release-blocking cycle through an unused,
|
|
25
|
+
* shadowed `next_page`. The runtime can only ever take the forward link or the
|
|
26
|
+
* decline branch, so following a field neither of them selects does not find
|
|
27
|
+
* extra bugs — it invents them. Accuracy is the safety property here, not
|
|
28
|
+
* breadth.
|
|
29
|
+
*
|
|
30
|
+
* One narrowing sits on top of that (#234): a field whose meaning the page's
|
|
31
|
+
* type cannot satisfy is not an edge either. `success_url` means "after
|
|
32
|
+
* payment" and `on_accept` means "after accepting this page's offer"; a page
|
|
33
|
+
* with no payment and no offer can satisfy neither, and honouring a
|
|
34
|
+
* copy-pasted one routed real shoppers past the checkout. This is NOT the
|
|
35
|
+
* page-type gate #230 removed — that dropped edges the author declared on
|
|
36
|
+
* tables that disagreed about which fields a type may use. `next_page` stays
|
|
37
|
+
* honoured on every page type. The carve-out is about what two fields MEAN.
|
|
38
|
+
*/
|
|
39
|
+
import type { Page } from './types.ts';
|
|
40
|
+
/**
|
|
41
|
+
* Forward-route fields in specific-before-generic precedence. `on_accept` and
|
|
42
|
+
* `success_url` each name a particular branch (upsell accept, order success);
|
|
43
|
+
* `next_page` is the generic "wherever this page goes next", so it loses to
|
|
44
|
+
* either. Order is load-bearing — `forwardRouteTarget` returns the first match
|
|
45
|
+
* — and is pinned by test, not only by this comment.
|
|
46
|
+
*
|
|
47
|
+
* Precedence is not the whole answer. `on_accept` and `success_url` each carry
|
|
48
|
+
* a meaning only some page types can satisfy, and a field its page cannot
|
|
49
|
+
* satisfy is skipped before precedence is consulted at all. `next_page` is the
|
|
50
|
+
* generic one and stays honoured everywhere. See FORWARD_FIELD_APPLICABILITY.
|
|
51
|
+
*/
|
|
52
|
+
export declare const FORWARD_ROUTE_FIELDS: readonly ["on_accept", "success_url", "next_page"];
|
|
53
|
+
/** The accept branch. Also a forward field, so it appears in both lists. */
|
|
54
|
+
export declare const ACCEPT_ROUTE_FIELD: "on_accept";
|
|
55
|
+
/** The decline branch. Separate because it is a second edge, not a fallback. */
|
|
56
|
+
export declare const DECLINE_ROUTE_FIELD: "on_decline";
|
|
57
|
+
/**
|
|
58
|
+
* Page types that take payment, and are therefore the only types that can
|
|
59
|
+
* satisfy `success_url`.
|
|
60
|
+
*
|
|
61
|
+
* `success_url` means "where the shopper goes after payment succeeds". Only a
|
|
62
|
+
* page that takes payment has a payment to succeed, so on any other type the
|
|
63
|
+
* field names an event that cannot happen there. In practice it is a
|
|
64
|
+
* copy-paste down from the checkout below it, and honouring it routes the
|
|
65
|
+
* shopper straight past the step they were meant to reach: a `select` page
|
|
66
|
+
* declaring `next_page: checkout` alongside `success_url: upsell` wired the
|
|
67
|
+
* upsell and skipped payment entirely.
|
|
68
|
+
*
|
|
69
|
+
* This is NOT the page-type routing gate #230 removed. That gate DROPPED edges
|
|
70
|
+
* an author had declared, on tables that disagreed about which fields a type
|
|
71
|
+
* was allowed to use. `next_page` and `on_accept` stay type-agnostic — every
|
|
72
|
+
* page may declare them and every declaration is honoured. The carve-out here
|
|
73
|
+
* is about one field's MEANING: reading a payment-shaped field on a page with
|
|
74
|
+
* no payment is not respecting the author's intent, it is inventing one.
|
|
75
|
+
*
|
|
76
|
+
* Membership is deliberately narrow. The three-step shop family types its
|
|
77
|
+
* `information` / `shipping` / `billing` pages as `checkout`, so they are
|
|
78
|
+
* already covered; no certified family carries a second payment-bearing type.
|
|
79
|
+
* An `upsell` takes a one-click payment but expresses its post-purchase branch
|
|
80
|
+
* through `on_accept`, which has its own applicability rule below.
|
|
81
|
+
* Ratified on campaigns-os#234, 2026-08-25.
|
|
82
|
+
*/
|
|
83
|
+
export declare const PAYMENT_BEARING_PAGE_TYPES: readonly ["checkout"];
|
|
84
|
+
/**
|
|
85
|
+
* Page types that present an offer the shopper can accept or decline, and are
|
|
86
|
+
* therefore the only types that can satisfy `on_accept`.
|
|
87
|
+
*
|
|
88
|
+
* Same reasoning as PAYMENT_BEARING_PAGE_TYPES, one field over. `on_accept`
|
|
89
|
+
* means "where the shopper goes after accepting the offer on this page". A
|
|
90
|
+
* page that presents no offer has no acceptance to branch on, so the field
|
|
91
|
+
* names an event that cannot happen there.
|
|
92
|
+
*
|
|
93
|
+
* This is not a hypothetical tidy-up. `on_accept` sits at the TOP of
|
|
94
|
+
* FORWARD_ROUTE_FIELDS, so before this rule existed a `select` or `landing`
|
|
95
|
+
* page declaring `next_page: "checkout"` alongside a copy-pasted
|
|
96
|
+
* `on_accept: "upsell"` wired the upsell and routed the shopper straight past
|
|
97
|
+
* payment — the identical break #234 fixed for `success_url`, reachable
|
|
98
|
+
* through the sibling field at higher precedence. Gating one field closed the
|
|
99
|
+
* reported instance; gating both closes the class.
|
|
100
|
+
*
|
|
101
|
+
* `downsell` is included because UpsellRoutingComplete already requires
|
|
102
|
+
* `on_accept` and `on_decline` on exactly `upsell` and `downsell`; leaving
|
|
103
|
+
* `downsell` out would make routing reject a field another rule demands.
|
|
104
|
+
*/
|
|
105
|
+
export declare const OFFER_BEARING_PAGE_TYPES: readonly ["upsell", "downsell"];
|
|
106
|
+
/**
|
|
107
|
+
* A forward field whose meaning only some page types can satisfy: the types
|
|
108
|
+
* that can, plus the plain-English meaning that explains why.
|
|
109
|
+
*/
|
|
110
|
+
export interface ForwardFieldApplicability {
|
|
111
|
+
/** Page types that can satisfy the field. */
|
|
112
|
+
readonly requiredTypes: readonly string[];
|
|
113
|
+
/** What the field means, phrased to complete "it means ...". */
|
|
114
|
+
readonly meaning: string;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Every field that can carry a route, for consumers that need to enumerate them
|
|
118
|
+
* (validation, tooling). This is NOT the edge set: which of these a page can
|
|
119
|
+
* actually traverse is decided by forwardRouteTarget/declineRouteTarget, since
|
|
120
|
+
* a lower-precedence forward field is shadowed and never taken.
|
|
121
|
+
*/
|
|
122
|
+
export declare const ROUTE_FIELDS: readonly ["on_accept", "success_url", "next_page", "on_decline"];
|
|
123
|
+
/**
|
|
124
|
+
* The single forward link, or null when the page declares none. Type-agnostic
|
|
125
|
+
* in the direction that matters: a journey that routes its checkout through
|
|
126
|
+
* `next_page`, or continues past a thank-you page into a second offer
|
|
127
|
+
* sequence, is unusual but entirely legal, and the author has said exactly
|
|
128
|
+
* what they meant. `next_page` is honoured on every page type without
|
|
129
|
+
* exception. `on_accept` and `success_url` are honoured only where they mean
|
|
130
|
+
* something — see FORWARD_FIELD_APPLICABILITY.
|
|
131
|
+
*/
|
|
132
|
+
export declare function forwardRouteTarget(page: Page | null | undefined): string | null;
|
|
133
|
+
/**
|
|
134
|
+
* What `field` means and which page types can satisfy it, or null when the
|
|
135
|
+
* field applies everywhere. Exported so a diagnostic can explain WHY a field
|
|
136
|
+
* was skipped without hand-writing a second copy of the rule it reports on.
|
|
137
|
+
*/
|
|
138
|
+
export declare function describeForwardField(field: string): ForwardFieldApplicability | null;
|
|
139
|
+
/**
|
|
140
|
+
* Forward fields this page's type CAN route from, declared or not. The
|
|
141
|
+
* complement of the applicability table rather than of what the author wrote,
|
|
142
|
+
* because its job is to answer "what should I have set instead" — a question
|
|
143
|
+
* about the page type, not about this spec.
|
|
144
|
+
*
|
|
145
|
+
* Kept here rather than derived at a call site: a consumer filtering
|
|
146
|
+
* FORWARD_ROUTE_FIELDS against `inapplicableForwardFields` would get the wrong
|
|
147
|
+
* answer, since that list only names fields the author actually declared. An
|
|
148
|
+
* upsell that declares no `success_url` would come back as though `success_url`
|
|
149
|
+
* were a legitimate option for it.
|
|
150
|
+
*/
|
|
151
|
+
export declare function applicableForwardFields(page: Page | null | undefined): string[];
|
|
152
|
+
/**
|
|
153
|
+
* Forward fields this page declares with a real target but which its type
|
|
154
|
+
* cannot satisfy, so routing skips them. Empty for almost every page; the
|
|
155
|
+
* diagnostic that teaches authors about a stray `success_url` reads it, rather
|
|
156
|
+
* than re-deriving the type rule and drifting from the resolver.
|
|
157
|
+
*/
|
|
158
|
+
export declare function inapplicableForwardFields(page: Page | null | undefined): string[];
|
|
159
|
+
/**
|
|
160
|
+
* The accept-branch target: `on_accept`, but only where the page can satisfy
|
|
161
|
+
* it. Distinct from `forwardRouteTarget`, which answers "the ONE forward link"
|
|
162
|
+
* and may resolve to a different field; this answers "where does accepting
|
|
163
|
+
* this page's offer go", which is a question only an offer page can be asked.
|
|
164
|
+
*
|
|
165
|
+
* Exists because reading `page.on_accept` raw is now wrong. The QA topology
|
|
166
|
+
* extractor did exactly that and, after #234 gated the field, emitted an
|
|
167
|
+
* `expected_accept_url` for a `select` page's inert `on_accept` — so QA looked
|
|
168
|
+
* for an accept link the built page correctly does not have, and flagged a
|
|
169
|
+
* correct build. Every consumer asks the resolver; that is the whole point of
|
|
170
|
+
* this module.
|
|
171
|
+
*/
|
|
172
|
+
export declare function acceptRouteTarget(page: Page | null | undefined): string | null;
|
|
173
|
+
/** The decline-branch target, wherever it is declared. */
|
|
174
|
+
export declare function declineRouteTarget(page: Page | null | undefined): string | null;
|
|
175
|
+
/**
|
|
176
|
+
* True when the page RESOLVES to a forward link. Not the same as declaring a
|
|
177
|
+
* forward field: a field the page's type cannot satisfy does not count, so a
|
|
178
|
+
* `select` page carrying only `success_url` reports false. CheckoutHasSuccessUrl
|
|
179
|
+
* reads this, and "can the shopper continue" is the question it means to ask.
|
|
180
|
+
*/
|
|
181
|
+
export declare function hasForwardRoute(page: Page | null | undefined): boolean;
|
|
182
|
+
/**
|
|
183
|
+
* The distinct edges this page can actually traverse: its forward link and its
|
|
184
|
+
* decline branch, deduplicated, forward first. Two kinds of declared field are
|
|
185
|
+
* excluded on purpose: one shadowed by a higher-precedence field (see the
|
|
186
|
+
* module note), and one whose page type cannot satisfy it (see
|
|
187
|
+
* PAYMENT_BEARING_PAGE_TYPES). Neither can be taken at runtime, so following
|
|
188
|
+
* either would invent a cycle rather than find one.
|
|
189
|
+
*/
|
|
190
|
+
export declare function outgoingEdgeIds(page: Page | null | undefined): string[];
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routing — the single source of truth for a page's outgoing edges.
|
|
3
|
+
*
|
|
4
|
+
* A campaign is a free-form headless shopping journey. It usually runs
|
|
5
|
+
* landing → checkout → upsells → receipt, but nothing requires that, and the
|
|
6
|
+
* routing fields a page declares are the author's statement of where the
|
|
7
|
+
* shopper goes next. Before this module those fields were interpreted by three
|
|
8
|
+
* independent page-type tables — source intake, cycle detection, and the QA
|
|
9
|
+
* topology extractor — which disagreed with each other and silently discarded
|
|
10
|
+
* any edge declared outside their own table.
|
|
11
|
+
*
|
|
12
|
+
* Everything that asks "where does this page go" now asks here, and every
|
|
13
|
+
* caller models the SAME traversable edges — the forward link the page resolves
|
|
14
|
+
* to, plus its decline branch. They differ only in how many of those they need:
|
|
15
|
+
*
|
|
16
|
+
* forwardRouteTarget() — the ONE forward link, for wiring (build output, QA
|
|
17
|
+
* expectations), which emits a single next URL.
|
|
18
|
+
* outgoingEdgeIds() — forward plus decline, for graph analysis (cycle
|
|
19
|
+
* detection), which must consider both branches.
|
|
20
|
+
*
|
|
21
|
+
* An earlier revision had outgoingEdgeIds return every DECLARED field on the
|
|
22
|
+
* theory that a safety analysis should over-approximate. That was wrong, and
|
|
23
|
+
* cost real correctness: a page whose `success_url` wins at runtime and
|
|
24
|
+
* terminates cleanly was reported as a release-blocking cycle through an unused,
|
|
25
|
+
* shadowed `next_page`. The runtime can only ever take the forward link or the
|
|
26
|
+
* decline branch, so following a field neither of them selects does not find
|
|
27
|
+
* extra bugs — it invents them. Accuracy is the safety property here, not
|
|
28
|
+
* breadth.
|
|
29
|
+
*
|
|
30
|
+
* One narrowing sits on top of that (#234): a field whose meaning the page's
|
|
31
|
+
* type cannot satisfy is not an edge either. `success_url` means "after
|
|
32
|
+
* payment" and `on_accept` means "after accepting this page's offer"; a page
|
|
33
|
+
* with no payment and no offer can satisfy neither, and honouring a
|
|
34
|
+
* copy-pasted one routed real shoppers past the checkout. This is NOT the
|
|
35
|
+
* page-type gate #230 removed — that dropped edges the author declared on
|
|
36
|
+
* tables that disagreed about which fields a type may use. `next_page` stays
|
|
37
|
+
* honoured on every page type. The carve-out is about what two fields MEAN.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Forward-route fields in specific-before-generic precedence. `on_accept` and
|
|
41
|
+
* `success_url` each name a particular branch (upsell accept, order success);
|
|
42
|
+
* `next_page` is the generic "wherever this page goes next", so it loses to
|
|
43
|
+
* either. Order is load-bearing — `forwardRouteTarget` returns the first match
|
|
44
|
+
* — and is pinned by test, not only by this comment.
|
|
45
|
+
*
|
|
46
|
+
* Precedence is not the whole answer. `on_accept` and `success_url` each carry
|
|
47
|
+
* a meaning only some page types can satisfy, and a field its page cannot
|
|
48
|
+
* satisfy is skipped before precedence is consulted at all. `next_page` is the
|
|
49
|
+
* generic one and stays honoured everywhere. See FORWARD_FIELD_APPLICABILITY.
|
|
50
|
+
*/
|
|
51
|
+
export const FORWARD_ROUTE_FIELDS = Object.freeze([
|
|
52
|
+
'on_accept',
|
|
53
|
+
'success_url',
|
|
54
|
+
'next_page',
|
|
55
|
+
]);
|
|
56
|
+
/** The accept branch. Also a forward field, so it appears in both lists. */
|
|
57
|
+
export const ACCEPT_ROUTE_FIELD = 'on_accept';
|
|
58
|
+
/** The decline branch. Separate because it is a second edge, not a fallback. */
|
|
59
|
+
export const DECLINE_ROUTE_FIELD = 'on_decline';
|
|
60
|
+
/**
|
|
61
|
+
* Page types that take payment, and are therefore the only types that can
|
|
62
|
+
* satisfy `success_url`.
|
|
63
|
+
*
|
|
64
|
+
* `success_url` means "where the shopper goes after payment succeeds". Only a
|
|
65
|
+
* page that takes payment has a payment to succeed, so on any other type the
|
|
66
|
+
* field names an event that cannot happen there. In practice it is a
|
|
67
|
+
* copy-paste down from the checkout below it, and honouring it routes the
|
|
68
|
+
* shopper straight past the step they were meant to reach: a `select` page
|
|
69
|
+
* declaring `next_page: checkout` alongside `success_url: upsell` wired the
|
|
70
|
+
* upsell and skipped payment entirely.
|
|
71
|
+
*
|
|
72
|
+
* This is NOT the page-type routing gate #230 removed. That gate DROPPED edges
|
|
73
|
+
* an author had declared, on tables that disagreed about which fields a type
|
|
74
|
+
* was allowed to use. `next_page` and `on_accept` stay type-agnostic — every
|
|
75
|
+
* page may declare them and every declaration is honoured. The carve-out here
|
|
76
|
+
* is about one field's MEANING: reading a payment-shaped field on a page with
|
|
77
|
+
* no payment is not respecting the author's intent, it is inventing one.
|
|
78
|
+
*
|
|
79
|
+
* Membership is deliberately narrow. The three-step shop family types its
|
|
80
|
+
* `information` / `shipping` / `billing` pages as `checkout`, so they are
|
|
81
|
+
* already covered; no certified family carries a second payment-bearing type.
|
|
82
|
+
* An `upsell` takes a one-click payment but expresses its post-purchase branch
|
|
83
|
+
* through `on_accept`, which has its own applicability rule below.
|
|
84
|
+
* Ratified on campaigns-os#234, 2026-08-25.
|
|
85
|
+
*/
|
|
86
|
+
export const PAYMENT_BEARING_PAGE_TYPES = Object.freeze(['checkout']);
|
|
87
|
+
/**
|
|
88
|
+
* Page types that present an offer the shopper can accept or decline, and are
|
|
89
|
+
* therefore the only types that can satisfy `on_accept`.
|
|
90
|
+
*
|
|
91
|
+
* Same reasoning as PAYMENT_BEARING_PAGE_TYPES, one field over. `on_accept`
|
|
92
|
+
* means "where the shopper goes after accepting the offer on this page". A
|
|
93
|
+
* page that presents no offer has no acceptance to branch on, so the field
|
|
94
|
+
* names an event that cannot happen there.
|
|
95
|
+
*
|
|
96
|
+
* This is not a hypothetical tidy-up. `on_accept` sits at the TOP of
|
|
97
|
+
* FORWARD_ROUTE_FIELDS, so before this rule existed a `select` or `landing`
|
|
98
|
+
* page declaring `next_page: "checkout"` alongside a copy-pasted
|
|
99
|
+
* `on_accept: "upsell"` wired the upsell and routed the shopper straight past
|
|
100
|
+
* payment — the identical break #234 fixed for `success_url`, reachable
|
|
101
|
+
* through the sibling field at higher precedence. Gating one field closed the
|
|
102
|
+
* reported instance; gating both closes the class.
|
|
103
|
+
*
|
|
104
|
+
* `downsell` is included because UpsellRoutingComplete already requires
|
|
105
|
+
* `on_accept` and `on_decline` on exactly `upsell` and `downsell`; leaving
|
|
106
|
+
* `downsell` out would make routing reject a field another rule demands.
|
|
107
|
+
*/
|
|
108
|
+
export const OFFER_BEARING_PAGE_TYPES = Object.freeze(['upsell', 'downsell']);
|
|
109
|
+
/**
|
|
110
|
+
* Forward fields whose meaning depends on the page type. A field absent from
|
|
111
|
+
* this table applies everywhere.
|
|
112
|
+
*
|
|
113
|
+
* The `meaning` string lives HERE, beside the types, and not in the rule that
|
|
114
|
+
* reports an ignored field. A rule that hand-wrote its own explanation would
|
|
115
|
+
* be a second derivation of this table: correct today, and quietly wrong the
|
|
116
|
+
* first time a second field or a second payment-bearing type joins it.
|
|
117
|
+
*/
|
|
118
|
+
const FORWARD_FIELD_APPLICABILITY = Object.freeze({
|
|
119
|
+
on_accept: Object.freeze({
|
|
120
|
+
requiredTypes: OFFER_BEARING_PAGE_TYPES,
|
|
121
|
+
meaning: 'where the shopper goes after accepting the offer on this page',
|
|
122
|
+
}),
|
|
123
|
+
success_url: Object.freeze({
|
|
124
|
+
requiredTypes: PAYMENT_BEARING_PAGE_TYPES,
|
|
125
|
+
meaning: 'where the shopper goes after payment succeeds',
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* Every field that can carry a route, for consumers that need to enumerate them
|
|
130
|
+
* (validation, tooling). This is NOT the edge set: which of these a page can
|
|
131
|
+
* actually traverse is decided by forwardRouteTarget/declineRouteTarget, since
|
|
132
|
+
* a lower-precedence forward field is shadowed and never taken.
|
|
133
|
+
*/
|
|
134
|
+
export const ROUTE_FIELDS = Object.freeze([
|
|
135
|
+
...FORWARD_ROUTE_FIELDS,
|
|
136
|
+
DECLINE_ROUTE_FIELD,
|
|
137
|
+
]);
|
|
138
|
+
/**
|
|
139
|
+
* Whether `field` carries a meaning this page can satisfy. Type-agnostic for
|
|
140
|
+
* every field except the ones in FORWARD_FIELD_APPLICABILITY.
|
|
141
|
+
*/
|
|
142
|
+
function fieldAppliesTo(page, field) {
|
|
143
|
+
const rule = FORWARD_FIELD_APPLICABILITY[field];
|
|
144
|
+
if (!rule)
|
|
145
|
+
return true;
|
|
146
|
+
const type = page?.type;
|
|
147
|
+
if (typeof type !== 'string')
|
|
148
|
+
return false;
|
|
149
|
+
// Trimmed and case-folded on purpose. This is the first thing that ever let
|
|
150
|
+
// `page.type` decide an edge, and normalize() does not touch that field, so
|
|
151
|
+
// an exact match would let `type: "Checkout"` silently drop a real checkout's
|
|
152
|
+
// success_url — the precise failure this module exists to prevent. Compare
|
|
153
|
+
// with RouteTargetResolves, which deliberately does NOT fold case: there
|
|
154
|
+
// folding would PASS a target the build then fails to resolve, so leniency
|
|
155
|
+
// hides a break. Here leniency prevents one.
|
|
156
|
+
return rule.requiredTypes.includes(type.trim().toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
function declared(page, field) {
|
|
159
|
+
const value = page?.[field];
|
|
160
|
+
if (typeof value !== 'string')
|
|
161
|
+
return null;
|
|
162
|
+
// Return the NORMALIZED value. Returning the raw one tested trimmed but
|
|
163
|
+
// handed back untrimmed made `success_url: " landing "` wire a live link
|
|
164
|
+
// (intake trims downstream) while cycle detection's pageMap.get(" landing ")
|
|
165
|
+
// missed — reopening the exact blind spot this module exists to close.
|
|
166
|
+
return value.trim() || null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The single forward link, or null when the page declares none. Type-agnostic
|
|
170
|
+
* in the direction that matters: a journey that routes its checkout through
|
|
171
|
+
* `next_page`, or continues past a thank-you page into a second offer
|
|
172
|
+
* sequence, is unusual but entirely legal, and the author has said exactly
|
|
173
|
+
* what they meant. `next_page` is honoured on every page type without
|
|
174
|
+
* exception. `on_accept` and `success_url` are honoured only where they mean
|
|
175
|
+
* something — see FORWARD_FIELD_APPLICABILITY.
|
|
176
|
+
*/
|
|
177
|
+
export function forwardRouteTarget(page) {
|
|
178
|
+
for (const field of FORWARD_ROUTE_FIELDS) {
|
|
179
|
+
if (!fieldAppliesTo(page, field))
|
|
180
|
+
continue;
|
|
181
|
+
const value = declared(page, field);
|
|
182
|
+
if (value)
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* What `field` means and which page types can satisfy it, or null when the
|
|
189
|
+
* field applies everywhere. Exported so a diagnostic can explain WHY a field
|
|
190
|
+
* was skipped without hand-writing a second copy of the rule it reports on.
|
|
191
|
+
*/
|
|
192
|
+
export function describeForwardField(field) {
|
|
193
|
+
return FORWARD_FIELD_APPLICABILITY[field] ?? null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Forward fields this page's type CAN route from, declared or not. The
|
|
197
|
+
* complement of the applicability table rather than of what the author wrote,
|
|
198
|
+
* because its job is to answer "what should I have set instead" — a question
|
|
199
|
+
* about the page type, not about this spec.
|
|
200
|
+
*
|
|
201
|
+
* Kept here rather than derived at a call site: a consumer filtering
|
|
202
|
+
* FORWARD_ROUTE_FIELDS against `inapplicableForwardFields` would get the wrong
|
|
203
|
+
* answer, since that list only names fields the author actually declared. An
|
|
204
|
+
* upsell that declares no `success_url` would come back as though `success_url`
|
|
205
|
+
* were a legitimate option for it.
|
|
206
|
+
*/
|
|
207
|
+
export function applicableForwardFields(page) {
|
|
208
|
+
return FORWARD_ROUTE_FIELDS.filter((field) => fieldAppliesTo(page, field));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Forward fields this page declares with a real target but which its type
|
|
212
|
+
* cannot satisfy, so routing skips them. Empty for almost every page; the
|
|
213
|
+
* diagnostic that teaches authors about a stray `success_url` reads it, rather
|
|
214
|
+
* than re-deriving the type rule and drifting from the resolver.
|
|
215
|
+
*/
|
|
216
|
+
export function inapplicableForwardFields(page) {
|
|
217
|
+
return FORWARD_ROUTE_FIELDS.filter((field) => !fieldAppliesTo(page, field) && declared(page, field) !== null);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The accept-branch target: `on_accept`, but only where the page can satisfy
|
|
221
|
+
* it. Distinct from `forwardRouteTarget`, which answers "the ONE forward link"
|
|
222
|
+
* and may resolve to a different field; this answers "where does accepting
|
|
223
|
+
* this page's offer go", which is a question only an offer page can be asked.
|
|
224
|
+
*
|
|
225
|
+
* Exists because reading `page.on_accept` raw is now wrong. The QA topology
|
|
226
|
+
* extractor did exactly that and, after #234 gated the field, emitted an
|
|
227
|
+
* `expected_accept_url` for a `select` page's inert `on_accept` — so QA looked
|
|
228
|
+
* for an accept link the built page correctly does not have, and flagged a
|
|
229
|
+
* correct build. Every consumer asks the resolver; that is the whole point of
|
|
230
|
+
* this module.
|
|
231
|
+
*/
|
|
232
|
+
export function acceptRouteTarget(page) {
|
|
233
|
+
return fieldAppliesTo(page, ACCEPT_ROUTE_FIELD) ? declared(page, ACCEPT_ROUTE_FIELD) : null;
|
|
234
|
+
}
|
|
235
|
+
/** The decline-branch target, wherever it is declared. */
|
|
236
|
+
export function declineRouteTarget(page) {
|
|
237
|
+
return declared(page, DECLINE_ROUTE_FIELD);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* True when the page RESOLVES to a forward link. Not the same as declaring a
|
|
241
|
+
* forward field: a field the page's type cannot satisfy does not count, so a
|
|
242
|
+
* `select` page carrying only `success_url` reports false. CheckoutHasSuccessUrl
|
|
243
|
+
* reads this, and "can the shopper continue" is the question it means to ask.
|
|
244
|
+
*/
|
|
245
|
+
export function hasForwardRoute(page) {
|
|
246
|
+
return forwardRouteTarget(page) !== null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The distinct edges this page can actually traverse: its forward link and its
|
|
250
|
+
* decline branch, deduplicated, forward first. Two kinds of declared field are
|
|
251
|
+
* excluded on purpose: one shadowed by a higher-precedence field (see the
|
|
252
|
+
* module note), and one whose page type cannot satisfy it (see
|
|
253
|
+
* PAYMENT_BEARING_PAGE_TYPES). Neither can be taken at runtime, so following
|
|
254
|
+
* either would invent a cycle rather than find one.
|
|
255
|
+
*/
|
|
256
|
+
export function outgoingEdgeIds(page) {
|
|
257
|
+
const ids = [];
|
|
258
|
+
for (const value of [forwardRouteTarget(page), declineRouteTarget(page)]) {
|
|
259
|
+
if (value && !ids.includes(value))
|
|
260
|
+
ids.push(value);
|
|
261
|
+
}
|
|
262
|
+
return ids;
|
|
263
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalyticsContractShape — validates the optional top-level `analytics` block
|
|
3
|
+
* when present. The block declares a campaign's analytics/attribution/param
|
|
4
|
+
* contract so doctor + QA can validate against intent (cf. the Chamelo Shield
|
|
5
|
+
* `?reviews=n`-has-no-handler QA finding and the Walla Sound Redtrack param
|
|
6
|
+
* conflict — both are gaps that had no declared contract to check against).
|
|
7
|
+
*
|
|
8
|
+
* The block is fully OPTIONAL — a spec without `analytics` is silent (SDK
|
|
9
|
+
* defaults apply, exactly as today). When `analytics` IS set, this rule catches
|
|
10
|
+
* authoring drift before doctor/QA see it. Every check is `warning` severity:
|
|
11
|
+
* authoring guidance, not a build blocker (matches DesignSourceShape).
|
|
12
|
+
*
|
|
13
|
+
* Checks:
|
|
14
|
+
* 0. If `analytics` is present but not a plain object (a non-plain-object
|
|
15
|
+
* value: string, array, number, boolean, boxed primitive, class instance,
|
|
16
|
+
* etc.), warn once and stop — there is no contract shape to inspect.
|
|
17
|
+
* Genuinely-absent `analytics` stays silent (optional, non-gating).
|
|
18
|
+
* 1. `mode`, if present, is one of auto | manual | disabled.
|
|
19
|
+
* 2. Each provider: `blockedEvents` (when present) is a string[], and each
|
|
20
|
+
* entry is a known SDK `dl_*` event (a misspelled/legacy name like
|
|
21
|
+
* "purchase" blocks nothing — the original drift bug this keystone closes);
|
|
22
|
+
* an enabled gtm provider should declare `containerId`, facebook `pixelId`,
|
|
23
|
+
* custom `endpoint` (warning — the id is what doctor/QA bind to).
|
|
24
|
+
* 3. Each `out_of_band_pixels[]` entry has a non-empty `vendor`.
|
|
25
|
+
* 4. Each `manual_events[]` entry has a non-empty `event`; if it names a
|
|
26
|
+
* `page`, that page id must exist; a purchase manual event SHOULD name a
|
|
27
|
+
* page (the first-upsell placement footgun — beacons lost in the
|
|
28
|
+
* checkout→upsell redirect when placed on checkout).
|
|
29
|
+
* 5. Each `params.content[]` entry has a non-empty `name`; referenced `pages`
|
|
30
|
+
* must exist (a content param pointing at a missing page is the
|
|
31
|
+
* `?reviews=n`-with-no-handler gap, inverted).
|
|
32
|
+
* 6. `params.tracking.click_id`, when present, declares both `inbound` and
|
|
33
|
+
* `maps_to` (half a mapping silently drops the affiliate click id).
|
|
34
|
+
* 7. `params.tracking.preserve` / `utmTransfer.paramsToCopy`, when present,
|
|
35
|
+
* are string[].
|
|
36
|
+
* 8. If analytics is active (the block is present and mode is not
|
|
37
|
+
* "disabled"; missing mode means SDK defaults apply), runtime/global_config
|
|
38
|
+
* sdk_version should be an exact released semver >= the SDK identity
|
|
39
|
+
* baseline so events carry campaign/session ids.
|
|
40
|
+
*/
|
|
41
|
+
import type { Rule } from '../types.ts';
|
|
42
|
+
export declare const AnalyticsContractShape: Rule;
|