@nextcommerce/campaigns-os 1.60.0 → 1.62.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 +244 -0
- package/README.md +86 -438
- package/agents/claude/CLAUDE.md +20 -0
- package/agents/codex/AGENTS.md +19 -0
- package/agents/copilot/copilot-instructions.md +19 -0
- package/agents/cursor/campaigns-os.mdc +21 -0
- 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 +29 -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 +2 -0
- package/campaign-spec/dist/rules/index.js +6 -0
- package/campaign-spec/dist/types.d.ts +43 -9
- package/compatibility.json +1 -1
- package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +68 -61
- package/contracts/effects.v1.json +549 -165
- package/contracts/fixtures/runtime-recipe/accept/current.json +3 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +3 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +3 -0
- package/contracts/release-ledger.json +1767 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +3 -0
- package/contracts/supported-surface.json +10 -10
- package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
- package/docs/access-model.md +20 -0
- package/docs/brand-theme-bridge.md +62 -3
- package/docs/build-packet.md +456 -36
- package/docs/campaign-build-brief.md +16 -0
- package/docs/campaign-standardization-report.md +4 -4
- package/docs/campaigns-os-build-flow.md +112 -2
- package/docs/campaignspec-authoring-examples.md +141 -0
- package/docs/design-source-package.md +64 -8
- package/docs/effects.md +17 -9
- package/docs/entry-points.md +188 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +42 -2
- package/docs/progress-snapshots.md +7 -4
- package/docs/qa-and-test-orders.md +197 -3
- package/docs/quickstart.md +438 -0
- package/docs/readback.md +154 -6
- package/docs/runtime-readiness.md +6 -3
- package/docs/sdk-storage-compatibility.md +4 -2
- package/docs/skills-revision.md +10 -10
- package/docs/source-adapters.md +213 -0
- package/package.json +10 -3
- package/schemas/campaign-design-source-package.v0.schema.json +8 -0
- 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 +19 -3
- package/schemas/campaign-spec.v5.schema.json +19 -3
- package/schemas/campaigns-os-readback.v2.schema.json +238 -1
- package/schemas/source-html-manifest.v0.schema.json +3 -0
- 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 +5 -4
- package/skills/next-campaigns-os/SKILL.md +7 -4
- package/skills/next-campaigns-os-setup/SKILL.md +5 -3
- package/skills/next-campaigns-polish/SKILL.md +8 -10
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills/next-campaigns-sdk-bump/SKILL.md +151 -25
- package/skills.json +11 -11
- package/src/brand-theme.mjs +415 -9
- package/src/brief-summary.mjs +6 -2
- package/src/browser-launch.mjs +2 -1
- package/src/build-brief.mjs +13 -3
- package/src/built-site-scope.mjs +2 -2
- package/src/campaign-workspace.mjs +288 -15
- package/src/change-scope.mjs +284 -0
- package/src/cli.mjs +358 -97
- package/src/commercial-journey.mjs +16 -3
- package/src/commercial-parity.mjs +15 -7
- package/src/content-residue.mjs +4 -2
- package/src/design-file-reference.mjs +93 -0
- package/src/design-source-package.mjs +91 -1
- package/src/design-source-publication.mjs +9 -6
- package/src/directory-lock.mjs +73 -19
- package/src/doctor/checks.mjs +87 -81
- package/src/doctor/ci.mjs +177 -0
- package/src/doctor/inspect.mjs +41 -6
- package/src/doctor/next-step.mjs +7 -0
- package/src/doctor/source-provenance.mjs +74 -0
- package/src/doctor-sidecar.mjs +3 -2
- package/src/evidence-debt.mjs +159 -0
- 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/parity/compare.mjs +316 -0
- package/src/parity/design-access.mjs +54 -0
- package/src/parity/element-parity-record.mjs +569 -0
- package/src/parity/probe.mjs +205 -0
- package/src/parity/snapshot.mjs +110 -0
- package/src/png-header.mjs +87 -0
- package/src/polish-browser.mjs +189 -10
- package/src/polish-captures-record.mjs +52 -0
- package/src/polish-deadline.mjs +14 -0
- package/src/polish-gate.mjs +25 -6
- package/src/polish-node.mjs +81 -7
- package/src/polish-readability.mjs +221 -9
- package/src/polish-visual-fixtures.mjs +74 -0
- package/src/polish-visual.mjs +613 -0
- package/src/progress-node.mjs +3 -2
- package/src/qa-analytics-correctness.mjs +14 -2
- package/src/qa-analytics-parity.mjs +111 -6
- package/src/qa-browser.mjs +40 -4
- package/src/qa-commercial-parity.mjs +50 -3
- package/src/qa-node.mjs +235 -22
- 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 +176 -6
- package/src/qc-check-registry.mjs +42 -2
- package/src/qc-results.mjs +477 -42
- package/src/readback.mjs +113 -21
- 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-attribute-index.mjs +11 -11
- package/src/sdk-markup.mjs +77 -2
- package/src/sdk-storage-compatibility.mjs +115 -6
- package/src/source-html-intake.mjs +15 -1
- package/src/source-html-manifest.mjs +7 -0
- package/src/source-html-server.mjs +129 -0
- 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 +100 -17
- package/src/target-lock.mjs +11 -1
- package/src/theme-gate.mjs +2 -2
- package/src/visual-diff.mjs +296 -0
- package/src/visual-reference.mjs +126 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Entry Points
|
|
2
|
+
|
|
3
|
+
A CampaignSpec is the destination contract. Source HTML is the input
|
|
4
|
+
that gets composed against it. **Where source HTML comes from is the
|
|
5
|
+
entry point.** Different campaigns enter the build pipeline from
|
|
6
|
+
different shapes:
|
|
7
|
+
|
|
8
|
+
| Entry point | `design_source` on spec | Source-HTML manifest | What it looks like |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| Template-stock | unset | absent | Spec + filesystem of starter-template pages. No design overlay. |
|
|
11
|
+
| Figma-driven | `type: "figma"` | emitted by `figma-sections-export` | Spec + source HTML produced by the Figma exporter. |
|
|
12
|
+
| AI-generated | `type: "ai-generated"` | emitted by the producing agent | Spec + source HTML written by an LLM/agent. |
|
|
13
|
+
| Hand-authored | unset | absent | Spec + source HTML hand-written by a designer/dev. Same shape as template-stock from the consumer's view. |
|
|
14
|
+
| Mixed | per-page | one manifest enumerating every page | Spec + source HTML composed from multiple producers (figma export for landing, template stock for checkout, etc.). |
|
|
15
|
+
|
|
16
|
+
The build pipeline (`prepare-build` → doctor → setup → build → polish
|
|
17
|
+
→ deploy → QA) is **identical across all five entry points**. The
|
|
18
|
+
only thing that varies is how the source HTML arrives and how
|
|
19
|
+
`design_source` / the manifest get populated. Doctor reads both, the
|
|
20
|
+
rest of the pipeline reads the packet doctor produced.
|
|
21
|
+
|
|
22
|
+
This doc explains how each entry point is recognized, what
|
|
23
|
+
populates each input, and what the operator does for that path. The
|
|
24
|
+
[Source HTML Manifest Auto-Population](./build-packet.md#source-html-manifest-auto-population)
|
|
25
|
+
section in `build-packet.md` covers the manifest consumer
|
|
26
|
+
mechanics; this doc names which entry point produces which input.
|
|
27
|
+
|
|
28
|
+
## Template-stock
|
|
29
|
+
|
|
30
|
+
The campaign is being scaffolded against a starter template
|
|
31
|
+
(e.g. `olympus-mv-single-step`) with **no design overlay yet**. Spec
|
|
32
|
+
values fill the template's placeholders. Source HTML is whatever the
|
|
33
|
+
template family ships, possibly with light brand tweaks.
|
|
34
|
+
|
|
35
|
+
- **CampaignSpec:** every page is present but `design_source` is
|
|
36
|
+
unset on every page.
|
|
37
|
+
- **Source HTML:** the starter template's `src/` folder copied into
|
|
38
|
+
the target repo. Filenames match standard page-type conventions
|
|
39
|
+
(`landing.html`, `checkout.html`, `upsell.html`, `receipt.html`).
|
|
40
|
+
- **Manifest:** absent.
|
|
41
|
+
- **`prepare-build` behavior:** falls back to filesystem matching
|
|
42
|
+
(slug-based filename → CampaignSpec page mapping).
|
|
43
|
+
- **Doctor behavior:** any page with no source mapping fires the
|
|
44
|
+
generic `source_html.pages.coverage` error (no Figma-specific hint
|
|
45
|
+
because `design_source` is unset).
|
|
46
|
+
- **When to use:** initial scaffold, A/B variant testing on top of
|
|
47
|
+
an existing template, no-design-yet builds where the team will add
|
|
48
|
+
design later.
|
|
49
|
+
- **Realistic example:** a single-product scaffold pass — clone
|
|
50
|
+
`olympus-mv-single-step`, fill spec, ship. Design overlay comes in
|
|
51
|
+
a separate later pass.
|
|
52
|
+
|
|
53
|
+
## Figma-driven
|
|
54
|
+
|
|
55
|
+
Design lives in Figma. `figma-sections-export` produces source HTML
|
|
56
|
+
plus a manifest at handoff.
|
|
57
|
+
|
|
58
|
+
- **CampaignSpec:** the pages that need design carry
|
|
59
|
+
`design_source.type === "figma"` with `file_url` plus
|
|
60
|
+
`breakpoints.{desktop, tablet, mobile}` per page.
|
|
61
|
+
- **Source HTML:** `figma-sections-export` writes
|
|
62
|
+
`<campaign>/landing.html`, `<campaign>/_includes/landing/*.html`,
|
|
63
|
+
and/or `<campaign>/presell.html` into the source root.
|
|
64
|
+
- **Manifest:** `<source>/.campaigns-os/source-html-manifest.json`
|
|
65
|
+
(or the file named by `--design-manifest <path>`) with
|
|
66
|
+
`generator: "figma-sections-export@<version>"`. Lists the
|
|
67
|
+
pages produced.
|
|
68
|
+
- **`prepare-build` behavior:** reads the manifest, populates
|
|
69
|
+
`packet.source_html.pages[]` directly from it (bypasses filesystem
|
|
70
|
+
matching).
|
|
71
|
+
- **Doctor behavior:** any spec page with `design_source` but no
|
|
72
|
+
source mapping fires a design_source-aware error naming the
|
|
73
|
+
Figma file and recommending
|
|
74
|
+
`npm run handoff -- <slug>` in figma-sections-export.
|
|
75
|
+
- **When to use:** designer authors in Figma, ships the export at
|
|
76
|
+
handoff, build picks up automatically.
|
|
77
|
+
|
|
78
|
+
## AI-generated
|
|
79
|
+
|
|
80
|
+
An LLM or agent (Claude, Codex, etc.) writes the source HTML
|
|
81
|
+
directly. The contract is the same as figma-driven from the
|
|
82
|
+
consumer's view: a manifest at the agreed location with the agreed
|
|
83
|
+
schema.
|
|
84
|
+
|
|
85
|
+
- **CampaignSpec:** pages that were AI-produced carry
|
|
86
|
+
`design_source.type === "ai-generated"` so doctor can produce a
|
|
87
|
+
matching error message ("AI run hasn't completed" vs "designer
|
|
88
|
+
hasn't exported").
|
|
89
|
+
- **Source HTML:** written by the AI agent into the source root,
|
|
90
|
+
with the same page-kit-ready conversion conventions documented in
|
|
91
|
+
[`source-adapters.md`](./source-adapters.md).
|
|
92
|
+
- **Manifest:** same shape as the figma-driven case
|
|
93
|
+
(`source-html-manifest/v0`), with `generator: "<agent>@<version>"`.
|
|
94
|
+
- **`prepare-build` behavior:** identical to figma-driven —
|
|
95
|
+
consumer doesn't care about provenance.
|
|
96
|
+
- **Doctor behavior:** when an AI-marked page has no source
|
|
97
|
+
mapping, the design_source-aware error variant for `ai-generated`
|
|
98
|
+
fires (recommends re-running the AI agent rather than re-exporting
|
|
99
|
+
from Figma).
|
|
100
|
+
- **When to use:** AI-generated landing/presell while
|
|
101
|
+
checkout/upsell stay on template stock. Pre-existing static HTML
|
|
102
|
+
pipelines that emit the manifest at handoff.
|
|
103
|
+
|
|
104
|
+
## Hand-authored
|
|
105
|
+
|
|
106
|
+
Designer or developer hand-writes the HTML directly. From the
|
|
107
|
+
consumer's view this is shaped identically to template-stock: spec
|
|
108
|
+
without `design_source`, no manifest, files on disk.
|
|
109
|
+
|
|
110
|
+
- **CampaignSpec:** `design_source` unset on the hand-authored
|
|
111
|
+
pages.
|
|
112
|
+
- **Source HTML:** the designer/dev places `.html` files in the
|
|
113
|
+
source root (any name; filesystem matching uses page-type slugs).
|
|
114
|
+
- **Manifest:** absent.
|
|
115
|
+
- **`prepare-build` behavior:** filesystem matching, same as
|
|
116
|
+
template-stock.
|
|
117
|
+
- **Doctor behavior:** generic coverage error for missing pages.
|
|
118
|
+
- **When to use:** one-off campaigns, presell article pages with
|
|
119
|
+
bespoke layouts, demos.
|
|
120
|
+
|
|
121
|
+
## Mixed
|
|
122
|
+
|
|
123
|
+
A single campaign with **per-page** entry points. Landing from
|
|
124
|
+
Figma export, checkout from template stock, presell from an AI run.
|
|
125
|
+
Per-page granularity is already in the schema (`design_source` is
|
|
126
|
+
per-page; `source_html.pages[]` is per-page).
|
|
127
|
+
|
|
128
|
+
- **CampaignSpec:** each page carries its own (or no)
|
|
129
|
+
`design_source`. There's no campaign-level "entry point" field —
|
|
130
|
+
per-page is the unit.
|
|
131
|
+
- **Source HTML:** one source root, with files placed by whichever
|
|
132
|
+
producer owns that page. Recommend per-page subdirectories
|
|
133
|
+
(`landing/`, `checkout/`, `presell/`) when producers don't
|
|
134
|
+
coordinate filenames.
|
|
135
|
+
- **Manifest:** one combined manifest at the source root listing
|
|
136
|
+
every page, with each entry's `path` pointing at its producer's
|
|
137
|
+
output location. Different producers write to different
|
|
138
|
+
subdirectories; the manifest unifies them. Manifest paths are
|
|
139
|
+
relative to the source root (`checkout/index.html`), and the
|
|
140
|
+
reference producer only auto-derives nested `index.html` files from
|
|
141
|
+
their parent directory. Use explicit `--page page_id=path` mappings
|
|
142
|
+
when multiple files would infer the same page id.
|
|
143
|
+
- **`prepare-build` behavior:** when a manifest is present it
|
|
144
|
+
governs every mapping — including pages whose path points into a
|
|
145
|
+
template-stock subdirectory. Operators with mixed sources should
|
|
146
|
+
always write a manifest rather than relying on filesystem
|
|
147
|
+
fallback (which is all-or-nothing). The manifest path remains source
|
|
148
|
+
provenance; `source_html.pages[].page_kit` carries the target Page Kit
|
|
149
|
+
file, route, CPK `page_type`, and frontmatter projection.
|
|
150
|
+
- **Doctor behavior:** per-page error variants fire based on each
|
|
151
|
+
page's `design_source.type`. A figma-marked landing page with no
|
|
152
|
+
manifest entry gets the Figma error; a template-stock checkout
|
|
153
|
+
page (no `design_source`) gets the generic error.
|
|
154
|
+
- **When to use:** the realistic case for most live campaigns once
|
|
155
|
+
the design-Figma + template-stock-commerce-surfaces split is
|
|
156
|
+
routine.
|
|
157
|
+
|
|
158
|
+
## Existing campaigns
|
|
159
|
+
|
|
160
|
+
A campaign repository that already exists — built or not, with or
|
|
161
|
+
without Campaigns OS artifacts — enters through
|
|
162
|
+
`campaigns-os standardize --target <repo>`, not through the build
|
|
163
|
+
pipeline. It is a read-only audit: it classifies each campaign root,
|
|
164
|
+
inventories source and runtime contracts, judges SDK versions and
|
|
165
|
+
checkout bindings against the bundled contracts, and, when a built
|
|
166
|
+
`_site` and a known template family exist, runs the built-output
|
|
167
|
+
doctor. The flags, exit codes and the slug-resolution rules are in
|
|
168
|
+
[campaign-standardization-report.md](./campaign-standardization-report.md).
|
|
169
|
+
Its findings name the entry point above the campaign should re-enter
|
|
170
|
+
through when repair is needed.
|
|
171
|
+
|
|
172
|
+
## Out of the consumer's hands
|
|
173
|
+
|
|
174
|
+
The build pipeline doesn't care which agent wrote the source HTML;
|
|
175
|
+
it only cares that the contract (filesystem location, manifest
|
|
176
|
+
shape, `design_source` shape) is honored. New entry points (Penpot,
|
|
177
|
+
Sketch, plain Markdown converters, etc.) slot in by:
|
|
178
|
+
|
|
179
|
+
1. Adding a `design_source.type` value (open-string, accepted today).
|
|
180
|
+
2. Emitting source HTML at the source root.
|
|
181
|
+
3. Emitting a `source-html-manifest/v0` manifest if the entry point
|
|
182
|
+
wants doctor's manifest-aware code path; otherwise relying on
|
|
183
|
+
filesystem fallback like template-stock and hand-authored.
|
|
184
|
+
|
|
185
|
+
No campaigns-os code change is required to add a new entry point if
|
|
186
|
+
the producer honors the existing contracts. Doctor's error wording
|
|
187
|
+
gets an additional `design_source.type` variant when a new entry
|
|
188
|
+
point ships, but that's a polish step, not a gate.
|
package/docs/local-setup.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
For a new campaign, create an empty working folder and run this from it:
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm init -y && npm install --save-exact next-campaign-page-kit@0.3.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.
|
|
6
|
+
npm init -y && npm install --save-exact next-campaign-page-kit@0.3.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.62.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
`npm init -y` gives the folder its own `package.json`. Without one, npm
|
|
@@ -26,7 +26,7 @@ Ledger schema id: `campaigns-os-release-ledger/v1`
|
|
|
26
26
|
Change policy version: `1.0.0`
|
|
27
27
|
Reason-code vocabulary version: `1.0.0`
|
|
28
28
|
Limits version: `1.0.0`
|
|
29
|
-
Supported surface at generation time: `1.
|
|
29
|
+
Supported surface at generation time: `1.62.0`
|
|
30
30
|
|
|
31
31
|
## Forward compatibility
|
|
32
32
|
|
package/docs/polish-evidence.md
CHANGED
|
@@ -54,7 +54,7 @@ per-field problem line.
|
|
|
54
54
|
|
|
55
55
|
| Field | Accepted shape (presence check) |
|
|
56
56
|
|---|---|
|
|
57
|
-
| `visual_review` | **Object
|
|
57
|
+
| `visual_review` | **Object.** `record polish` needs the package capture record `captures`, which every `polish capture` run at this version writes. That record must be package-produced, bound to the current build and intact, and every image it claims must be a full-page capture at the capture's own viewport width at pixel ratio 1. Every planned route must have at least one rendered build cell with a captured actual shot; an all-skipped or all-failed build side does not qualify. Screenshot paths written by an agent are not read. After upgrading, run `polish capture` again before `record polish`. A Polish record written by an earlier version, without `captures`, is still read as before: a `screenshots` array (aliases `screenshot_paths`, `paths`, `urls`) with at least one non-empty string. Never hand-author `page_load` or `captures`. |
|
|
58
58
|
| `brand_review` | Non-empty object (semantic checks in §3 apply: `favicon`, `brand_bleed`). |
|
|
59
59
|
| `checkout_review` | Non-empty object (semantic checks in §3 apply: `field_labels`, `bump_compare_price_rule`). |
|
|
60
60
|
| `template_residue_review` | Non-empty object/array/string (semantic check on `starter_favicon` in §3). |
|
|
@@ -121,6 +121,42 @@ The size check covers `<img>` elements in the page, including those inside open
|
|
|
121
121
|
|
|
122
122
|
**Readability.** `polish capture` also measures text contrast on every page under `_site/<slug>/` (up to 128 pages per run), at desktop (1440×1200) and mobile (390×844) widths, device pixel ratio 1, including campaigns whose pages are all template stock: the Polish route plan now covers the mapped pages plus every built page, while the page-load capture keeps its own page set. On a campaign with no mapped pages, `polish capture` measures readability, prints the page-load message as a warning, and exits successfully. The text it reads: purchase, accept and decline controls; submit buttons and submit-type inputs; SDK action controls; bundle cards (names, prices, badges and the card selected at load); order bumps; prices; checkout labels and placeholder hints; and other body text. Ratios follow WCAG 2.x and are compared unrounded: 4.5:1 for normal text, 3:1 for large text (at least 24 CSS px, or at least 18.66 CSS px at weight 700 or more; semibold does not qualify). Colours in any CSS Color 4 form the browser reports are converted to sRGB; values outside sRGB are clipped and marked. Backgrounds are composited through transparent layers. SVG text is measured with its fill colour, and placeholder hints with their own colour, size and weight. Gradients, background images, pseudo-element backgrounds, a `::first-line` or `::first-letter` style that changes the text's colour, background or opacity, overlapping positioned layers or images (and the shapes of an SVG drawing under its text), text clipped to a background, colours that cannot be read (including a text fill colour, and an SVG fill that is not a plain colour or is combined with a stroke), and controls marked disabled only by the SDK's `next-disabled` class produce a review result with a reason and a crop; so do opacity, filters, blend modes and masks on the text or on any element around it. `clip-path` is ignored. Disabled controls (`disabled` or `aria-disabled="true"`) are listed as excluded (inactive control) and are never accepted; `readonly` is not disabled. Polish measures the states present when the page loads — default, selected, active, and open panels — without clicking, scrolling or typing. Text that has not rendered, controls still loading, controls hidden at load, text supplied by CSS `content`, selection states and collapsed panels not present at load, text inside closed shadow roots or cross-origin frames, pages measured before the SDK, stylesheets or fonts were ready, and pages not captured are listed as unexercised. Overlapping layers are detected by box intersection: a layer the page paints over text in another way is not detected. A ratio under the requirement is a warning. Warnings never block Polish or change readiness. The probe reads the page without changing it, and adds at most about five minutes of work per run; pages it could not reach in that time are listed as unexercised. The stored measurements are re-derived on every read, and the page set, widths and campaign are checked against the current build; but the colours the browser reported are tamper-evident only, like other Polish captures. Readability and media weight results read as stale when the built output differs from the recorded build, even before a new `record build`.
|
|
123
123
|
|
|
124
|
+
**Layout conventions.** In the same pass as readability, at the mobile width, `polish capture` checks two layout conventions from the `layout_conventions` block of `contracts/template-brand-contract.shared-commerce.v0.json`. Each reports as a `layout.conventions` row, keyed by convention, on every built page. The first, `floating_cta`, covers floating CTA containers. A floating CTA container is the nearest box around a visible link or button whose computed position is `fixed` or `sticky`. Headers, navigation, dialogs, the announcement bar, boxes that hold a visible form field, and boxes taller than half the viewport do not count. Such a container should hold its control alone: one link or button and no other visible text. A container with a second control, or with text outside its control (rendered or CSS-generated), is a `floating_cta_extra_content` warning that names the container, its controls and up to ten of the other text elements. The record keeps the container's box and each control's box. The second, `cta_label_size`, reads the readability record's own measurements. Every rendered, enabled text element of a purchase or accept control or a submit button (readability roles `add_to_cart`, `upsell_accept` and `submit_control`) should be at least 16 CSS px. A smaller one is a `cta_label_too_small` warning with its measured size. A CTA that is a plain link with no SDK action reads as body text in the readability record and is not checked for label size. A page that meets both conventions reads pass. Both warnings can be accepted with `checkpoint accept`; neither blocks Polish, changes readiness, or reads the merchant's copy. A readability record captured before this check holds no layout, and the check reads `not_captured_by_this_version` until the next `polish capture`.
|
|
125
|
+
|
|
126
|
+
**Element parity.** For each page built from provided source HTML, `polish capture` reads the source HTML render and the built page that the package capture already makes, with the same browser at the same width, height and pixel ratio (desktop 1440×1200 and mobile 390×844). On both it reads every visible element that holds text: its box, its text, its computed font family, size, weight, style, line height, letter spacing and text transform, and its text and background colour. Elements are matched by their text: exact matches that occur once on each side first, then the nearest exact match after removing the page's median offset, then text split across child elements, then close wording (at least 60% of words shared). A design text of four or more characters is reported missing only when it appears nowhere in the built page's visible text; where part of the built page could not be read (very long text, closed components, frames), it is listed for review instead. Differences become warnings when they exceed: 6 px for horizontal position, width and height, and for the vertical gap to the previous matched element (so one inserted band reports once, where the layout changed); 0.6 px font size; 2 px line height; 0.2 px letter spacing; 3 of 255 in any colour channel; any change of font family, weight, style or text transform. A web font that loaded for the source but not for the build is a warning even when the CSS names the same family. The font each glyph was actually drawn with is not read, and the handoff says so. Colour that cannot be measured on either side is listed for review. A source with no text, a source or build that could not be rendered or was not ready in time, a width or pixel-ratio mismatch (never resized), a source file that differs from the one recorded at intake, pages over the run's limits (1 500 elements per page, 32 pages, 200 font faces, about one minute of probing), and pages with unread parts are listed as unexercised or for review, never as a match. The probe's time counts against the package capture's own time budget. Both measurements are saved as local, git-ignored files, hashed and bound to the record. That makes an edit to part of the evidence detectable, but a process that rewrites the files and both records consistently is not detected. The source is measured again on every capture, so changed stylesheets or fonts are always seen; an unchanged source produces the same saved file. Warnings can be accepted with `checkpoint accept` and lapse when the measured values change; they never block Polish or change readiness. The comparison uses the HTML export as the design; a design file recorded for the page is not read by this version.
|
|
127
|
+
|
|
128
|
+
### Package captures and design comparison
|
|
129
|
+
|
|
130
|
+
After its existing page-load and readability work has finished and been recorded, `polish capture` takes full-page captures of every mapped page at desktop (1440×1200) and mobile (390×844), device pixel ratio 1, in a freshly loaded state with no scrolling or interaction. It also renders each page's source HTML from the source folder with the same browser at the same size. This visual pass uses its own browser instances, so its failures never change page-load or readability evidence.
|
|
131
|
+
|
|
132
|
+
The source is served read-only from a local address, in a browser instance that can reach only that address. Requests and WebSocket connections to anything else are blocked and counted, other connections are refused by the local server, and no host name is looked up. Nothing from the source render leaves the machine.
|
|
133
|
+
|
|
134
|
+
Each render waits for web fonts (up to 3 seconds) and for `[data-next-await]` elements to clear (up to 5 seconds) before either capture. The first capture is unchanged. The second differs only by its stabilization treatment: it stops animations and transitions, hides the caret, and masks SDK timers and prices. Timeouts are recorded, never hidden.
|
|
135
|
+
|
|
136
|
+
Images are stored under `.campaign-runtime/polish/captures/` and `.campaign-runtime/polish/diff/`, named by their SHA-256. Each folder gets its own `.gitignore` the first time an image is written, so the images stay out of the repository even in a campaign set up by an earlier version. Old images are not deleted.
|
|
137
|
+
|
|
138
|
+
Source screenshots recorded in the Design Source Package are opened and checked. The PNG structure is read without dependencies, and the image is fully decoded when the optional `pngjs` package is installed. An image whose declared size exceeds 17.28 million pixels (1440 × 12,000) is not decoded and is reported as not verifiable. Each screenshot is recorded as header-checked, decoded, or not verifiable, with its SHA-256 (computed when none was recorded) and identity.
|
|
139
|
+
|
|
140
|
+
A reference or source page that is the build's own output is reported as self-referential. That means a file inside `_site/` or `.campaign-runtime/`, a file identical to a Polish capture or a built page, or the campaign's own Page Kit page. Accepting that warning records the output as a revision baseline, not as proof that the page matches the design. A self-referential file that also fails verification cannot be accepted. A reference whose origin cannot be established is listed for review.
|
|
141
|
+
|
|
142
|
+
Compatible pairs are compared with the optional `pixelmatch` package:
|
|
143
|
+
- images are padded, never resized;
|
|
144
|
+
- width and height differences are reported;
|
|
145
|
+
- a heatmap is written;
|
|
146
|
+
- the most different areas are listed per section.
|
|
147
|
+
|
|
148
|
+
Sections are elements marked with `data-section`, or, when a page has none, `section`, `header` and `footer` elements with an `id`. Each section is cut from each image at its own position, so a taller section above it does not shift the comparison. A section missing or repeated on one side is reported as not comparable. Areas masked on either side are excluded from both sides. When a page is taller than the 12,000-pixel capture limit, the whole-page figure is not computed, and only sections inside both captures are compared.
|
|
149
|
+
|
|
150
|
+
Source screenshots are compared as whole images only, and only when their width and recorded `scale_factor` match the capture. Otherwise the comparison is reported as not supported. Their stabilization, state, crop and viewport height are not recorded, and each comparison says so.
|
|
151
|
+
|
|
152
|
+
The comparison is a diagnostic for a reviewer: it is listed for review, it never passes or fails Polish, and it never changes readiness. When `pngjs` or `pixelmatch` is not installed, captures are still taken and the comparison is listed as unavailable. Animations driven by script and content below the fold that loads on scroll are limits of the comparison. The visual pass adds at most five minutes per run, and pages it could not reach in that time are listed as unexercised.
|
|
153
|
+
|
|
154
|
+
The stored comparison figures are tamper-evident only, like other Polish captures. The images are re-checked on every read against their SHA-256 and against the pixel size recorded for them.
|
|
155
|
+
|
|
156
|
+
Template-stock pages are outside the mapped capture plan and produce no visual rows.
|
|
157
|
+
|
|
158
|
+
Hand-deleting `captures` reverts later reads to the legacy rule, but a new completed `record polish` still refuses. A fully reconstructed, internally consistent record can pass; the checksum is tamper evidence, not authorship proof. Pixel metrics and geometry are range-checked rather than recomputed on every read.
|
|
159
|
+
|
|
124
160
|
### Collector response records (the producer's wire form)
|
|
125
161
|
|
|
126
162
|
Inside the producer, the browser adapter's CDP collector emits one
|
|
@@ -469,6 +505,8 @@ deliberately). Waived runs pass with status `waived`, never silently.
|
|
|
469
505
|
|
|
470
506
|
## 6. Complete passing example
|
|
471
507
|
|
|
508
|
+
Run `polish capture` first to attach `page_load` and `captures`; the example omits these package-owned records. Screenshot paths below document review locations and cannot satisfy a new completed record.
|
|
509
|
+
|
|
472
510
|
The hand-authored portion below is completed first. Run `campaigns-os polish
|
|
473
511
|
capture` to attach the versioned `visual_review.page_load` object; that package
|
|
474
512
|
artifact is intentionally not reproduced as editable JSON here.
|
|
@@ -520,6 +558,8 @@ stack).
|
|
|
520
558
|
|
|
521
559
|
## 7. Recording with `record polish`
|
|
522
560
|
|
|
561
|
+
After upgrading, run `polish capture` again. A completed record requires its current `captures` record; leave it out of the evidence file so the captured value is carried forward. Existing records without `captures` retain the legacy read rule.
|
|
562
|
+
|
|
523
563
|
```bash
|
|
524
564
|
campaigns-os record polish --packet <campaign-runtime.build.json> --evidence <polish-evidence.json> [--report <json>] [--dry-run] [--json]
|
|
525
565
|
```
|
|
@@ -531,7 +571,7 @@ any other key is refused:
|
|
|
531
571
|
| Key | Required | Written to |
|
|
532
572
|
|---|---|---|
|
|
533
573
|
| `status` | no (default `completed`) | `stages.polish.status`; `completed`, `completed_with_warnings`, `blocked` or `skipped` |
|
|
534
|
-
| `evidence` | for `completed*` | `stages.polish.evidence`, the seven categories of §2. Leave out `visual_review.page_load`: the value `polish capture` recorded is kept, and a file that carries one is refused. Without it, a `blocked` or `skipped` record keeps the evidence already on the report. |
|
|
574
|
+
| `evidence` | for `completed*` | `stages.polish.evidence`, the seven categories of §2. Leave out `visual_review.page_load` and `visual_review.captures`: the value `polish capture` recorded is kept, and a file that carries one is refused. Without it, a `blocked` or `skipped` record keeps the evidence already on the report. |
|
|
535
575
|
| `blockers` | for `blocked` only | `stages.polish.blockers`: a non-empty array of `{"code", "message"}` objects. |
|
|
536
576
|
| `skip_reason` | for `skipped` only | `stages.polish.skip_reason`: a non-empty string. |
|
|
537
577
|
| `repair_loop_defect` | no | `report.theme.repair_loop_defect`: `null`, or the first brand-layer repair-loop defect as an object (`code`, `message`, `path`, `detail`). A non-null defect needs a recorded `report.theme`. |
|
|
@@ -65,10 +65,13 @@ rename; a live process is never evicted. A lock directory with no owner record
|
|
|
65
65
|
a second. If one is found, or if recovery itself is interrupted, capture fails
|
|
66
66
|
closed and the warning names the affected `.allocation-lock` directory: stop
|
|
67
67
|
all Campaigns OS writers for that target, then remove that directory before
|
|
68
|
-
retrying `next`. A file or other non-lock entry at
|
|
69
|
-
the warning and never removed: move it out of the
|
|
70
|
-
while a writer is active.
|
|
71
|
-
|
|
68
|
+
retrying `next`. A file, an unreadable directory or other non-lock entry at
|
|
69
|
+
the lock path is named in the warning and never removed: move it out of the
|
|
70
|
+
way. Do not remove a lock while a writer is active. Run one Campaigns OS
|
|
71
|
+
release against a target at a time: a lock held by a live process of another
|
|
72
|
+
release (its owner record has no `lock_format`, or a different one) is refused
|
|
73
|
+
at once and the warning names that process. Two releases that start at the
|
|
74
|
+
same instant are not detected.
|
|
72
75
|
|
|
73
76
|
An unchanged projection reuses its ID, timestamp and sequence. Identity
|
|
74
77
|
changes start a new stream. Each local scope retains at most 32 snapshots and
|
|
@@ -134,6 +134,21 @@ waiver commands keep them strict. Progress
|
|
|
134
134
|
snapshots record a carried-forward gate as `not_applicable`, because their
|
|
135
135
|
schema has no carried-forward state.
|
|
136
136
|
|
|
137
|
+
### Polish advisory for a page-only change
|
|
138
|
+
|
|
139
|
+
After launch, a change that touches only some pages' HTML and leaves checkout,
|
|
140
|
+
commerce markup, routes, scripts and the campaign's Page Kit config alone is a
|
|
141
|
+
page-only change ([what a change owes](campaigns-os-build-flow.md#what-a-change-owes)).
|
|
142
|
+
On any target, its stale Polish evidence (`polish.evidence_missing` after
|
|
143
|
+
`record build`, `polish.stale`, `polish.output_drift`, and the
|
|
144
|
+
`polish.hidden_eager_media.capture_stale` checkpoint) is carried forward with
|
|
145
|
+
the `page_only_change` policy. Doctor reports it as a warning starting
|
|
146
|
+
"Advisory for a page-only change"; `next` and `record deploy` move past Polish;
|
|
147
|
+
QA records it as a `skipped` row, so it does not hold the verdict to
|
|
148
|
+
`ready_with_exceptions`. A Polish recorded as blocked, incomplete or
|
|
149
|
+
self-certified stays blocking, and so does a stage a brief or spec change
|
|
150
|
+
requires.
|
|
151
|
+
|
|
137
152
|
## Resolve
|
|
138
153
|
|
|
139
154
|
Use resolve before a full run:
|
|
@@ -382,6 +397,67 @@ currency is recorded as `evidence.currency` on the entry page's
|
|
|
382
397
|
`http:<page_id>` assertion; an entry URL that does not parse gets no currency,
|
|
383
398
|
and its row records `evidence.currency_not_applied` with the reason instead.
|
|
384
399
|
|
|
400
|
+
The run also checks that the currency took. Campaign Cart falls back to the
|
|
401
|
+
campaign's default currency when the requested one is not available, so a page
|
|
402
|
+
can load with `?currency=GBP` and still take a USD order:
|
|
403
|
+
|
|
404
|
+
- Each test order gets a `browser-order-currency:<path>` row comparing the
|
|
405
|
+
created order's `currency` (read from the order API response) with the run
|
|
406
|
+
currency. An order in any other currency fails as a blocker, and so does a
|
|
407
|
+
verified order whose currency was not recorded. The row's evidence is
|
|
408
|
+
`{ requested, order_currency }`.
|
|
409
|
+
- Automatic commercial parity (below) asks the calculate endpoint for expected
|
|
410
|
+
totals in the run currency, in place of the Map's `campaign.currency`. When
|
|
411
|
+
the endpoint answers in another currency, the campaign does not price in the
|
|
412
|
+
run currency: a `commercial-currency:<RUN>:<ANSWERED>` row (family `pricing`)
|
|
413
|
+
fails as a blocker, and the commercial report lists the
|
|
414
|
+
`calculate_currency_mismatch` issue, so its status reads `incomplete`. The
|
|
415
|
+
row's id names the answered code upper-cased, one row per answered currency,
|
|
416
|
+
and it is written even when the verdict's assertion budget is used up.
|
|
417
|
+
- Commercial parity reads prices from each page's source HTML, where a bound
|
|
418
|
+
price such as `data-next-display="bundle.main.price"` holds the authored
|
|
419
|
+
fallback text in the Map's `campaign.currency` (Campaign Cart replaces it at
|
|
420
|
+
runtime). When the run currency is not the Map's `campaign.currency`, those
|
|
421
|
+
price and recurring-price claims are not compared with the run-currency
|
|
422
|
+
totals: they count as unresolved, and the commercial report records
|
|
423
|
+
`unresolved_money_claims_reason: "static_source_not_in_run_currency"`. A
|
|
424
|
+
correctly built multi-currency page gets no `price-claim-mismatch` warning,
|
|
425
|
+
and the commercial status reads `incomplete` instead of `pass`. With
|
|
426
|
+
`--currency` set to the Map's own currency, the claims are compared as
|
|
427
|
+
before.
|
|
428
|
+
|
|
429
|
+
Either blocker makes the verdict `blocked` and the command exit `4`. Without
|
|
430
|
+
`--currency` neither row is written and expected totals use
|
|
431
|
+
`campaign.currency`, as before.
|
|
432
|
+
|
|
433
|
+
`--currency` sets the currency only. Test orders still ship to the address the
|
|
434
|
+
`--test-*` flags give, a US address by default (see
|
|
435
|
+
[Test customer and shipping address](#test-customer-and-shipping-address)). A
|
|
436
|
+
non-USD `--currency` on a run that places orders, with no `--test-country`,
|
|
437
|
+
prints a warning on stderr and the run goes ahead. Only the missing flag is
|
|
438
|
+
checked: a currency such as EUR covers many countries, so an explicit
|
|
439
|
+
`--test-country` is taken as meant. `qa run --site` takes `--currency` the same
|
|
440
|
+
way and tags the slug's entry page.
|
|
441
|
+
|
|
442
|
+
`--page <page_id>` (repeatable) checks only the named pages. It pays the QA a
|
|
443
|
+
page-only change owes: `doctor --ci` and `next` name the changed pages by
|
|
444
|
+
their route ids (`upsell1` for `upsell1/index.html`), and the CampaignSpec page
|
|
445
|
+
id is accepted too. It needs `--packet`, places no test orders (it refuses
|
|
446
|
+
`--test-order` and `--legacy-api-test-order` other than `off`, and
|
|
447
|
+
`--vendor-beacons`), and refuses before checking anything when the
|
|
448
|
+
output on disk is not the recorded build (run `record build` first), when the
|
|
449
|
+
change since the build QA reviewed touches commerce, when a changed page is
|
|
450
|
+
left out, when the QA it would extend did not complete (a blocked full run
|
|
451
|
+
owes a full run), or when QA's inputs (the brief's `qa_policy` and the
|
|
452
|
+
CampaignSpec) changed or cannot be confirmed current. Every other page gets a
|
|
453
|
+
`page-scope:<page_id>` row, `skipped` with `outside_page_scope`. The QA stage
|
|
454
|
+
it records carries `evidence.page_scope` (`pages`, `owed_pages`,
|
|
455
|
+
`base_build_fingerprint`) and keeps the `purchase_proof` of the full run it
|
|
456
|
+
extends. Its `evidence.qc_results` holds the rows for the scoped pages only,
|
|
457
|
+
so until the next full run the QC handoff coverage reports the earlier full
|
|
458
|
+
run's other QA checks (order paths and tracking among them) as not captured.
|
|
459
|
+
QA already recorded in full for the current build is re-run without `--page`.
|
|
460
|
+
|
|
385
461
|
### Automatic commercial parity
|
|
386
462
|
|
|
387
463
|
When the CampaignSpec has enabled pages with package rows, the same `qa run`
|
|
@@ -432,7 +508,11 @@ readback consumes. It is written for every finalized disposition, blocked
|
|
|
432
508
|
included: the sidecar records what QA concluded, it is not a pass mark. A run
|
|
433
509
|
that dies before verdict finalization or fails local validation never touches
|
|
434
510
|
an existing sidecar. Packet-less runs (`--site`, raw map-id) have no packet
|
|
435
|
-
home and write no sidecar.
|
|
511
|
+
home and write no sidecar. For a project's second and later funnels the sidecar
|
|
512
|
+
is `.campaign-runtime/funnels/<route>/qa-verdict.json` beside that funnel's
|
|
513
|
+
packet, the runtime directory its `assembly.runtime_dir` names (see
|
|
514
|
+
[Several funnels in one project](build-packet.md#several-funnels-in-one-project));
|
|
515
|
+
`qa promote` and `qa publish` read and write the same file.
|
|
436
516
|
|
|
437
517
|
The sidecar is an allowlist **projection** of the full verdict, same schema
|
|
438
518
|
(`1.0`), stamped with its own `generated_at` at promotion time. Full verdicts
|
|
@@ -1144,7 +1224,7 @@ What the diff asserts (BLOCKER unless noted):
|
|
|
1144
1224
|
For a real SDK 0.4 migration example that required typed-card post-purchase
|
|
1145
1225
|
traversal, persisted-order price verification, receipt-context analytics, and
|
|
1146
1226
|
independent order readback, see
|
|
1147
|
-
[SDK 0.4 Migration Proof Case Study](sdk04-migration-proof-case-study.md).
|
|
1227
|
+
[SDK 0.4 Migration Proof Case Study](https://github.com/NextCommerceCo/campaigns-os/blob/main/docs/sdk04-migration-proof-case-study.md).
|
|
1148
1228
|
|
|
1149
1229
|
## Parity capture (fixture-driven migration proof)
|
|
1150
1230
|
|
|
@@ -1329,6 +1409,80 @@ controls on an ordinary or unknown page remain blockers; they are not treated
|
|
|
1329
1409
|
as evidence that a receipt was reached. Cross-origin handoffs count as terminal
|
|
1330
1410
|
navigation, but not as Campaigns OS receipt rendering or persisted-receipt proof.
|
|
1331
1411
|
|
|
1412
|
+
### Analytics vendor beacons (`--vendor-beacons live`)
|
|
1413
|
+
|
|
1414
|
+
A test order is a real purchase on the page, so the page reports it to every
|
|
1415
|
+
analytics vendor the campaign loads. To keep test orders out of vendor data, a
|
|
1416
|
+
`qa run --test-order` run answers the vendors' beacon requests locally. A
|
|
1417
|
+
beacon is the request a vendor script sends to report a page view or an event,
|
|
1418
|
+
as opposed to the loader, the request that downloads the vendor script. Every
|
|
1419
|
+
browser context the run opens (page checks, analytics capture and the orders)
|
|
1420
|
+
answers a request to a listed beacon endpoint with an empty `204` response, so
|
|
1421
|
+
nothing reaches the vendor. Those contexts also block service workers, because
|
|
1422
|
+
a request a page's service worker makes on the page's behalf would bypass the
|
|
1423
|
+
local answer. Loader scripts load normally: a blocked loader would leave the
|
|
1424
|
+
vendor script undefined, and the page would send nothing to observe.
|
|
1425
|
+
|
|
1426
|
+
| Vendor | Beacon endpoints answered locally | Loaders that still load |
|
|
1427
|
+
|---|---|---|
|
|
1428
|
+
| GA4 (`ga4`) | `*.google-analytics.com/g/…`, `analytics.google.com/g/…`, `stats.g.doubleclick.net/g/…`, `www.google.com/g/collect` | `www.googletagmanager.com/gtag/js` |
|
|
1429
|
+
| Meta (`meta`) | `www.facebook.com/tr`, `www.facebook.com/privacy_sandbox/…` | `connect.facebook.net` |
|
|
1430
|
+
| TikTok (`tiktok`) | `analytics.tiktok.com/api/v2/…`, `analytics-ipv6.tiktokw.us` | `analytics.tiktok.com/i18n/pixel/…` |
|
|
1431
|
+
| Snap (`snap`) | `tr.snapchat.com`, `tr6.snapchat.com`, `tr-shadow.snapchat.com` | `sc-static.net` |
|
|
1432
|
+
| Pinterest (`pinterest`) | `ct.pinterest.com/v3/…`, `/user/…`, `/stats/…` | `s.pinimg.com`, `ct.pinterest.com/static/…` and `/ct.html` |
|
|
1433
|
+
| Taboola (`taboola`) | `trc.taboola.com` | `cdn.taboola.com` |
|
|
1434
|
+
| Triple Whale (`triplewhale`) | `api.config-security.com` (all but `/first`), `api.whaledb.io/keep-alive` | `api.config-security.com/first`, `conf.config-security.com` |
|
|
1435
|
+
| AppLovin Axon (`axon`) | `b.applovin.com/v1/pixel` | `s.axon.ai`, `c.albss.com`, `res4.applovin.com` |
|
|
1436
|
+
|
|
1437
|
+
Each endpoint was read from the vendor's public loader script or public
|
|
1438
|
+
documentation; the source is recorded per vendor in
|
|
1439
|
+
`src/qa-vendor-endpoints.mjs`. Two vendor keys are not intercepted, and the
|
|
1440
|
+
verdict names them:
|
|
1441
|
+
|
|
1442
|
+
- `northbeam`: its beacon host could not be confirmed (the loader is per client
|
|
1443
|
+
and the public docs name no beacon host), so Northbeam still receives what
|
|
1444
|
+
the page sends it, test purchases included.
|
|
1445
|
+
- `gtm`: Google Tag Manager sends nothing of its own. The tags it loads send to
|
|
1446
|
+
their own vendors, held back only when that vendor is in the table. A GTM tag
|
|
1447
|
+
for any other vendor (Google Ads, an affiliate pixel) still sends.
|
|
1448
|
+
|
|
1449
|
+
`qa parity` orders are not intercepted.
|
|
1450
|
+
|
|
1451
|
+
Every request to a listed endpoint is recorded, and the verdict carries one
|
|
1452
|
+
informational row, `browser-test-order:vendor-beacons` (status `pass`, severity
|
|
1453
|
+
`info`). Its `evidence` gives, per vendor seen, the request count, whether one
|
|
1454
|
+
of the requests named a purchase event (`purchase_seen`; `null` for Axon, whose
|
|
1455
|
+
event field could not be confirmed), and the ids, event names and endpoints
|
|
1456
|
+
(origin and path only, never a query string); `configured_not_seen` lists the
|
|
1457
|
+
vendors the CampaignSpec analytics block declares that sent nothing to a listed
|
|
1458
|
+
endpoint; `configured_not_observable` lists the declared vendors with no listed
|
|
1459
|
+
endpoint (GTM, Northbeam), which the row cannot see either way; and
|
|
1460
|
+
`not_intercepted` lists the two vendors above with the reason for each (with
|
|
1461
|
+
`live`, every vendor). The row is evidence only. A
|
|
1462
|
+
request the page sent proves the page sent it, not that the vendor received it,
|
|
1463
|
+
so it never counts as vendor purchase proof and never blocks the run. Purchase
|
|
1464
|
+
proof stays with `analytics-correctness:purchase-fires` (the dataLayer
|
|
1465
|
+
purchase, a Meta Purchase pixel request, or a GA4 purchase hit); with
|
|
1466
|
+
interception on, the Meta and GA4 requests behind it are still observed, just
|
|
1467
|
+
answered locally. The same requests feed the analytics inventory and parity
|
|
1468
|
+
legs as tag fires.
|
|
1469
|
+
|
|
1470
|
+
To send the beacons to the vendors, pass `--vendor-beacons live`. `live` is
|
|
1471
|
+
the only value; any other value is refused before anything runs, and so is the
|
|
1472
|
+
flag on a run without `--test-order`, where it would change nothing. The row
|
|
1473
|
+
then records the requests as sent.
|
|
1474
|
+
|
|
1475
|
+
```bash
|
|
1476
|
+
npm run campaigns-os -- qa run \
|
|
1477
|
+
--packet campaign-runtime.build.json \
|
|
1478
|
+
--test-order common \
|
|
1479
|
+
--vendor-beacons live
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
Do not hold beacons back with `?ignore=true` on the campaign URL: from SDK
|
|
1483
|
+
0.4.42 it stops the SDK's `dl_*` events entirely, so the run would prove no
|
|
1484
|
+
purchase event at all.
|
|
1485
|
+
|
|
1332
1486
|
### Purchase-proof coverage (`--test-order off` is a diagnostic)
|
|
1333
1487
|
|
|
1334
1488
|
A QA run finalizes a verdict and records a terminal stage status whether or not
|
|
@@ -1959,6 +2113,41 @@ that died mid-submit. QA reports it on the path's `browser-test-order` row as
|
|
|
1959
2113
|
(or `--test-email`), or wait. The shipping address is not part of the match, so
|
|
1960
2114
|
changing `--test-address1` does not help.
|
|
1961
2115
|
|
|
2116
|
+
### Test customer and shipping address
|
|
2117
|
+
|
|
2118
|
+
The runner fills the checkout as one test customer. Every field has a default,
|
|
2119
|
+
and each has a flag:
|
|
2120
|
+
|
|
2121
|
+
| Flag | Default | Field |
|
|
2122
|
+
|---|---|---|
|
|
2123
|
+
| `--test-first-name` | `QA` | first name (shipping and billing) |
|
|
2124
|
+
| `--test-last-name` | `Playwright` | last name |
|
|
2125
|
+
| `--test-phone` | empty | phone, filled only when the checkout has the field |
|
|
2126
|
+
| `--test-email` | see above | email |
|
|
2127
|
+
| `--test-country` | `US` | country select (value or label, such as `GB`) |
|
|
2128
|
+
| `--test-address1` | `1600 Amphitheatre Pkwy` | street address |
|
|
2129
|
+
| `--test-city` | `Mountain View` | city |
|
|
2130
|
+
| `--test-province` | `CA` | state, province or region select (value or label) |
|
|
2131
|
+
| `--test-postal` | `94043` | postal code |
|
|
2132
|
+
|
|
2133
|
+
The billing fields get the same values when the checkout shows them. To order
|
|
2134
|
+
in another market, pass `--test-country` and all four address flags for a real
|
|
2135
|
+
address in that country, since the address defaults are a US one, and pass
|
|
2136
|
+
`--currency` for that market's currency: the country flag does not change the
|
|
2137
|
+
currency. A UK run looks like this:
|
|
2138
|
+
|
|
2139
|
+
```bash
|
|
2140
|
+
npm run campaigns-os -- qa run --packet campaign-runtime.build.json \
|
|
2141
|
+
--currency GBP --browser --test-order common \
|
|
2142
|
+
--test-country GB --test-address1 "<street>" --test-city "<city>" \
|
|
2143
|
+
--test-province "<option from the province select>" --test-postal "<postcode>"
|
|
2144
|
+
```
|
|
2145
|
+
|
|
2146
|
+
The province select is a required step. A checkout that shows no province
|
|
2147
|
+
select for the chosen country fails the path at that field
|
|
2148
|
+
(`Missing checkout select: province`); place those orders by hand on the
|
|
2149
|
+
market-pinned page (`?currency=<code>&country=<code>`).
|
|
2150
|
+
|
|
1962
2151
|
The browser driver intentionally behaves like a user:
|
|
1963
2152
|
|
|
1964
2153
|
- package selection uses rendered `[data-next-package-id]` controls when
|
|
@@ -2116,10 +2305,15 @@ npm run campaigns-os -- doctor --built ../my-campaign-repo --family arjuna --emi
|
|
|
2116
2305
|
|
|
2117
2306
|
# QA a built, served campaign with no packet/spec.
|
|
2118
2307
|
npm run campaigns-os -- qa run --site ../my-campaign-repo --base-url http://localhost:8080 --family arjuna --browser
|
|
2308
|
+
|
|
2309
|
+
# The same, entering the slug's first page in GBP.
|
|
2310
|
+
npm run campaigns-os -- qa run --site ../my-campaign-repo --base-url http://localhost:8080 --family arjuna --slug my-slug --currency GBP
|
|
2119
2311
|
```
|
|
2120
2312
|
|
|
2121
2313
|
`--family` is required (the residue gates need the family's brand contract).
|
|
2122
|
-
`--slug` selects the campaign when `_site/` holds more than one.
|
|
2314
|
+
`--slug` selects the campaign when `_site/` holds more than one. `--base-url`
|
|
2315
|
+
can be the served site origin or the campaign root: QA adds the campaign's slug
|
|
2316
|
+
when the URL does not already end in it, so pages resolve under `/<slug>/`. With no theme
|
|
2123
2317
|
artifacts the theme gate resolves to `not_applicable` (non-blocking), so the
|
|
2124
2318
|
placeholder-text blocker and the other residue gates still run. The emitted
|
|
2125
2319
|
minimal packet is marked `_synthesized` — it points doctor/QA at the built
|