@nextcommerce/campaigns-os 1.59.0 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +242 -0
  2. package/README.md +11 -10
  3. package/agents/claude/CLAUDE.md +15 -0
  4. package/agents/codex/AGENTS.md +15 -0
  5. package/agents/copilot/copilot-instructions.md +15 -0
  6. package/agents/cursor/campaigns-os.mdc +15 -0
  7. package/compatibility.json +1 -1
  8. package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
  9. package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
  10. package/contracts/commerce-surface-catalog.json +27 -26
  11. package/contracts/release-ledger.json +1311 -0
  12. package/contracts/supported-surface.json +4 -4
  13. package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
  14. package/docs/access-model.md +20 -0
  15. package/docs/brand-theme-bridge.md +62 -3
  16. package/docs/build-packet.md +42 -13
  17. package/docs/campaign-build-brief.md +17 -1
  18. package/docs/campaigns-os-build-flow.md +62 -3
  19. package/docs/campaignspec-authoring-examples.md +116 -0
  20. package/docs/design-source-package.md +46 -2
  21. package/docs/entry-points.md +188 -0
  22. package/docs/local-setup.md +1 -1
  23. package/docs/orientation-contract-reference.md +1 -1
  24. package/docs/polish-evidence.md +41 -3
  25. package/docs/qa-and-test-orders.md +50 -2
  26. package/docs/quickstart.md +438 -0
  27. package/docs/readback.md +119 -6
  28. package/docs/runtime-readiness.md +1 -1
  29. package/docs/sdk-storage-compatibility.md +3 -1
  30. package/docs/skills-revision.md +10 -10
  31. package/docs/source-adapters.md +213 -0
  32. package/package.json +10 -3
  33. package/schemas/campaign-design-source-package.v0.schema.json +8 -0
  34. package/schemas/campaigns-os-readback.v2.schema.json +177 -1
  35. package/schemas/source-html-manifest.v0.schema.json +3 -0
  36. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  37. package/skills/campaign-readback-classification/SKILL.md +3 -3
  38. package/skills/campaign-run-evidence/SKILL.md +3 -3
  39. package/skills/contribution-intake/SKILL.md +3 -3
  40. package/skills/next-campaigns-build/SKILL.md +5 -5
  41. package/skills/next-campaigns-os/SKILL.md +4 -3
  42. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  43. package/skills/next-campaigns-polish/SKILL.md +12 -16
  44. package/skills/next-campaigns-qa/SKILL.md +3 -3
  45. package/skills/next-campaigns-sdk-bump/SKILL.md +286 -0
  46. package/skills.json +18 -10
  47. package/src/brand-theme.mjs +413 -8
  48. package/src/brief-summary.mjs +6 -2
  49. package/src/browser-launch.mjs +2 -1
  50. package/src/build-brief.mjs +9 -2
  51. package/src/built-site-scope.mjs +7 -14
  52. package/src/campaign-identity.mjs +6 -4
  53. package/src/cli.mjs +89 -73
  54. package/src/design-file-reference.mjs +93 -0
  55. package/src/design-source-package.mjs +55 -1
  56. package/src/doctor/checks.mjs +14 -77
  57. package/src/doctor/ci.mjs +112 -0
  58. package/src/doctor/inspect.mjs +20 -8
  59. package/src/doctor/next-step.mjs +6 -3
  60. package/src/doctor/source-provenance.mjs +74 -0
  61. package/src/evidence-debt.mjs +119 -0
  62. package/src/orchestration-stage-contract.mjs +6 -3
  63. package/src/page-kit-build-summary.mjs +21 -1
  64. package/src/page-kit-campaign-config.mjs +19 -0
  65. package/src/parity/compare.mjs +316 -0
  66. package/src/parity/design-access.mjs +54 -0
  67. package/src/parity/element-parity-record.mjs +569 -0
  68. package/src/parity/probe.mjs +205 -0
  69. package/src/parity/snapshot.mjs +110 -0
  70. package/src/png-header.mjs +87 -0
  71. package/src/polish-browser.mjs +186 -9
  72. package/src/polish-captures-record.mjs +52 -0
  73. package/src/polish-deadline.mjs +14 -0
  74. package/src/polish-gate.mjs +26 -11
  75. package/src/polish-node.mjs +81 -7
  76. package/src/polish-visual-fixtures.mjs +74 -0
  77. package/src/polish-visual.mjs +606 -0
  78. package/src/progress-node.mjs +2 -1
  79. package/src/qa-analytics-correctness.mjs +14 -2
  80. package/src/qa-analytics-parity.mjs +94 -5
  81. package/src/qa-node.mjs +39 -6
  82. package/src/qc-accept.mjs +148 -6
  83. package/src/qc-check-registry.mjs +37 -1
  84. package/src/qc-results.mjs +404 -16
  85. package/src/readback.mjs +57 -13
  86. package/src/sdk-attribute-index.mjs +11 -11
  87. package/src/sdk-markup.mjs +77 -2
  88. package/src/sdk-storage-compatibility.mjs +94 -4
  89. package/src/source-html-manifest.mjs +7 -0
  90. package/src/source-html-server.mjs +129 -0
  91. package/src/spec-derive.mjs +24 -9
  92. package/src/stage-ledger.mjs +27 -1
  93. package/src/stage-record.mjs +63 -9
  94. package/src/theme-gate.mjs +2 -2
  95. package/src/visual-diff.mjs +296 -0
  96. 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.
