@nextcommerce/campaigns-os 1.61.0 → 1.63.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 +2 -1
- package/CHANGELOG.md +127 -0
- package/README.md +86 -438
- package/agents/claude/CLAUDE.md +9 -4
- package/agents/codex/AGENTS.md +8 -4
- package/agents/copilot/copilot-instructions.md +8 -4
- package/agents/cursor/campaigns-os.mdc +10 -4
- package/campaign-spec/README.md +7 -0
- package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
- package/campaign-spec/dist/analytics-vendors.js +44 -0
- package/campaign-spec/dist/checkout-flow.d.ts +18 -0
- package/campaign-spec/dist/checkout-flow.js +28 -0
- package/campaign-spec/dist/index.d.ts +4 -2
- package/campaign-spec/dist/index.js +5 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
- package/campaign-spec/dist/rules/campaign-currencies.d.ts +30 -0
- package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
- package/campaign-spec/dist/rules/index.d.ts +3 -0
- package/campaign-spec/dist/rules/index.js +9 -0
- package/campaign-spec/dist/rules/package-currency-prices.d.ts +29 -0
- package/campaign-spec/dist/rules/package-currency-prices.js +137 -0
- package/campaign-spec/dist/types.d.ts +59 -9
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +59 -53
- package/contracts/effects.v1.json +549 -165
- package/contracts/fixtures/runtime-recipe/accept/current.json +4 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +4 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +4 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +4 -0
- package/contracts/release-ledger.json +1260 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +4 -0
- package/contracts/supported-surface.json +8 -8
- package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
- package/docs/build-packet.md +430 -37
- package/docs/campaign-standardization-report.md +4 -4
- package/docs/campaigns-os-build-flow.md +54 -3
- package/docs/campaignspec-authoring-examples.md +59 -4
- package/docs/design-source-package.md +18 -6
- package/docs/effects.md +17 -9
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +2 -0
- package/docs/progress-snapshots.md +7 -4
- package/docs/qa-and-test-orders.md +147 -1
- package/docs/readback.md +40 -5
- package/docs/runtime-readiness.md +7 -3
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
- package/schemas/campaign-spec.v4.schema.json +34 -3
- package/schemas/campaign-spec.v5.schema.json +34 -3
- package/schemas/campaigns-os-readback.v2.schema.json +62 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +4 -3
- package/skills/next-campaigns-os/SKILL.md +6 -4
- package/skills/next-campaigns-os-setup/SKILL.md +5 -3
- package/skills/next-campaigns-polish/SKILL.md +5 -4
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills/next-campaigns-sdk-bump/SKILL.md +42 -9
- package/skills.json +11 -11
- package/src/brand-theme.mjs +2 -1
- package/src/build-brief.mjs +4 -1
- package/src/campaign-workspace.mjs +288 -15
- package/src/change-scope.mjs +284 -0
- package/src/cli.mjs +308 -86
- package/src/commercial-journey.mjs +16 -3
- package/src/commercial-parity.mjs +15 -7
- package/src/content-residue.mjs +4 -2
- package/src/design-source-package.mjs +36 -0
- package/src/design-source-publication.mjs +9 -6
- package/src/directory-lock.mjs +73 -19
- package/src/doctor/checks.mjs +79 -7
- package/src/doctor/ci.mjs +75 -10
- package/src/doctor/inspect.mjs +23 -6
- package/src/doctor/next-step.mjs +7 -0
- package/src/doctor-sidecar.mjs +3 -2
- package/src/evidence-debt.mjs +47 -7
- package/src/finding-cause.mjs +12 -11
- package/src/input-refresh.mjs +339 -32
- package/src/invocation.mjs +7 -1
- package/src/live-campaign-refs.mjs +7 -2
- package/src/local-preview-policy.mjs +35 -2
- package/src/page-kit-analytics-vendors.mjs +132 -0
- package/src/polish-browser.mjs +3 -1
- package/src/polish-readability.mjs +221 -9
- package/src/polish-visual.mjs +9 -2
- package/src/progress-node.mjs +3 -2
- package/src/qa-analytics-parity.mjs +17 -1
- package/src/qa-browser.mjs +40 -4
- package/src/qa-commercial-parity.mjs +50 -3
- package/src/qa-node.mjs +196 -16
- package/src/qa-publish.mjs +9 -8
- package/src/qa-sidecar.mjs +16 -4
- package/src/qa-vendor-beacons.mjs +232 -0
- package/src/qa-vendor-endpoints.mjs +306 -0
- package/src/qc-accept.mjs +29 -1
- package/src/qc-check-registry.mjs +5 -1
- package/src/qc-results.mjs +73 -26
- package/src/readback.mjs +58 -10
- package/src/route-identity.mjs +10 -0
- package/src/run-record.mjs +18 -10
- package/src/runtime-state-ignore.mjs +6 -2
- package/src/sdk-storage-compatibility.mjs +21 -2
- package/src/source-html-intake.mjs +15 -1
- package/src/spec-derive-campaign.mjs +216 -0
- package/src/spec-derive.mjs +473 -53
- package/src/spec-identity.mjs +14 -0
- package/src/stage-ledger.mjs +40 -1
- package/src/stage-record.mjs +58 -13
- package/src/target-lock.mjs +11 -1
package/agents/codex/AGENTS.md
CHANGED
|
@@ -7,6 +7,7 @@ A NEXT campaign is one connected commerce journey: the pages the CampaignSpec de
|
|
|
7
7
|
- Read `campaign-runtime.build.json` first.
|
|
8
8
|
- To pick up an existing campaign, run `npx --no-install campaigns-os readback .` and `npx --no-install campaigns-os next --packet campaign-runtime.build.json` before reading artifacts by hand; `next` blocks polish and QA again whenever the built output changes. The committed `.campaign-runtime/qa-verdict.json` keeps no order records or URLs: its `browser-test-order:<path>` assertions are the typed-card proof, and the full verdict is under `qa-output/`.
|
|
9
9
|
- If `.campaign-runtime/build-context.json` or `.campaign-runtime/assembly-report.json` exists, read them before editing campaign files.
|
|
10
|
+
- A project that holds several funnels has one Build Packet per funnel: `campaign-runtime.build.json` for the first funnel set up, whose records are directly under `.campaign-runtime/`, and `campaign-runtime-<route>.build.json` for each other funnel, whose records are under `.campaign-runtime/funnels/<route>/` (its `assembly.runtime_dir`). Pass that funnel's packet with `--packet` to every command, `readback . --packet` included; without one, readback refuses and names the packets.
|
|
10
11
|
- Run `npx --no-install campaigns-os doctor --packet campaign-runtime.build.json` before build work.
|
|
11
12
|
- Treat CampaignSpec validation as owned by the public `@nextcommerce/campaigns-os/campaign-spec` rules surfaced through doctor `spec.validation` findings; use structured rule/path detail when available.
|
|
12
13
|
- Respect the selected template family's `agentContract`.
|
|
@@ -37,10 +38,13 @@ A NEXT campaign is one connected commerce journey: the pages the CampaignSpec de
|
|
|
37
38
|
Readback's `evidence_debt` compares the build on disk with the builds Polish and QA were recorded for.
|
|
38
39
|
Changed pages are listed against the recorded assembly build manifest; an owed stage with an empty page list still means evidence is owed.
|
|
39
40
|
2. For pages built from a provided design, the Design Source Package is the authority, not the punch list's wording.
|
|
40
|
-
3. Before opening the pull request, after the final edit, rebuild, then run `npx --no-install campaigns-os record build --packet campaign-runtime.build.json
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
3. Before opening the pull request, after the final edit, rebuild, then run `npx --no-install campaigns-os record build --packet campaign-runtime.build.json`.
|
|
42
|
+
When `next` or the pull-request check says the change is page-only, run
|
|
43
|
+
`npx --no-install campaigns-os qa run --packet campaign-runtime.build.json --page <page_id>` against that build,
|
|
44
|
+
with one `--page` for each changed page it names; Polish is optional for it.
|
|
45
|
+
Otherwise run `npx --no-install campaigns-os polish capture --packet campaign-runtime.build.json --base-url <served-current-build-url>` for the whole route
|
|
46
|
+
and `npx --no-install campaigns-os record polish --packet campaign-runtime.build.json --evidence <polish-evidence.json>`, in that order,
|
|
47
|
+
and when QA is owed, run `npx --no-install campaigns-os qa run --packet campaign-runtime.build.json` against that build.
|
|
44
48
|
4. In the pull request body, put the design reference and the preview of each changed page side by side.
|
|
45
49
|
|
|
46
50
|
The standing pull-request check is `npx --no-install campaigns-os doctor --ci --packet campaign-runtime.build.json --no-live-refs`, report-only unless the repo opts in.
|
|
@@ -5,6 +5,7 @@ When this repository contains Campaigns OS artifacts, use them as the build hand
|
|
|
5
5
|
- `campaign-runtime.build.json` defines the CampaignSpec, source adapter, target output, template family, deploy target, SDK origin state, and QA proof depth.
|
|
6
6
|
- `.campaign-runtime/build-context.json` records page mappings and setup/build handoff details.
|
|
7
7
|
- `.campaign-runtime/assembly-report.json` records stage evidence and blockers.
|
|
8
|
+
- A project that holds several funnels has one Build Packet per funnel: `campaign-runtime.build.json` for the first funnel set up, whose records are directly under `.campaign-runtime/`, and `campaign-runtime-<route>.build.json` for each other funnel, whose records are under `.campaign-runtime/funnels/<route>/` (its `assembly.runtime_dir`). Pass that funnel's packet with `--packet` to every command, `readback . --packet` included; without one, readback refuses and names the packets.
|
|
8
9
|
- To pick up an existing campaign, run `npx --no-install campaigns-os readback .` and `npx --no-install campaigns-os next --packet campaign-runtime.build.json` before reading artifacts by hand; `next` blocks polish and QA again whenever the built output changes. The committed `.campaign-runtime/qa-verdict.json` keeps no order records or URLs: its `browser-test-order:<path>` assertions are the typed-card proof, and the full verdict is under `qa-output/`.
|
|
9
10
|
- `.campaign-runtime/theme/theme-report.json`, when present, is optional brand-theme evidence. Generated `brand-theme.css` must load after `next-core.css`; missing or low-confidence theme is a warning/skipped reason, not permission to edit SDK-owned runtime surfaces.
|
|
10
11
|
|
|
@@ -23,10 +24,13 @@ Before Polish becomes terminal or hands off to deploy/QA, install the package-ow
|
|
|
23
24
|
Readback's `evidence_debt` compares the build on disk with the builds Polish and QA were recorded for.
|
|
24
25
|
Changed pages are listed against the recorded assembly build manifest; an owed stage with an empty page list still means evidence is owed.
|
|
25
26
|
2. For pages built from a provided design, the Design Source Package is the authority, not the punch list's wording.
|
|
26
|
-
3. Before opening the pull request, after the final edit, rebuild, then run `npx --no-install campaigns-os record build --packet campaign-runtime.build.json
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
3. Before opening the pull request, after the final edit, rebuild, then run `npx --no-install campaigns-os record build --packet campaign-runtime.build.json`.
|
|
28
|
+
When `next` or the pull-request check says the change is page-only, run
|
|
29
|
+
`npx --no-install campaigns-os qa run --packet campaign-runtime.build.json --page <page_id>` against that build,
|
|
30
|
+
with one `--page` for each changed page it names; Polish is optional for it.
|
|
31
|
+
Otherwise run `npx --no-install campaigns-os polish capture --packet campaign-runtime.build.json --base-url <served-current-build-url>` for the whole route
|
|
32
|
+
and `npx --no-install campaigns-os record polish --packet campaign-runtime.build.json --evidence <polish-evidence.json>`, in that order,
|
|
33
|
+
and when QA is owed, run `npx --no-install campaigns-os qa run --packet campaign-runtime.build.json` against that build.
|
|
30
34
|
4. In the pull request body, put the design reference and the preview of each changed page side by side.
|
|
31
35
|
|
|
32
36
|
The standing pull-request check is `npx --no-install campaigns-os doctor --ci --packet campaign-runtime.build.json --no-live-refs`, report-only unless the repo opts in.
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
description: Campaigns OS target campaign guidance
|
|
3
3
|
globs:
|
|
4
4
|
- "campaign-runtime.build.json"
|
|
5
|
+
- "campaign-runtime-*.build.json"
|
|
5
6
|
- ".campaign-runtime/**/*.json"
|
|
6
7
|
alwaysApply: false
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
Read Campaigns OS artifacts before editing campaign pages. To pick up an existing campaign, run `npx --no-install campaigns-os readback .` and `npx --no-install campaigns-os next --packet campaign-runtime.build.json` before reading artifacts by hand; `next` blocks polish and QA again whenever the built output changes. The committed `.campaign-runtime/qa-verdict.json` keeps no order records or URLs: its `browser-test-order:<path>` assertions are the typed-card proof, and the full verdict is under `qa-output/`. Treat CampaignSpec/API values as live commerce truth, starter-template contracts as SDK surface truth, and designed HTML/assets as visual/content intent. Treat CampaignSpec validation as owned by the public `@nextcommerce/campaigns-os/campaign-spec` rules surfaced through doctor `spec.validation` findings; use structured rule/path detail when available. If `context.theme` or `.campaign-runtime/theme/theme-report.json` exists, use it as optional brand-theme evidence; generated `brand-theme.css` must load after `next-core.css`, and missing/low-confidence theme is a warning or skipped reason, not permission to edit SDK-owned runtime surfaces.
|
|
10
11
|
|
|
12
|
+
A project that holds several funnels has one Build Packet per funnel: `campaign-runtime.build.json` for the first funnel set up, whose records are directly under `.campaign-runtime/`, and `campaign-runtime-<route>.build.json` for each other funnel, whose records are under `.campaign-runtime/funnels/<route>/` (its `assembly.runtime_dir`). Pass that funnel's packet with `--packet` to every command, `readback . --packet` included; without one, readback refuses and names the packets.
|
|
13
|
+
|
|
11
14
|
A NEXT campaign is one connected commerce journey: the pages the CampaignSpec declares, in its order, through product choice, checkout, post-purchase offers and receipt. Creative intent — audience, purpose, layout, imagery, approved copy, visual style — belongs to the operator, the Campaign Build Brief and the accepted design source. Commerce behaviour — products, package IDs, prices, offers, shipping, routes, cart, checkout, payment and post-purchase — belongs to CampaignSpec/API values and the SDK. Before changing a campaign, read the normalized brief at `.campaign-runtime/input/campaign-build-brief.normalized.json` and the recorded decisions: `adapter_decisions`, `decisions` and `theme` in `.campaign-runtime/assembly-report.json`, and `assembly.template_decision_notes` in the Build Packet. The setup, build, Polish and QA prompts begin with a campaign intent summary, and every `next` result carries it (`intent_summary` in JSON); treat it as orientation, never as a source of prices or behaviour. When a revision is narrow, keep the recorded intent and ask only about missing choices that affect that revision. Ask the operator before saving brief answers.
|
|
12
15
|
|
|
13
16
|
Do not carry over demo package, shipping, voucher, payment, tracking, footer, or SEO values. Prepared AI/exported HTML must be converted into page-kit-ready source first: keep page-owned body markup, strip document wrappers, add YAML frontmatter, move shared CSS/assets into the campaign structure, and use Liquid helpers only for page-kit links/assets/includes. Preserve prepared landing/presell source HTML when it is a real standalone design. For checkout/upsell/downsell/receipt, use starter-template commerce surfaces as SDK contract references while campaign/source owns visual chrome. Reproduce the source design's own proof and urgency elements as designed (reviews, ratings, stock counters, countdowns, guarantees). They are the merchant's content; Campaigns OS does not review them, so do not remove, soften or flag them. Copy starter template families atomically with dependent pages, `_includes/`, `_layouts/`, `assets/css/`, and `assets/js/`; do not copy only checkout/receipt pages. Emit SDK routing meta tags as campaign-root paths such as `/campaign-slug/upsell/`. Preserve SDK-owned checkout/cart/upsell/receipt surfaces. For `shop-three-step`, shipping is dynamic via `window.next.getShippingMethods()`.
|
|
@@ -21,10 +24,13 @@ Before Polish becomes terminal or hands off to deploy/QA, install the package-ow
|
|
|
21
24
|
Readback's `evidence_debt` compares the build on disk with the builds Polish and QA were recorded for.
|
|
22
25
|
Changed pages are listed against the recorded assembly build manifest; an owed stage with an empty page list still means evidence is owed.
|
|
23
26
|
2. For pages built from a provided design, the Design Source Package is the authority, not the punch list's wording.
|
|
24
|
-
3. Before opening the pull request, after the final edit, rebuild, then run `npx --no-install campaigns-os record build --packet campaign-runtime.build.json
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
3. Before opening the pull request, after the final edit, rebuild, then run `npx --no-install campaigns-os record build --packet campaign-runtime.build.json`.
|
|
28
|
+
When `next` or the pull-request check says the change is page-only, run
|
|
29
|
+
`npx --no-install campaigns-os qa run --packet campaign-runtime.build.json --page <page_id>` against that build,
|
|
30
|
+
with one `--page` for each changed page it names; Polish is optional for it.
|
|
31
|
+
Otherwise run `npx --no-install campaigns-os polish capture --packet campaign-runtime.build.json --base-url <served-current-build-url>` for the whole route
|
|
32
|
+
and `npx --no-install campaigns-os record polish --packet campaign-runtime.build.json --evidence <polish-evidence.json>`, in that order,
|
|
33
|
+
and when QA is owed, run `npx --no-install campaigns-os qa run --packet campaign-runtime.build.json` against that build.
|
|
28
34
|
4. In the pull request body, put the design reference and the preview of each changed page side by side.
|
|
29
35
|
|
|
30
36
|
The standing pull-request check is `npx --no-install campaigns-os doctor --ci --packet campaign-runtime.build.json --no-live-refs`, report-only unless the repo opts in.
|
package/campaign-spec/README.md
CHANGED
|
@@ -145,6 +145,13 @@ shape — `CheckoutForwardTarget`, `PrePaymentForwardTarget`,
|
|
|
145
145
|
reads page order or page names. `paymentPageFrom`, `checkoutPathFrom` and
|
|
146
146
|
`paymentPagesForFunnel` answer "which Checkout is on this path".
|
|
147
147
|
|
|
148
|
+
`CheckoutPathHasPackages` adds that every path into a Checkout passes a page
|
|
149
|
+
selling a main-cart package (an entry marked neither `is_upsell` nor
|
|
150
|
+
`is_order_bump`; `isMainCartPackage` and `sellsMainCartPackage` in
|
|
151
|
+
`checkout-flow.ts`). The selling page can be a select page, a landing page, a
|
|
152
|
+
checkout step or the Checkout itself. The contract is written up in
|
|
153
|
+
`../docs/template-family-contracts.md`, "Main-cart entry".
|
|
154
|
+
|
|
148
155
|
## See also
|
|
149
156
|
|
|
150
157
|
- `../CONTEXT.md` — domain vocabulary.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template analytics vendors: the eight direct vendor adapters the certified
|
|
3
|
+
* starter templates ship (GA4, AppLovin Axon, Taboola, Triple Whale, TikTok,
|
|
4
|
+
* Northbeam, Snapchat, Pinterest).
|
|
5
|
+
*
|
|
6
|
+
* A vendor is switched on by a flat key in the campaign's page-kit
|
|
7
|
+
* `_data/campaigns.json` entry (`ga4_id`, `triplewhale_name`, …): absent or
|
|
8
|
+
* empty means off. The template then loads the vendor's base snippet, the
|
|
9
|
+
* shared forwarder and the vendor adapter, which maps the SDK's dl_* stream to
|
|
10
|
+
* the vendor. These vendors are separate from the SDK providers in
|
|
11
|
+
* `analytics.providers` (gtm, facebook, rudderstack, custom), which reach the
|
|
12
|
+
* page through config.js.
|
|
13
|
+
*
|
|
14
|
+
* MIRROR of the Map Builder repo's `shared/route-c-vendors.mjs` (ROUTE_C_VENDORS,
|
|
15
|
+
* "Route C" in that repo), which drives the Map Builder `analytics.vendors`
|
|
16
|
+
* emit and the spec compiler's campaigns.json key upsert. The key table itself
|
|
17
|
+
* is the starter templates' `_shared/analytics/README.md`. Keep `key`,
|
|
18
|
+
* `idKey`, `platformKey`, `piiKey` and `numericId` identical to that registry;
|
|
19
|
+
* analytics-vendors.test.ts pins them.
|
|
20
|
+
*/
|
|
21
|
+
export interface AnalyticsVendorDefinition {
|
|
22
|
+
/** Key under `analytics.vendors` in the CampaignSpec. */
|
|
23
|
+
key: string;
|
|
24
|
+
/** Human label. */
|
|
25
|
+
label: string;
|
|
26
|
+
/** campaigns.json key that carries the vendor id and switches the vendor on. */
|
|
27
|
+
idKey: string;
|
|
28
|
+
/** campaigns.json key for the vendor platform (Triple Whale only). */
|
|
29
|
+
platformKey?: string;
|
|
30
|
+
/** campaigns.json key for the vendor's identity / advanced-matching (PII) flag. */
|
|
31
|
+
piiKey?: string;
|
|
32
|
+
/** True when the template injects the id unquoted, so it must be digits only. */
|
|
33
|
+
numericId?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare const ANALYTICS_VENDORS: readonly AnalyticsVendorDefinition[];
|
|
36
|
+
/** The `analytics.vendors` keys, in registry order. */
|
|
37
|
+
export declare const ANALYTICS_VENDOR_KEYS: readonly string[];
|
|
38
|
+
export declare function analyticsVendorByKey(key: string): AnalyticsVendorDefinition | undefined;
|
|
39
|
+
/** Whether a vendor id looks like a documentation placeholder, not a real id. */
|
|
40
|
+
export declare function isPlaceholderVendorId(value: string): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template analytics vendors: the eight direct vendor adapters the certified
|
|
3
|
+
* starter templates ship (GA4, AppLovin Axon, Taboola, Triple Whale, TikTok,
|
|
4
|
+
* Northbeam, Snapchat, Pinterest).
|
|
5
|
+
*
|
|
6
|
+
* A vendor is switched on by a flat key in the campaign's page-kit
|
|
7
|
+
* `_data/campaigns.json` entry (`ga4_id`, `triplewhale_name`, …): absent or
|
|
8
|
+
* empty means off. The template then loads the vendor's base snippet, the
|
|
9
|
+
* shared forwarder and the vendor adapter, which maps the SDK's dl_* stream to
|
|
10
|
+
* the vendor. These vendors are separate from the SDK providers in
|
|
11
|
+
* `analytics.providers` (gtm, facebook, rudderstack, custom), which reach the
|
|
12
|
+
* page through config.js.
|
|
13
|
+
*
|
|
14
|
+
* MIRROR of the Map Builder repo's `shared/route-c-vendors.mjs` (ROUTE_C_VENDORS,
|
|
15
|
+
* "Route C" in that repo), which drives the Map Builder `analytics.vendors`
|
|
16
|
+
* emit and the spec compiler's campaigns.json key upsert. The key table itself
|
|
17
|
+
* is the starter templates' `_shared/analytics/README.md`. Keep `key`,
|
|
18
|
+
* `idKey`, `platformKey`, `piiKey` and `numericId` identical to that registry;
|
|
19
|
+
* analytics-vendors.test.ts pins them.
|
|
20
|
+
*/
|
|
21
|
+
export const ANALYTICS_VENDORS = [
|
|
22
|
+
{ key: 'ga4', label: 'GA4', idKey: 'ga4_id' },
|
|
23
|
+
{ key: 'axon', label: 'AppLovin Axon', idKey: 'axon_event_key' },
|
|
24
|
+
{ key: 'taboola', label: 'Taboola', idKey: 'taboola_account_id', numericId: true },
|
|
25
|
+
{ key: 'triplewhale', label: 'Triple Whale', idKey: 'triplewhale_name', platformKey: 'triplewhale_platform', piiKey: 'triplewhale_contact_enabled' },
|
|
26
|
+
{ key: 'tiktok', label: 'TikTok', idKey: 'tiktok_pixel_id', piiKey: 'tiktok_advanced_matching_enabled' },
|
|
27
|
+
{ key: 'northbeam', label: 'Northbeam', idKey: 'northbeam_client_id', piiKey: 'northbeam_identity_enabled' },
|
|
28
|
+
{ key: 'snap', label: 'Snapchat', idKey: 'snap_pixel_id', piiKey: 'snap_advanced_matching_enabled' },
|
|
29
|
+
{ key: 'pinterest', label: 'Pinterest', idKey: 'pinterest_tag_id', piiKey: 'pinterest_enhanced_match_enabled' },
|
|
30
|
+
];
|
|
31
|
+
/** The `analytics.vendors` keys, in registry order. */
|
|
32
|
+
export const ANALYTICS_VENDOR_KEYS = ANALYTICS_VENDORS.map((vendor) => vendor.key);
|
|
33
|
+
export function analyticsVendorByKey(key) {
|
|
34
|
+
return ANALYTICS_VENDORS.find((vendor) => vendor.key === key);
|
|
35
|
+
}
|
|
36
|
+
// The same pattern the ops registry warns on (route-c-placeholder-id): a
|
|
37
|
+
// placeholder-shaped id goes live on any non-development build, because the
|
|
38
|
+
// template gate is only "non-empty". Bare XXXX also catches a half-edited
|
|
39
|
+
// placeholder such as "G-XXXXABC123".
|
|
40
|
+
const PLACEHOLDER_VENDOR_ID = /XXXX|YOUR[_-]|CHANGE[_-]?ME|<[^>]*>|EXAMPLE/i;
|
|
41
|
+
/** Whether a vendor id looks like a documentation placeholder, not a real id. */
|
|
42
|
+
export function isPlaceholderVendorId(value) {
|
|
43
|
+
return PLACEHOLDER_VENDOR_ID.test(value);
|
|
44
|
+
}
|
|
@@ -41,6 +41,24 @@ export declare function isPaymentPage(page: Page | null | undefined): boolean;
|
|
|
41
41
|
export declare function isCheckoutStepPage(page: Page | null | undefined): boolean;
|
|
42
42
|
export declare function isPrePaymentPage(page: Page | null | undefined): boolean;
|
|
43
43
|
export declare function isCheckoutFlowPage(page: Page | null | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* A main-cart package entry: one with neither `is_upsell: true` nor
|
|
46
|
+
* `is_order_bump: true` (campaigns-os#206, ratified 2026-09-17). Either flag
|
|
47
|
+
* marks a bump on a checkout page and an offer on an upsell; real exports mark
|
|
48
|
+
* checkout bumps with `is_upsell`, so the two are read as one signal.
|
|
49
|
+
* `default_selected` says which entry a selector starts on and never changes
|
|
50
|
+
* the role. Contract: docs/template-family-contracts.md, "Main-cart entry".
|
|
51
|
+
*/
|
|
52
|
+
export declare function isMainCartPackage(entry: unknown): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* True when `page` puts a main-cart package in front of the shopper: a page
|
|
55
|
+
* that is not a post-payment page and declares at least one main-cart entry.
|
|
56
|
+
* A select page's bundle picker, a landing page's package cards and a
|
|
57
|
+
* Checkout's own selector all count; page type beyond post-payment is not
|
|
58
|
+
* consulted. Upsell, downsell and thank-you entries are post-purchase offers,
|
|
59
|
+
* never the main cart.
|
|
60
|
+
*/
|
|
61
|
+
export declare function sellsMainCartPackage(page: Page | null | undefined): boolean;
|
|
44
62
|
/**
|
|
45
63
|
* The page a route target names within `pages`, or null when it names none
|
|
46
64
|
* (an absolute URL, a fragment, a route outside this funnel, or a typo that
|
|
@@ -53,6 +53,34 @@ export function isPrePaymentPage(page) {
|
|
|
53
53
|
export function isCheckoutFlowPage(page) {
|
|
54
54
|
return CHECKOUT_FLOW_PAGE_TYPES.includes(typeOf(page));
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A main-cart package entry: one with neither `is_upsell: true` nor
|
|
58
|
+
* `is_order_bump: true` (campaigns-os#206, ratified 2026-09-17). Either flag
|
|
59
|
+
* marks a bump on a checkout page and an offer on an upsell; real exports mark
|
|
60
|
+
* checkout bumps with `is_upsell`, so the two are read as one signal.
|
|
61
|
+
* `default_selected` says which entry a selector starts on and never changes
|
|
62
|
+
* the role. Contract: docs/template-family-contracts.md, "Main-cart entry".
|
|
63
|
+
*/
|
|
64
|
+
export function isMainCartPackage(entry) {
|
|
65
|
+
if (!entry || typeof entry !== 'object')
|
|
66
|
+
return false;
|
|
67
|
+
const flags = entry;
|
|
68
|
+
return flags.is_upsell !== true && flags.is_order_bump !== true;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True when `page` puts a main-cart package in front of the shopper: a page
|
|
72
|
+
* that is not a post-payment page and declares at least one main-cart entry.
|
|
73
|
+
* A select page's bundle picker, a landing page's package cards and a
|
|
74
|
+
* Checkout's own selector all count; page type beyond post-payment is not
|
|
75
|
+
* consulted. Upsell, downsell and thank-you entries are post-purchase offers,
|
|
76
|
+
* never the main cart.
|
|
77
|
+
*/
|
|
78
|
+
export function sellsMainCartPackage(page) {
|
|
79
|
+
if (!page || POST_PAYMENT_PAGE_TYPES.includes(typeOf(page)))
|
|
80
|
+
return false;
|
|
81
|
+
const packages = page.packages;
|
|
82
|
+
return Array.isArray(packages) && packages.some(isMainCartPackage);
|
|
83
|
+
}
|
|
56
84
|
/**
|
|
57
85
|
* Strip surrounding slashes and a `.html` suffix so a route target, a page ID
|
|
58
86
|
* and a `page_url` compare on equal terms. The corpus writes targets in all
|
|
@@ -11,17 +11,19 @@
|
|
|
11
11
|
* Corpus, normalize) and ./README.md for usage patterns.
|
|
12
12
|
*/
|
|
13
13
|
import type { CampaignSpec, RuleSet, Violation } from './types.ts';
|
|
14
|
-
export type { CampaignSpec, Rule, RuleSet, Violation, Tag, Severity, Fixture, Page, PageType, Funnel, Offer, Campaign, DesignSource, DesignSourceBreakpoints, TemplateFamilyHint, UpsellTemplatePattern, UpsellMvTiers, VariantLabels, PromoCode, AnalyticsContract, AnalyticsMode, AnalyticsProvider, OutOfBandPixel, ManualEvent, ContentParam, TrackingParams, AnalyticsParams, UtmTransfer, } from './types.ts';
|
|
14
|
+
export type { CampaignSpec, Rule, RuleSet, Violation, Tag, Severity, Fixture, Page, PageType, Funnel, Offer, Campaign, DesignSource, DesignSourceBreakpoints, TemplateFamilyHint, UpsellTemplatePattern, UpsellMvTiers, VariantLabels, PromoCode, AnalyticsContract, AnalyticsMode, AnalyticsProvider, AnalyticsVendor, OutOfBandPixel, ManualEvent, ContentParam, TrackingParams, AnalyticsParams, UtmTransfer, } from './types.ts';
|
|
15
15
|
export { normalize, NormalizeError } from './normalize.ts';
|
|
16
16
|
export { FORWARD_ROUTE_FIELDS, ACCEPT_ROUTE_FIELD, DECLINE_ROUTE_FIELD, ROUTE_FIELDS, PAYMENT_BEARING_PAGE_TYPES, OFFER_BEARING_PAGE_TYPES, forwardRouteTarget, acceptRouteTarget, declineRouteTarget, hasForwardRoute, applicableForwardFields, inapplicableForwardFields, describeForwardField, outgoingEdgeIds, } from './routing.ts';
|
|
17
17
|
export type { ForwardFieldApplicability } from './routing.ts';
|
|
18
|
-
export { CURRENT_SCHEMA_VERSION, PAYMENT_PAGE_TYPE, CHECKOUT_STEP_PAGE_TYPE, PRE_PAYMENT_PAGE_TYPES, CHECKOUT_FLOW_PAGE_TYPES, POST_PAYMENT_PAGE_TYPES, POST_PURCHASE_OFFER_PAGE_TYPES, isPaymentPage, isCheckoutStepPage, isPrePaymentPage, isCheckoutFlowPage, resolveRouteTargetPage, forwardTargetPage, checkoutPathFrom, paymentPageFrom, entryPages, paymentPageForFunnel, paymentPagesForFunnel, upgradeCampaignSpec, } from './checkout-flow.ts';
|
|
18
|
+
export { CURRENT_SCHEMA_VERSION, PAYMENT_PAGE_TYPE, CHECKOUT_STEP_PAGE_TYPE, PRE_PAYMENT_PAGE_TYPES, CHECKOUT_FLOW_PAGE_TYPES, POST_PAYMENT_PAGE_TYPES, POST_PURCHASE_OFFER_PAGE_TYPES, isPaymentPage, isCheckoutStepPage, isPrePaymentPage, isCheckoutFlowPage, isMainCartPackage, sellsMainCartPackage, resolveRouteTargetPage, forwardTargetPage, checkoutPathFrom, paymentPageFrom, entryPages, paymentPageForFunnel, paymentPagesForFunnel, upgradeCampaignSpec, } from './checkout-flow.ts';
|
|
19
19
|
export { allRules, fastRules, specOnlyRules } from './rules/index.ts';
|
|
20
20
|
export { SUPPORTED_SCHEMA_VERSIONS } from './rules/schema-version.ts';
|
|
21
21
|
export { RELEASED_SDK_VERSION_PATTERN, parseSdkVersion, isReleasedSdkVersion, describeSdkVersionRejection, } from './sdk-version-parse.ts';
|
|
22
22
|
export type { SdkVersionParseResult, SdkVersionRejectionReason, } from './sdk-version-parse.ts';
|
|
23
23
|
export { CAMPAIGN_CART_ANALYTICS_IDENTITY_MIN_SDK_VERSION, CAMPAIGN_CART_ANALYTICS_VOCABULARY_SDK_VERSION, DL_EVENTS, DL_EVENT_NAMES, DL_EVENT_NAME_SET, isKnownDlEvent, } from './analytics-vocabulary.ts';
|
|
24
24
|
export type { DlEventCategory, DlEventDefinition, } from './analytics-vocabulary.ts';
|
|
25
|
+
export { ANALYTICS_VENDORS, ANALYTICS_VENDOR_KEYS, analyticsVendorByKey, isPlaceholderVendorId, } from './analytics-vendors.ts';
|
|
26
|
+
export type { AnalyticsVendorDefinition } from './analytics-vendors.ts';
|
|
25
27
|
/**
|
|
26
28
|
* Run a RuleSet against a normalized spec. Returns the flat list of
|
|
27
29
|
* violations across all rules; callers choose their failure policy (throw on
|
|
@@ -20,7 +20,7 @@ export { FORWARD_ROUTE_FIELDS, ACCEPT_ROUTE_FIELD, DECLINE_ROUTE_FIELD, ROUTE_FI
|
|
|
20
20
|
// Which page takes payment, and the pages leading to it (campaigns-os#641).
|
|
21
21
|
// Every "the Checkout on this path" lookup asks here; upgradeCampaignSpec is
|
|
22
22
|
// the v4 → v5 read-time upgrade normalize() applies.
|
|
23
|
-
export { CURRENT_SCHEMA_VERSION, PAYMENT_PAGE_TYPE, CHECKOUT_STEP_PAGE_TYPE, PRE_PAYMENT_PAGE_TYPES, CHECKOUT_FLOW_PAGE_TYPES, POST_PAYMENT_PAGE_TYPES, POST_PURCHASE_OFFER_PAGE_TYPES, isPaymentPage, isCheckoutStepPage, isPrePaymentPage, isCheckoutFlowPage, resolveRouteTargetPage, forwardTargetPage, checkoutPathFrom, paymentPageFrom, entryPages, paymentPageForFunnel, paymentPagesForFunnel, upgradeCampaignSpec, } from "./checkout-flow.js";
|
|
23
|
+
export { CURRENT_SCHEMA_VERSION, PAYMENT_PAGE_TYPE, CHECKOUT_STEP_PAGE_TYPE, PRE_PAYMENT_PAGE_TYPES, CHECKOUT_FLOW_PAGE_TYPES, POST_PAYMENT_PAGE_TYPES, POST_PURCHASE_OFFER_PAGE_TYPES, isPaymentPage, isCheckoutStepPage, isPrePaymentPage, isCheckoutFlowPage, isMainCartPackage, sellsMainCartPackage, resolveRouteTargetPage, forwardTargetPage, checkoutPathFrom, paymentPageFrom, entryPages, paymentPageForFunnel, paymentPagesForFunnel, upgradeCampaignSpec, } from "./checkout-flow.js";
|
|
24
24
|
export { allRules, fastRules, specOnlyRules } from "./rules/index.js";
|
|
25
25
|
// Supported schema_version matrix — single source for the SchemaVersion rule
|
|
26
26
|
// and any consumer that needs to present or gate on the supported lineages.
|
|
@@ -35,6 +35,10 @@ export { RELEASED_SDK_VERSION_PATTERN, parseSdkVersion, isReleasedSdkVersion, de
|
|
|
35
35
|
// The AnalyticsContractShape rule validates blockedEvents against it; the Map
|
|
36
36
|
// Builder picker (via the campaign-spec.js shim) autocompletes from it.
|
|
37
37
|
export { CAMPAIGN_CART_ANALYTICS_IDENTITY_MIN_SDK_VERSION, CAMPAIGN_CART_ANALYTICS_VOCABULARY_SDK_VERSION, DL_EVENTS, DL_EVENT_NAMES, DL_EVENT_NAME_SET, isKnownDlEvent, } from "./analytics-vocabulary.js";
|
|
38
|
+
// The starter templates' analytics vendor adapters (analytics.vendors) and the
|
|
39
|
+
// campaigns.json key each one is switched on by, mirrored from the ops
|
|
40
|
+
// registry. spec derive and doctor read the same table.
|
|
41
|
+
export { ANALYTICS_VENDORS, ANALYTICS_VENDOR_KEYS, analyticsVendorByKey, isPlaceholderVendorId, } from "./analytics-vendors.js";
|
|
38
42
|
// ── Phases ─────────────────────────────────────────────────────────────────
|
|
39
43
|
/**
|
|
40
44
|
* Run a RuleSet against a normalized spec. Returns the flat list of
|
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
* "disabled"; missing mode means SDK defaults apply), runtime/global_config
|
|
38
38
|
* sdk_version should be an exact released semver >= the SDK identity
|
|
39
39
|
* baseline so events carry campaign/session ids.
|
|
40
|
+
* 9. `vendors`, when present, is an object keyed by one of the eight
|
|
41
|
+
* starter-template vendor keys (analytics-vendors.ts); an unknown key is
|
|
42
|
+
* carried but compiles to no campaigns.json key, so nothing loads it. Each
|
|
43
|
+
* enabled vendor declares an `id` (the compiler skips a vendor without
|
|
44
|
+
* one), and `allowedEvents` / `blockedEvents` are string arrays. The
|
|
45
|
+
* id-format, placeholder and event-name checks for vendors live with the
|
|
46
|
+
* ops registry the Map Builder export gate composes on top of this rule,
|
|
47
|
+
* so they are not repeated here.
|
|
40
48
|
*/
|
|
41
49
|
import type { Rule } from '../types.ts';
|
|
42
50
|
export declare const AnalyticsContractShape: Rule;
|
|
@@ -37,8 +37,17 @@
|
|
|
37
37
|
* "disabled"; missing mode means SDK defaults apply), runtime/global_config
|
|
38
38
|
* sdk_version should be an exact released semver >= the SDK identity
|
|
39
39
|
* baseline so events carry campaign/session ids.
|
|
40
|
+
* 9. `vendors`, when present, is an object keyed by one of the eight
|
|
41
|
+
* starter-template vendor keys (analytics-vendors.ts); an unknown key is
|
|
42
|
+
* carried but compiles to no campaigns.json key, so nothing loads it. Each
|
|
43
|
+
* enabled vendor declares an `id` (the compiler skips a vendor without
|
|
44
|
+
* one), and `allowedEvents` / `blockedEvents` are string arrays. The
|
|
45
|
+
* id-format, placeholder and event-name checks for vendors live with the
|
|
46
|
+
* ops registry the Map Builder export gate composes on top of this rule,
|
|
47
|
+
* so they are not repeated here.
|
|
40
48
|
*/
|
|
41
49
|
import { CAMPAIGN_CART_ANALYTICS_IDENTITY_MIN_SDK_VERSION, isKnownDlEvent, } from "../analytics-vocabulary.js";
|
|
50
|
+
import { ANALYTICS_VENDOR_KEYS, analyticsVendorByKey } from "../analytics-vendors.js";
|
|
42
51
|
const VALID_MODES = new Set(['auto', 'manual', 'disabled']);
|
|
43
52
|
const PURCHASE_EVENT = /^(?:dl_)?purchase$/i;
|
|
44
53
|
function isNonEmptyString(value) {
|
|
@@ -208,6 +217,36 @@ export const AnalyticsContractShape = {
|
|
|
208
217
|
}
|
|
209
218
|
}
|
|
210
219
|
}
|
|
220
|
+
// 9. vendors
|
|
221
|
+
const vendors = analytics.vendors;
|
|
222
|
+
if (vendors !== undefined) {
|
|
223
|
+
if (!isPlainAnalyticsObject(vendors)) {
|
|
224
|
+
warn(`analytics.vendors must be an object keyed by vendor (${ANALYTICS_VENDOR_KEYS.join(', ')}).`, '/analytics/vendors', 'vendors-not-object');
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
for (const [key, vendor] of Object.entries(vendors)) {
|
|
228
|
+
const base = `/analytics/vendors/${key}`;
|
|
229
|
+
const definition = analyticsVendorByKey(key);
|
|
230
|
+
if (!definition) {
|
|
231
|
+
warn(`analytics.vendors.${key} is not a starter-template vendor (${ANALYTICS_VENDOR_KEYS.join(', ')}). No campaigns.json key is compiled for it, so nothing loads it; check for a typo, or declare a pixel loaded another way under out_of_band_pixels.`, base, 'vendor-unknown', { vendor: key });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!vendor || typeof vendor !== 'object' || Array.isArray(vendor)) {
|
|
235
|
+
warn(`analytics.vendors.${key} must be an object ({ enabled, id, … }).`, base, 'vendor-shape', { vendor: key });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const entry = vendor;
|
|
239
|
+
if (entry.enabled !== false && !isNonEmptyString(entry.id)) {
|
|
240
|
+
warn(`analytics.vendors.${key} is enabled but has no id; the template loads ${definition.label} only when ${definition.idKey} is set, so it stays off.`, `${base}/id`, 'vendor-id-missing', { vendor: key });
|
|
241
|
+
}
|
|
242
|
+
for (const list of ['allowedEvents', 'blockedEvents']) {
|
|
243
|
+
if (entry[list] !== undefined && !isStringArray(entry[list])) {
|
|
244
|
+
warn(`analytics.vendors.${key}.${list} must be an array of event-name strings.`, `${base}/${list}`, 'vendor-events-shape', { vendor: key, list });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
211
250
|
const { pageIds, checkoutPageIds } = collectPageIds(spec);
|
|
212
251
|
// 3. out_of_band_pixels
|
|
213
252
|
if (analytics.out_of_band_pixels !== undefined) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CampaignCurrencies — checks the campaign's default currency and its list of
|
|
3
|
+
* selling currencies (#714).
|
|
4
|
+
*
|
|
5
|
+
* 1. campaign.currency, when set, is a three-letter upper-case ISO 4217
|
|
6
|
+
* code (error). A lower-case or made-up code can make Campaign Cart
|
|
7
|
+
* fall back to a cached USD campaign, so the page shows the wrong prices
|
|
8
|
+
* and nothing else complains.
|
|
9
|
+
* 2. campaign.available_currencies, when set, is an array whose every entry
|
|
10
|
+
* is such a code (error). A `{ code, label }` object, the Campaigns API's
|
|
11
|
+
* shape, is reported with the code it should be written as: Map Builder
|
|
12
|
+
* writes code strings, and so should a hand-authored spec.
|
|
13
|
+
* 3. campaign.currency is one of campaign.available_currencies when that
|
|
14
|
+
* list is non-empty (warning). An empty list states nothing and is
|
|
15
|
+
* silent, since Map Builder exports [] for a campaign with no list.
|
|
16
|
+
*
|
|
17
|
+
* The spec owns `campaign.available_currencies`: every currency the campaign
|
|
18
|
+
* sells in, the default included. The Campaigns App API's
|
|
19
|
+
* `additional_currencies` is the same list without the default; it is not a
|
|
20
|
+
* CampaignSpec field, and this rule does not read it.
|
|
21
|
+
*
|
|
22
|
+
* An absent, null or empty currency is silent here: a missing default is a
|
|
23
|
+
* draft state, not a malformed code.
|
|
24
|
+
*
|
|
25
|
+
* The ISO list is `Intl.supportedValuesOf('currency')`. A runtime without it
|
|
26
|
+
* (very old browsers) gets the shape check only.
|
|
27
|
+
*/
|
|
28
|
+
import type { Rule } from '../types.ts';
|
|
29
|
+
export declare function isCurrencyCode(value: unknown): value is string;
|
|
30
|
+
export declare const CampaignCurrencies: Rule;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CampaignCurrencies — checks the campaign's default currency and its list of
|
|
3
|
+
* selling currencies (#714).
|
|
4
|
+
*
|
|
5
|
+
* 1. campaign.currency, when set, is a three-letter upper-case ISO 4217
|
|
6
|
+
* code (error). A lower-case or made-up code can make Campaign Cart
|
|
7
|
+
* fall back to a cached USD campaign, so the page shows the wrong prices
|
|
8
|
+
* and nothing else complains.
|
|
9
|
+
* 2. campaign.available_currencies, when set, is an array whose every entry
|
|
10
|
+
* is such a code (error). A `{ code, label }` object, the Campaigns API's
|
|
11
|
+
* shape, is reported with the code it should be written as: Map Builder
|
|
12
|
+
* writes code strings, and so should a hand-authored spec.
|
|
13
|
+
* 3. campaign.currency is one of campaign.available_currencies when that
|
|
14
|
+
* list is non-empty (warning). An empty list states nothing and is
|
|
15
|
+
* silent, since Map Builder exports [] for a campaign with no list.
|
|
16
|
+
*
|
|
17
|
+
* The spec owns `campaign.available_currencies`: every currency the campaign
|
|
18
|
+
* sells in, the default included. The Campaigns App API's
|
|
19
|
+
* `additional_currencies` is the same list without the default; it is not a
|
|
20
|
+
* CampaignSpec field, and this rule does not read it.
|
|
21
|
+
*
|
|
22
|
+
* An absent, null or empty currency is silent here: a missing default is a
|
|
23
|
+
* draft state, not a malformed code.
|
|
24
|
+
*
|
|
25
|
+
* The ISO list is `Intl.supportedValuesOf('currency')`. A runtime without it
|
|
26
|
+
* (very old browsers) gets the shape check only.
|
|
27
|
+
*/
|
|
28
|
+
const CODE_SHAPE = /^[A-Z]{3}$/;
|
|
29
|
+
let isoCodes;
|
|
30
|
+
function knownIsoCodes() {
|
|
31
|
+
if (isoCodes === undefined) {
|
|
32
|
+
const supportedValuesOf = Intl.supportedValuesOf;
|
|
33
|
+
isoCodes = typeof supportedValuesOf === 'function' ? new Set(supportedValuesOf('currency')) : null;
|
|
34
|
+
}
|
|
35
|
+
return isoCodes;
|
|
36
|
+
}
|
|
37
|
+
export function isCurrencyCode(value) {
|
|
38
|
+
if (typeof value !== 'string' || !CODE_SHAPE.test(value))
|
|
39
|
+
return false;
|
|
40
|
+
const known = knownIsoCodes();
|
|
41
|
+
return known === null || known.has(value);
|
|
42
|
+
}
|
|
43
|
+
// The code a malformed value most likely meant: the upper-cased string, or an
|
|
44
|
+
// object's `code`, when that is a valid code. Null when nothing obvious fits.
|
|
45
|
+
function suggestedCode(value) {
|
|
46
|
+
const raw = typeof value === 'string'
|
|
47
|
+
? value
|
|
48
|
+
: value && typeof value === 'object' && !Array.isArray(value)
|
|
49
|
+
? value.code
|
|
50
|
+
: undefined;
|
|
51
|
+
if (typeof raw !== 'string')
|
|
52
|
+
return null;
|
|
53
|
+
const candidate = raw.trim().toUpperCase();
|
|
54
|
+
return isCurrencyCode(candidate) ? candidate : null;
|
|
55
|
+
}
|
|
56
|
+
function isAbsent(value) {
|
|
57
|
+
return value === undefined || value === null || (typeof value === 'string' && value.trim() === '');
|
|
58
|
+
}
|
|
59
|
+
function quoted(value) {
|
|
60
|
+
return typeof value === 'string' ? `"${value}"` : JSON.stringify(value) ?? String(value);
|
|
61
|
+
}
|
|
62
|
+
function codeMessage(field, value) {
|
|
63
|
+
const suggestion = suggestedCode(value);
|
|
64
|
+
const isObject = value !== null && typeof value === 'object';
|
|
65
|
+
const base = isObject
|
|
66
|
+
? `${field} is an object; CampaignSpec lists currencies as three-letter ISO 4217 code strings.`
|
|
67
|
+
: `${field} ${quoted(value)} is not a three-letter upper-case ISO 4217 currency code.`;
|
|
68
|
+
return suggestion ? `${base} Write it as "${suggestion}".` : base;
|
|
69
|
+
}
|
|
70
|
+
export const CampaignCurrencies = {
|
|
71
|
+
id: 'CampaignCurrencies',
|
|
72
|
+
severity: 'error',
|
|
73
|
+
tags: ['fast', 'spec-only'],
|
|
74
|
+
check(spec) {
|
|
75
|
+
const violations = [];
|
|
76
|
+
const campaign = spec.campaign;
|
|
77
|
+
if (!campaign || typeof campaign !== 'object')
|
|
78
|
+
return violations;
|
|
79
|
+
const currency = campaign.currency;
|
|
80
|
+
const currencyValid = !isAbsent(currency) && isCurrencyCode(currency);
|
|
81
|
+
if (!isAbsent(currency) && !currencyValid) {
|
|
82
|
+
violations.push({
|
|
83
|
+
ruleId: 'CampaignCurrencies',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
message: codeMessage('campaign.currency', currency),
|
|
86
|
+
path: '/campaign/currency',
|
|
87
|
+
data: { check: 'currency_code', received: currency, suggestion: suggestedCode(currency) },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const available = campaign.available_currencies;
|
|
91
|
+
if (available === undefined || available === null)
|
|
92
|
+
return violations;
|
|
93
|
+
if (!Array.isArray(available)) {
|
|
94
|
+
violations.push({
|
|
95
|
+
ruleId: 'CampaignCurrencies',
|
|
96
|
+
severity: 'error',
|
|
97
|
+
message: 'campaign.available_currencies must be an array of three-letter ISO 4217 currency codes, the default currency included.',
|
|
98
|
+
path: '/campaign/available_currencies',
|
|
99
|
+
data: { check: 'available_currencies_shape', received: available },
|
|
100
|
+
});
|
|
101
|
+
return violations;
|
|
102
|
+
}
|
|
103
|
+
available.forEach((entry, index) => {
|
|
104
|
+
if (isCurrencyCode(entry))
|
|
105
|
+
return;
|
|
106
|
+
violations.push({
|
|
107
|
+
ruleId: 'CampaignCurrencies',
|
|
108
|
+
severity: 'error',
|
|
109
|
+
message: codeMessage(`campaign.available_currencies[${index}]`, entry),
|
|
110
|
+
path: `/campaign/available_currencies/${index}`,
|
|
111
|
+
data: { check: 'available_currency_code', received: entry, suggestion: suggestedCode(entry) },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// Membership compares what each entry most likely means, so a list entry
|
|
115
|
+
// already reported as "usd" does not also make "USD" read as missing.
|
|
116
|
+
if (currencyValid && available.length > 0) {
|
|
117
|
+
const listed = new Set(available.map((entry) => (isCurrencyCode(entry) ? entry : suggestedCode(entry))));
|
|
118
|
+
if (!listed.has(currency)) {
|
|
119
|
+
violations.push({
|
|
120
|
+
ruleId: 'CampaignCurrencies',
|
|
121
|
+
severity: 'warning',
|
|
122
|
+
message: `campaign.currency "${currency}" is not in campaign.available_currencies. List every currency the campaign sells in, the default included, or change the default.`,
|
|
123
|
+
path: '/campaign/available_currencies',
|
|
124
|
+
data: { check: 'currency_not_available', currency, available },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return violations;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CheckoutPathHasPackages — every path from an entry page to a Checkout passes
|
|
3
|
+
* at least one page that sells a main-cart package (campaigns-os#711, #206).
|
|
4
|
+
*
|
|
5
|
+
* A Checkout the shopper reaches with nothing in the cart cannot take an
|
|
6
|
+
* order, and until this rule nothing said so before a test order failed. The
|
|
7
|
+
* selling page may be any page on the path up to and including the Checkout:
|
|
8
|
+
* a select page's bundle picker, a landing page's package cards, a checkout
|
|
9
|
+
* step, or the Checkout's own selector. So the rule counts per path, never per
|
|
10
|
+
* page: a Checkout with no packages of its own passes when every path into it
|
|
11
|
+
* sells first.
|
|
12
|
+
*
|
|
13
|
+
* "Main-cart package" is the ratified #206 contract (checkout-flow.ts
|
|
14
|
+
* `isMainCartPackage`, docs/template-family-contracts.md "Main-cart entry"):
|
|
15
|
+
* an entry with neither `is_upsell: true` nor `is_order_bump: true`. Bump rows
|
|
16
|
+
* alone sell nothing, `default_selected` never changes the role, and entries
|
|
17
|
+
* on upsell, downsell or thank-you pages never count.
|
|
18
|
+
*
|
|
19
|
+
* Paths are walked the way OneCheckoutPerPath walks them: both edges a page
|
|
20
|
+
* can traverse (routing.ts), starting at the funnel's entry pages
|
|
21
|
+
* (checkout-flow.ts `entryPages`). No page order or name is consulted. The
|
|
22
|
+
* search runs, per entry page, over (page, sold yet) states and stops at the
|
|
23
|
+
* first Checkout, so each empty Checkout is reported once per entry however
|
|
24
|
+
* many of that entry's paths reach it. A second Checkout on one path is
|
|
25
|
+
* OneCheckoutPerPath's to report.
|
|
26
|
+
*
|
|
27
|
+
* With build_scope.mode === 'partial' the finding is a warning: a partial
|
|
28
|
+
* build may ship a Checkout that a page outside the build fills.
|
|
29
|
+
*/
|
|
30
|
+
import type { Rule } from '../types.ts';
|
|
31
|
+
export declare const CheckoutPathHasPackages: Rule;
|