@@ -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.2.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.59.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
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.61.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.59.0`
29
+ Supported surface at generation time: `1.61.0`
30
30
 
31
31
  ## Forward compatibility
32
32
 
@@ -37,7 +37,7 @@ The gate only applies once assembly is complete
37
37
  | `status` | `stages.polish.status` | Must start with `completed`. `blocked` → `polish.blocked`; anything else → `polish.evidence_missing`. |
38
38
  | `performed_by` | `stages.polish.performed_by`, `stages.polish.command_identity`, `evidence.performed_by` | Must be exactly `next-campaigns-polish`. Anything else (including missing) → `polish.self_certified`. |
39
39
  | `commands` | `stages.polish.commands` and/or `evidence.commands` | Must NOT mention a build command anywhere — a polish record that ran build is self-certification → `polish.self_certified`. The matcher is exactly the strings `next-campaigns-build` and `campaigns-os next build` (case-insensitive); other build tooling (e.g. page-kit's `campaign-build`) is not matched. |
40
- | `source_build_fingerprint` | `stages.polish.source_build_fingerprint` or `evidence.source_build_fingerprint` | Required, and must equal the current build fingerprint (`stages.assembly.build_fingerprint`, falling back to `stages.assembly.artifact_fingerprint`, `report.build_fingerprint`, `report.artifact_fingerprint`). Missing → `polish.source_build_fingerprint_missing`; different → `polish.stale`. Doctor, QA, and `polish capture` also recompute the fingerprint from the built output under `_site/<slug>/` (algorithm in `docs/build-packet.md`); when the output no longer matches the recorded value the gate is `polish.output_drift` with `current_output_fingerprint` and a `rerun_build` action, whatever the recorded string says. |
40
+ | `source_build_fingerprint` | `stages.polish.source_build_fingerprint` or `evidence.source_build_fingerprint` | Required, and must equal the current build fingerprint (`stages.assembly.build_fingerprint`). Missing → `polish.source_build_fingerprint_missing`; different → `polish.stale`. Doctor, QA, and `polish capture` also recompute the fingerprint from the built output under `_site/<slug>/` (algorithm in `docs/build-packet.md`); when the output no longer matches the recorded value the gate is `polish.output_drift` with `current_output_fingerprint` and a `rerun_build` action, whatever the recorded string says. |
41
41
  | `source_package_material_fingerprint` | `stages.polish.source_package_material_fingerprint` or `evidence.source_package_material_fingerprint` | Only enforced when the report carries a current Design Source Package material fingerprint (`design_source_package.material_fingerprint` or equivalents). Missing → `polish.source_package_material_fingerprint_missing`; different → `polish.source_package_stale`. |
42
42
  | `completed_at` | `stages.polish.completed_at` or `evidence.completed_at` | Required non-empty timestamp string → else `polish.completed_at_missing`. |
43
43
 
@@ -54,7 +54,7 @@ per-field problem line.
54
54
 
55
55
  | Field | Accepted shape (presence check) |
56
56
  |---|---|
57
- | `visual_review` | **Object** with a `screenshots` array (accepted aliases for the array key: `screenshot_paths`, `paths`, `urls`) containing at least one non-empty string, plus package-generated `page_load`. A bare string, an object without a screenshot array, or an empty array all fail. The gate's shape check accepts a single screenshot entry; the `next-campaigns-polish` responsibility bar is desktop **and** mobile captures of the key commerce anchors — record both. Never hand-author `page_load`. |
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,40 @@ 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
+ **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.
125
+
126
+ ### Package captures and design comparison
127
+
128
+ 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.
129
+
130
+ 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.
131
+
132
+ 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.
133
+
134
+ 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.
135
+
136
+ 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.
137
+
138
+ 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.
139
+
140
+ Compatible pairs are compared with the optional `pixelmatch` package:
141
+ - images are padded, never resized;
142
+ - width and height differences are reported;
143
+ - a heatmap is written;
144
+ - the most different areas are listed per section.
145
+
146
+ 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.
147
+
148
+ 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.
149
+
150
+ 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.
151
+
152
+ 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.
153
+
154
+ Template-stock pages are outside the mapped capture plan and produce no visual rows.
155
+
156
+ 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.
157
+
124
158
  ### Collector response records (the producer's wire form)
125
159
 
126
160
  Inside the producer, the browser adapter's CDP collector emits one
@@ -469,6 +503,8 @@ deliberately). Waived runs pass with status `waived`, never silently.
469
503
 
470
504
  ## 6. Complete passing example
471
505
 
506
+ 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.
507
+
472
508
  The hand-authored portion below is completed first. Run `campaigns-os polish
473
509
  capture` to attach the versioned `visual_review.page_load` object; that package
474
510
  artifact is intentionally not reproduced as editable JSON here.
@@ -520,6 +556,8 @@ stack).
520
556
 
521
557
  ## 7. Recording with `record polish`
522
558
 
559
+ 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.
560
+
523
561
  ```bash
524
562
  campaigns-os record polish --packet <campaign-runtime.build.json> --evidence <polish-evidence.json> [--report <json>] [--dry-run] [--json]
525
563
  ```
@@ -531,7 +569,7 @@ any other key is refused:
531
569
  | Key | Required | Written to |
532
570
  |---|---|---|
533
571
  | `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. |
572
+ | `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
573
  | `blockers` | for `blocked` only | `stages.polish.blockers`: a non-empty array of `{"code", "message"}` objects. |
536
574
  | `skip_reason` | for `skipped` only | `stages.polish.skip_reason`: a non-empty string. |
537
575
  | `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`. |
@@ -381,6 +381,14 @@ default currency. The persisted verdict redacts every URL query, so the
381
381
  currency is recorded as `evidence.currency` on the entry page's
382
382
  `http:<page_id>` assertion; an entry URL that does not parse gets no currency,
383
383
  and its row records `evidence.currency_not_applied` with the reason instead.
384
+ `--currency` sets the currency only. Test orders still ship to the address the
385
+ `--test-*` flags give, a US address by default (see
386
+ [Test customer and shipping address](#test-customer-and-shipping-address)). A
387
+ non-USD `--currency` on a run that places orders, with no `--test-country`,
388
+ prints a warning on stderr and the run goes ahead. Only the missing flag is
389
+ checked: a currency such as EUR covers many countries, so an explicit
390
+ `--test-country` is taken as meant. `qa run --site` takes `--currency` the same
391
+ way and tags the slug's entry page.
384
392
 
385
393
  ### Automatic commercial parity
386
394
 
@@ -1144,7 +1152,7 @@ What the diff asserts (BLOCKER unless noted):
1144
1152
  For a real SDK 0.4 migration example that required typed-card post-purchase
1145
1153
  traversal, persisted-order price verification, receipt-context analytics, and
1146
1154
  independent order readback, see
1147
- [SDK 0.4 Migration Proof Case Study](sdk04-migration-proof-case-study.md).
1155
+ [SDK 0.4 Migration Proof Case Study](https://github.com/NextCommerceCo/campaigns-os/blob/main/docs/sdk04-migration-proof-case-study.md).
1148
1156
 
1149
1157
  ## Parity capture (fixture-driven migration proof)
1150
1158
 
@@ -1959,6 +1967,41 @@ that died mid-submit. QA reports it on the path's `browser-test-order` row as
1959
1967
  (or `--test-email`), or wait. The shipping address is not part of the match, so
1960
1968
  changing `--test-address1` does not help.
1961
1969
 
1970
+ ### Test customer and shipping address
1971
+
1972
+ The runner fills the checkout as one test customer. Every field has a default,
1973
+ and each has a flag:
1974
+
1975
+ | Flag | Default | Field |
1976
+ |---|---|---|
1977
+ | `--test-first-name` | `QA` | first name (shipping and billing) |
1978
+ | `--test-last-name` | `Playwright` | last name |
1979
+ | `--test-phone` | empty | phone, filled only when the checkout has the field |
1980
+ | `--test-email` | see above | email |
1981
+ | `--test-country` | `US` | country select (value or label, such as `GB`) |
1982
+ | `--test-address1` | `1600 Amphitheatre Pkwy` | street address |
1983
+ | `--test-city` | `Mountain View` | city |
1984
+ | `--test-province` | `CA` | state, province or region select (value or label) |
1985
+ | `--test-postal` | `94043` | postal code |
1986
+
1987
+ The billing fields get the same values when the checkout shows them. To order
1988
+ in another market, pass `--test-country` and all four address flags for a real
1989
+ address in that country, since the address defaults are a US one, and pass
1990
+ `--currency` for that market's currency: the country flag does not change the
1991
+ currency. A UK run looks like this:
1992
+
1993
+ ```bash
1994
+ npm run campaigns-os -- qa run --packet campaign-runtime.build.json \
1995
+ --currency GBP --browser --test-order common \
1996
+ --test-country GB --test-address1 "<street>" --test-city "<city>" \
1997
+ --test-province "<option from the province select>" --test-postal "<postcode>"
1998
+ ```
1999
+
2000
+ The province select is a required step. A checkout that shows no province
2001
+ select for the chosen country fails the path at that field
2002
+ (`Missing checkout select: province`); place those orders by hand on the
2003
+ market-pinned page (`?currency=<code>&country=<code>`).
2004
+
1962
2005
  The browser driver intentionally behaves like a user:
1963
2006
 
1964
2007
  - package selection uses rendered `[data-next-package-id]` controls when
@@ -2116,10 +2159,15 @@ npm run campaigns-os -- doctor --built ../my-campaign-repo --family arjuna --emi
2116
2159
 
2117
2160
  # QA a built, served campaign with no packet/spec.
2118
2161
  npm run campaigns-os -- qa run --site ../my-campaign-repo --base-url http://localhost:8080 --family arjuna --browser
2162
+
2163
+ # The same, entering the slug's first page in GBP.
2164
+ npm run campaigns-os -- qa run --site ../my-campaign-repo --base-url http://localhost:8080 --family arjuna --slug my-slug --currency GBP
2119
2165
  ```
2120
2166
 
2121
2167
  `--family` is required (the residue gates need the family's brand contract).
2122
- `--slug` selects the campaign when `_site/` holds more than one. With no theme
2168
+ `--slug` selects the campaign when `_site/` holds more than one. `--base-url`
2169
+ can be the served site origin or the campaign root: QA adds the campaign's slug
2170
+ when the URL does not already end in it, so pages resolve under `/<slug>/`. With no theme
2123
2171
  artifacts the theme gate resolves to `not_applicable` (non-blocking), so the
2124
2172
  placeholder-text blocker and the other residue gates still run. The emitted
2125
2173
  minimal packet is marked `_synthesized` — it points doctor/QA at the built