@nextcommerce/campaigns-os 1.48.0 → 1.52.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 (80) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/agents/claude/CLAUDE.md +6 -5
  3. package/agents/codex/AGENTS.md +6 -5
  4. package/agents/copilot/copilot-instructions.md +3 -3
  5. package/agents/cursor/campaigns-os.mdc +3 -3
  6. package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
  7. package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
  8. package/campaign-spec/dist/rules/design-source-shape.js +13 -3
  9. package/campaign-spec/dist/rules/sdk-version.js +2 -1
  10. package/compatibility.json +1 -1
  11. package/contracts/commerce-surface-catalog.json +26 -46
  12. package/contracts/effects.v1.json +254 -2
  13. package/contracts/release-ledger.json +1239 -0
  14. package/contracts/supported-surface.json +4 -4
  15. package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
  16. package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
  17. package/docs/brand-theme-bridge.md +12 -6
  18. package/docs/build-packet.md +101 -12
  19. package/docs/campaign-build-brief.md +25 -1
  20. package/docs/effects.md +6 -0
  21. package/docs/local-setup.md +1 -1
  22. package/docs/orientation-contract-reference.md +1 -1
  23. package/docs/polish-evidence.md +10 -0
  24. package/docs/qa-and-test-orders.md +66 -11
  25. package/docs/runtime-readiness.md +1 -1
  26. package/docs/sdk-storage-compatibility.md +1 -1
  27. package/docs/skills-revision.md +10 -10
  28. package/package.json +1 -1
  29. package/schemas/campaign-runtime-build-packet.v0.schema.json +4 -0
  30. package/schemas/campaigns-os-qa-verdict.v0.schema.json +8 -3
  31. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  32. package/skills/campaign-readback-classification/SKILL.md +3 -3
  33. package/skills/campaign-run-evidence/SKILL.md +3 -3
  34. package/skills/contribution-intake/SKILL.md +3 -3
  35. package/skills/next-campaigns-build/SKILL.md +4 -4
  36. package/skills/next-campaigns-os/SKILL.md +4 -4
  37. package/skills/next-campaigns-os/references/session-intake.md +7 -3
  38. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  39. package/skills/next-campaigns-polish/SKILL.md +5 -4
  40. package/skills/next-campaigns-qa/SKILL.md +6 -5
  41. package/skills.json +10 -10
  42. package/src/adapter-decision-contract.mjs +1 -1
  43. package/src/brand-theme.mjs +25 -2
  44. package/src/build-brief.mjs +68 -21
  45. package/src/built-site-scope.mjs +39 -6
  46. package/src/built-smoke-qc.mjs +1117 -0
  47. package/src/campaign-identity.mjs +36 -2
  48. package/src/cart-placeholders.mjs +730 -0
  49. package/src/cli.mjs +320 -42
  50. package/src/commercial-journey.mjs +65 -4
  51. package/src/commercial-parity.mjs +6 -1
  52. package/src/doctor/checks.mjs +291 -24
  53. package/src/doctor/inspect.mjs +53 -2
  54. package/src/doctor/next-step.mjs +1 -1
  55. package/src/install-mode.mjs +0 -8
  56. package/src/invocation.mjs +5 -2
  57. package/src/local-preview-policy.mjs +1 -1
  58. package/src/local-proof.mjs +4 -1
  59. package/src/polish-browser.mjs +218 -1
  60. package/src/polish-capture.mjs +1 -1
  61. package/src/polish-media-weight.mjs +492 -0
  62. package/src/polish-node.mjs +96 -4
  63. package/src/progress-node.mjs +5 -1
  64. package/src/qa-binding-evidence.mjs +21 -0
  65. package/src/qa-browser.mjs +338 -97
  66. package/src/qa-content-params.mjs +889 -0
  67. package/src/qa-node.mjs +114 -14
  68. package/src/qa-order-bump.mjs +22 -1
  69. package/src/qa-policy-links.mjs +1019 -0
  70. package/src/qa-tracking-params.mjs +1389 -0
  71. package/src/qa-url-privacy.mjs +168 -0
  72. package/src/qc-accept.mjs +446 -0
  73. package/src/qc-check-registry.mjs +83 -0
  74. package/src/qc-results.mjs +1049 -0
  75. package/src/sdk-attribute-index.mjs +71 -0
  76. package/src/sdk-markup.mjs +2 -2
  77. package/src/sdk-storage-compatibility.mjs +63 -3
  78. package/src/source-prep.mjs +37 -7
  79. package/src/stage-record.mjs +356 -36
  80. package/src/theme-gate.mjs +3 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act — hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here — src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) — is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
3
- "surface_version": "1.48.0",
3
+ "surface_version": "1.52.0",
4
4
  "package_exports": [
5
5
  "./commercial-journey",
6
6
  "./commercial-parity",
@@ -66,7 +66,7 @@
66
66
  "sha256": "f310c1b7844d9b1994864d15858ff0c6f17efc1e5285ba193d181bdb2dd4be2f"
67
67
  },
68
68
  "schemas/campaign-runtime-build-packet.v0.schema.json": {
69
- "sha256": "47c2899f409ed5927cf410bddd63e7bb8a9d154eabf7a07455777aed118212e9"
69
+ "sha256": "5002529c5979c4d8fef7e298de82ebdf625ffbd11f5284ba684d96d173788d62"
70
70
  },
71
71
  "schemas/campaign-spec.v4.schema.json": {
72
72
  "sha256": "af56c3de638d3c4e1fd5df8c2d733754d9ad16caa23b5bec92ce3a6f71bd0271"
@@ -84,7 +84,7 @@
84
84
  "sha256": "3c40fd79f218d1b95f0424f9a2b67db140a306329e2ca02793e2740d3afded59"
85
85
  },
86
86
  "schemas/campaigns-os-qa-verdict.v0.schema.json": {
87
- "sha256": "ec4514e81a791a63bc0b358d7977aa823f3c5524e0f5caaba2712e66f262e18a"
87
+ "sha256": "8c689d29850b2b894c17438ee4411363033bae6f805820af53b7a04a5693d0e0"
88
88
  },
89
89
  "schemas/campaigns-os-doctor-output.v0.schema.json": {
90
90
  "sha256": "87866e619d2a18f01a208a1682cb14a2079f61df51c642f4e1902b6e6bce0ce5"
@@ -120,7 +120,7 @@
120
120
  "sha256": "dfc9abed38d456969e47a21f606d308a03bdf036f3466f7d6e47a602747dacf4"
121
121
  },
122
122
  "contracts/effects.v1.json": {
123
- "sha256": "5f8c6f2f88fc9e0c11c09e265640071d3c1d81fd96c6e05283ed5cfed21d1d59"
123
+ "sha256": "f49ac6dcbf441c90b12b51be9bc07805f6228f4143784045c5925e3c155a6181"
124
124
  },
125
125
  "schemas/campaigns-os-effects.v1.schema.json": {
126
126
  "sha256": "3eadd22169ab98bc7c2f682af2751267605170581182158d96be035b0cfe44dc"
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "asset_pin": {
57
57
  "repo": "NextCommerceCo/campaign-cart-starter-templates",
58
- "sha": "37a8d945db4c360739ded98348814cfd632da94f",
58
+ "sha": "2c618942391ae398fb4812f36fc93e6632054874",
59
59
  "path": "src/<family>/assets/<asset>",
60
60
  "note": "asset_sha256 is the sha256 of each shipped starter asset at this commit (the commerce-surface-catalog _synced_from_sha pin; the bytes are identical across every family). A served asset whose bytes hash to the shipped value is the untouched starter asset and counts as residue even though its markup names no method; different bytes mean it was edited in place. check-template-doctrine verifies these hashes against the pinned checkout."
61
61
  },
@@ -75,7 +75,7 @@
75
75
  ],
76
76
  "placeholder_text_residue": {
77
77
  "page_types": ["checkout", "select", "upsell", "downsell", "receipt", "presell", "landing"],
78
- "terms": ["Lorem", "lorem ipsum", "Placeholder", "TODO", "Product Name"],
78
+ "terms": ["Lorem", "lorem ipsum", "Placeholder", "TODO", "Product Name", "Benefit one", "Benefit two", "Benefit three", "Benefit four"],
79
79
  "rule": "Rendered campaign output must not contain literal template placeholder copy. Every term is replaced from the CampaignSpec/design before QA; QA fails (blocker) on any match in visible page text, regardless of any theme-gate waiver. Matched on word boundaries, case-insensitive. Scope is governed by page_types: the blocker applies only to the listed campaign funnel page types (generic, non-funnel pages whose type is not listed are not checked). Narrow page_types to exempt a page type where placeholder copy is acceptable."
80
80
  },
81
81
  "computed_style_checks": [
@@ -1394,6 +1394,14 @@
1394
1394
  },
1395
1395
  "demo_value": "Our formula works with your body's own sleep systems \u2014 not against them. Our synergistic blend lower"
1396
1396
  },
1397
+ {
1398
+ "key": "benefits_2_section_id",
1399
+ "kind": "meta",
1400
+ "role": "page_meta",
1401
+ "owner": "template",
1402
+ "description": "Section anchor id, the target of the header nav's in-page link. Template-owned, assembly never touches it: the nav href and this id must stay paired.",
1403
+ "source_policy": "template_static"
1404
+ },
1397
1405
  {
1398
1406
  "key": "benefits_2_section_heading",
1399
1407
  "kind": "copy",
@@ -1709,6 +1717,14 @@
1709
1717
  },
1710
1718
  "demo_value": "The compounding effect reaches its peak. Consistent deep sleep every night, sustained daytime energy"
1711
1719
  },
1720
+ {
1721
+ "key": "ingredients_3_section_id",
1722
+ "kind": "meta",
1723
+ "role": "page_meta",
1724
+ "owner": "template",
1725
+ "description": "Section anchor id, the target of the header nav's in-page link. Template-owned, assembly never touches it: the nav href and this id must stay paired.",
1726
+ "source_policy": "template_static"
1727
+ },
1712
1728
  {
1713
1729
  "key": "ingredients_3_heading",
1714
1730
  "kind": "copy",
@@ -2531,6 +2547,14 @@
2531
2547
  "proof_modality": "review",
2532
2548
  "needs_merchant_input_able": true
2533
2549
  },
2550
+ {
2551
+ "key": "reviews_3_section_id",
2552
+ "kind": "meta",
2553
+ "role": "page_meta",
2554
+ "owner": "template",
2555
+ "description": "Section anchor id, the target of the header nav's in-page link. Template-owned, assembly never touches it: the nav href and this id must stay paired.",
2556
+ "source_policy": "template_static"
2557
+ },
2534
2558
  {
2535
2559
  "key": "reviews_3_badge_text",
2536
2560
  "kind": "copy",
@@ -151,8 +151,9 @@ When `theme inspect` reports `can_generate: true` and the campaign ships
151
151
  commerce pages (checkout/upsell/downsell/receipt), the gate **blocks**
152
152
  `next polish`, `next deploy`, `next qa`, and `qa run` until one of:
153
153
 
154
- - the brand layer is generated and recorded as applied
155
- (`report.theme.status: applied`, `load_order: after-next-core`), or
154
+ - the brand layer is generated, linked and recorded as applied with
155
+ `campaigns-os record theme --packet <p>` (`report.theme.status: applied`,
156
+ `load_order: after-next-core`), or
156
157
  - an explicit waiver is recorded:
157
158
  `campaigns-os theme waive --packet <p> --reason "<why>" --waived-by "<named human>"`
158
159
  (optionally `--expires-at <ISO>`; placeholders such as "operator" are refused), or
@@ -197,8 +198,12 @@ If a fresh `brand-theme.css` exists:
197
198
  3. Preserve SDK-owned runtime surfaces: `data-next-*`, package selectors,
198
199
  payment fields, totals, submit controls, receipt templates, route meta tags,
199
200
  and SDK JavaScript.
200
- 4. Record `report.theme.status`, `css_path`, `commerce_pages`, `load_order`,
201
- and evidence.
201
+ 4. Rebuild, run `campaigns-os record build --packet <p>`, then
202
+ `campaigns-os record theme --packet <p>`. It reads each built commerce
203
+ page's stylesheet links and records `report.theme.status`, `css_path`,
204
+ `commerce_pages`, `load_order` and evidence. It refuses, writing nothing,
205
+ when a page that loads `next-core.css` does not load the brand layer after
206
+ it, or when no built commerce page loads `next-core.css`.
202
207
 
203
208
  ### Where next-core.css belongs
204
209
 
@@ -212,8 +217,9 @@ stylesheets off those pages and list them only in the frontmatter styles of
212
217
  the pages that render family components, often just checkout.
213
218
 
214
219
  `report.theme.commerce_pages` is the list of pages where the brand layer was
215
- applied, recorded by the build. Record the pages you actually scoped, for
216
- example `commerce_pages: ["checkout"]`. The theme gate does not compare this
220
+ applied. `record theme` writes it from the built pages: every commerce page
221
+ that loads `next-core.css`, for example `commerce_pages: ["checkout"]`. A page
222
+ that loads neither stylesheet is left out and noted in the evidence. The theme gate does not compare this
217
223
  list with the funnel. It passes on `report.theme.status: applied` with
218
224
  `load_order: after-next-core`. The gate's own `commerce_pages` output is a
219
225
  different field: every checkout, upsell, downsell, receipt or thank-you page
@@ -22,8 +22,9 @@ ordinary CampaignSpec from the brief, source design and configured campaign's
22
22
  real commerce values, following `schemas/campaign-spec.v4.schema.json`. The
23
23
  operator supplies the selected store/campaign, public Campaigns API key, intended
24
24
  pages and commercial choices, plus store contact details and policy URLs. Verify
25
- the store/campaign binding and package/offer references; do not guess commerce
26
- values. No gateway or Map provisioning is required for this entry.
25
+ the store/campaign binding, and read the package, offer and shipping references
26
+ from the campaign itself (below); do not guess commerce values. No gateway or
27
+ Map provisioning is required for this entry.
27
28
 
28
29
  Set `spec_identity.local_spec_id` to a new UUID once, commit it with the spec,
29
30
  and keep it unchanged through revisions and fresh checkouts. It accepts 1–64
@@ -45,7 +46,9 @@ npx --no-install campaigns-os next --packet campaign-runtime.build.json
45
46
  The packet and report retain `map_id: null` and carry `local_spec_id`. Doctor,
46
47
  report writes, polish capture, progress, run closeout and QA compare that local
47
48
  identity. Material spec hashes still bind the current revision; a changed ID or
48
- content cannot reuse earlier proof. After a material revision, follow `next` to
49
+ content cannot reuse earlier proof. After a material revision, doctor and
50
+ `next` warn `spec.material_stale` (the spec no longer has the material hash
51
+ prepare-build bound on the Assembly Report, which QA refuses); follow `next` to
49
52
  refresh preparation and affected evidence. Keep the spec, source, dependency
50
53
  pins and canonical sidecars in Git. Use `readback` and `next` after a fresh
51
54
  checkout; identity survives the move, but proof freshness is assessed again.
@@ -65,6 +68,56 @@ not relax template certification, source proof, store/SDK parity, polish,
65
68
  commerce checks, or typed-card checkout proof. Resolve their reported gates;
66
69
  localhost readiness is not production approval.
67
70
 
71
+ ### Reading package, offer and shipping refs
72
+
73
+ No command writes commerce refs into a local spec, and `login` does not read
74
+ them: a gateway login serves only the Store Profile fields that `spec derive
75
+ --from-store` fills. Read them with the campaign's public Campaigns API key,
76
+ through the request doctor and QA already make to check built pages against
77
+ the live campaign: one GET of NEXT's proxy with the key in the
78
+ `X-Campaign-Key` header. No store or Admin credential is involved.
79
+
80
+ ```sh
81
+ CAMPAIGN_KEY='<public key>' node -e '
82
+ fetch("https://campaign-map.nextcommerce.com/api/campaign", {
83
+ headers: { Accept: "application/json", "X-Campaign-Key": process.env.CAMPAIGN_KEY },
84
+ }).then(async (res) => console.log(res.status, JSON.stringify(await res.json(), null, 2)));
85
+ '
86
+ ```
87
+
88
+ `curl -sS -H "Accept: application/json" -H "X-Campaign-Key: <public key>" https://campaign-map.nextcommerce.com/api/campaign`
89
+ returns the same. Add `?ref_id=<campaign id>` when one key serves several
90
+ campaigns. Other HTTP clients work with the same header, but the proxy refuses
91
+ some default user agents, Python `urllib`'s and Perl `libwww-perl`'s among
92
+ them, with a 403 whose body is `error code: 1010`; send another `User-Agent`
93
+ or use one of the commands above.
94
+
95
+ The answer is an envelope, `{ ok, status, endpoint, requested_ref_id,
96
+ retrieved_at, data }`, with `requested_ref_id` present only when `?ref_id=`
97
+ was sent; `ok: false` carries an `error` instead of a campaign. `data` is the
98
+ campaign retrieve body the Campaign Cart SDK reads in the browser: one
99
+ campaign, or an array of them, in which case use the entry whose `id` is the
100
+ selected campaign. Copy from it as follows:
101
+
102
+ | `data` field | CampaignSpec field |
103
+ |---|---|
104
+ | `id` | `campaign.ref_id` (doctor and QA send it as `?ref_id=`) |
105
+ | `name`, `currency`, `language`, `payment_env_key` | the `campaign` fields of the same name |
106
+ | `packages[]`: `ref_id`, `name`, `qty`, `price`, `price_retail`, `image`, the `product_*` fields and the recurring fields (`is_recurring`, `price_recurring`, `interval`, `interval_count`) | one `funnels[].pages[].packages[]` entry for each package the page sells |
107
+ | `offers[]`: `ref_id`, `name`, `type`, `code`, `condition`, `benefit`, `packages[]` (by `package_id`), `shipping_methods[]` | root `offers[]`, copied whole; a page that presents an offer lists it in `funnels[].pages[].offers[]` by `ref_id` |
108
+ | `shipping_methods[]`: `ref_id`, `code`, `price` | root `shipping_methods[]` |
109
+
110
+ A package's ref is its `ref_id`, numbered within the campaign. `external_id`,
111
+ `product_id` and `product_variant_id` are catalog ids, never package refs. The
112
+ same `ref_id` values are what pages render in `data-next-package-id` and
113
+ `data-next-shipping-id`. The read supplies refs and the values the campaign
114
+ serves, not the selection: which packages and offers each page carries, and
115
+ the role flags `is_upsell`, `is_order_bump` and `default_selected`, come from
116
+ the brief and the operator. Copy prices and availability from the read rather
117
+ than typing them from the brief. After the build, doctor and QA repeat this
118
+ read and block a page ref the live campaign does not serve (see
119
+ [the live campaign read](effects.md#the-live-campaign-read)).
120
+
68
121
  ## Root-Served Campaigns (`campaign.route_root`)
69
122
 
70
123
  Most campaigns are served under a slug prefix (`/<public_route_slug>/...`), and
@@ -732,7 +785,14 @@ intended) when:
732
785
  (`…attribution_drift`).
733
786
 
734
787
  One error per finding; each names the two files and the two values, so the
735
- repair is a one-line edit. Pages whose route contains a `-backup-` or `-old-`
788
+ repair is a one-line edit. Every built HTML file under `_site/<slug>/` is
789
+ scanned, because every one is served. When doctor has the CampaignSpec, a
790
+ finding that names a file no active spec page builds to says so and lists it
791
+ under `stray_files`. Such a file is leftover output (page-kit does not prune
792
+ `_site/`) or an HTML file copied into the source, such as a design export's
793
+ `index.html` under `assets/`. The repair is to remove the source file if
794
+ there is one, delete the built file, and rebuild and record the build again,
795
+ not to retag it. Pages whose route contains a `-backup-` or `-old-`
736
796
  segment are parked copies: skipped and listed on the gate as `pages_skipped`,
737
797
  never scanned. Presence is not asserted: a campaign whose pages carry no key
738
798
  at all, or no `setAttribution` anywhere, passes on the funnel tag alone.
@@ -893,6 +953,24 @@ canonical rendered output of every certified starter family
893
953
  it reads for credential declarations (`script-parse:<page_id>`; see
894
954
  [QA and test orders](qa-and-test-orders.md)).
895
955
 
956
+ ### Built-output cart placeholder check (`built_output.cart_placeholders`)
957
+
958
+ **Raw cart placeholders (`built_output.cart_placeholders`).** Doctor warns when a known SDK placeholder, such as `{item.name}`, `{subtotal}` or `{package.name}`, appears in live built HTML text or in a text attribute (`alt`, `title`, `placeholder`, `aria-label`, button `value`), where it prints as raw text. Placeholders inside `<template>`, inside `data-next-cart-items`/`data-next-order-items` rows and their declared row templates, and SDK-substituted quantity text are expected. Brace strings that are not known SDK placeholders, including `{tax}`, are review results. The placeholder list is vendored from the SDK version pinned in `src/sdk-attribute-index.mjs`. Pages whose SDK loader does not name an exact version, or names a version the list was not verified against, are reported as unexercised rather than passing. These are warnings, never blockers.
959
+
960
+ ### Built-output smoke checks (`built_output.smoke_qc`)
961
+
962
+ **Smoke checks (`built_output.smoke_qc`).** Doctor warns on built pages that have:
963
+
964
+ - an in-page `#link` with no matching `id` or `name` on the same page;
965
+ - no favicon link;
966
+ - no `og:title`, `og:description` or `og:image`;
967
+ - an `og:image` that is relative or points to a missing file;
968
+ - the Tailwind CDN script in a production build;
969
+ - references to the `cdn.29next.store` asset host in any attribute or `<style>` block (use `cdn.cachebucket.com`);
970
+ - `localhost` or loopback URLs in a production build.
971
+
972
+ Targets inside `<template>` or named in page scripts are review results, and pages whose scripts could not all be read are unexercised. Remote `og:image` URLs are not fetched by doctor. The Tailwind and loopback checks need a recorded production build: development builds (including local proof) and `doctor --built`, which cannot tell the environment, report them as unexercised. All are warnings, never blockers.
973
+
896
974
  > **Where does the source HTML come from?** See [docs/entry-points.md](./entry-points.md) for the five recognized entry points (template-stock, Figma-driven, AI-generated, hand-authored, mixed) and how each populates `source_html.pages[]` + `design_source`.
897
975
 
898
976
  ## Artifact Locations
@@ -970,8 +1048,9 @@ starter palette is acceptable>" --waived-by "<named human>"`, optionally
970
1048
  (status and severity — never `fail`) and keeps the shipped palette visible in
971
1049
  the verdict; or hand-author the brand
972
1050
  layer — write `brand-theme.css`, list it after `next-core.css` in commerce-page
973
- frontmatter styles, rebuild, and record `report.theme.status: applied` with
974
- `load_order: after-next-core`. Nothing waives the gate on the operator's
1051
+ frontmatter styles, rebuild, `record build`, and `record theme`, which records
1052
+ `report.theme.status: applied` with `load_order: after-next-core` from the
1053
+ built pages. Nothing waives the gate on the operator's
975
1054
  behalf. See [Brand Theme Bridge](./brand-theme-bridge.md) for both lanes in
976
1055
  full.
977
1056
 
@@ -1237,7 +1316,9 @@ loopback host (`127.0.0.1`, `[::1]`) with a ready line naming the
1237
1316
  `http://localhost:<port>/` fallback, and warns (`deploy.local_serve_url`) when
1238
1317
  the recorded URL is neither.
1239
1318
  `next` at the deploy stage then hands off a serve-locally prompt and action
1240
- instead of a ship-to-host one. The directory to serve is `_site/`; for a
1319
+ instead of a ship-to-host one, and the served URL is recorded with
1320
+ `campaigns-os record deploy --packet <p> --base-url <url>` (the record command
1321
+ table below). The directory to serve is `_site/`; for a
1241
1322
  root-served campaign (`campaign.route_root: "/"`) the handoff adds that pages
1242
1323
  are served at site-root paths while assets keep the `/<public_route_slug>/`
1243
1324
  prefix, so `_site/` needs the same rewrite of root-level page routes onto
@@ -1248,9 +1329,10 @@ URL.
1248
1329
  `local-serve` also selects **local proof mode** for the build stage: page-kit
1249
1330
  is built in the development environment (`CPK_ENV=development npx
1250
1331
  campaign-build --json > .campaign-runtime/page-kit-build-summary.json`) into
1251
- `_site/`, and the build records `stages.assembly.evidence.build_environment:
1332
+ `_site/`, and `campaigns-os record build --packet <packet> --build-environment
1333
+ development` records `stages.assembly.evidence.build_environment:
1252
1334
  "development"` on the Assembly Report (a free-form stage field; no schema
1253
- change). The starter templates gate every vendor loader on the environment,
1335
+ change; never hand-edited). The starter templates gate every vendor loader on the environment,
1254
1336
  and a production build's protocol-relative loaders (`//host/...`) fail over a
1255
1337
  plain-HTTP local serve, voiding polish capture unwaivably; the SDK's `dl_*`
1256
1338
  events still fire in development. Before commit, `campaigns-os page-kit parity
@@ -1607,12 +1689,17 @@ hand-editing `.campaign-runtime/` JSON:
1607
1689
  | Command | Writes | Refused (nothing written) when |
1608
1690
  |---|---|---|
1609
1691
  | `campaigns-os record setup --packet <p>` | Build Context `scaffold.required=false` (`handoff_skill` next-campaigns-build) and `stages.setup` completed | the campaign output directory (`assembly.output_dir`) does not exist, or there is no Build Context or Assembly Report |
1610
- | `campaigns-os record build --packet <p>` | `stages.assembly` completed with `build_fingerprint` = doctor's `derived.build_output_fingerprint.value`, `source_package_material_fingerprint` = the report's Design Source Package material fingerprint when present, and `stages.polish` reset to `required` (`required_by` build, `required_for` qa) unless its evidence is bound to this exact output | doctor cannot compute the fingerprint (no `_site/<public_route_slug>/`), setup is still required, or `stages.setup` is not terminal |
1692
+ | `campaigns-os record build --packet <p> [--build-environment <development\|production>]` | `stages.assembly` completed with `build_fingerprint` = doctor's `derived.build_output_fingerprint.value`, `source_package_material_fingerprint` = the report's Design Source Package material fingerprint when present, `evidence.build_environment` = the `--build-environment` value when given (kept from the last record otherwise), and `stages.polish` reset to `required` (`required_by` build, `required_for` qa) unless its evidence is bound to this exact output | doctor cannot compute the fingerprint (no `_site/<public_route_slug>/`), setup is still required, or `stages.setup` is not terminal |
1611
1693
  | `campaigns-os record polish --packet <p> --evidence <file>` | `stages.polish` from the file (`docs/polish-evidence.md` §7: completed, blocked or skipped), bound to doctor's current fingerprint; `report.theme.repair_loop_defect` when the file sets it | build is not recorded for the current output, the file has a shape error (named by field), or, for a completed status, the polish gate doctor evaluates would not pass on the result |
1694
+ | `campaigns-os record theme --packet <p>` | `report.theme`: status `applied`, `load_order` `after-next-core`, `css_path`, `commerce_pages` and per-page evidence read from each built commerce page's stylesheet links; any earlier theme waiver is cleared | build is not recorded for the current output, the campaign ships no commerce pages, a built commerce page that loads `next-core.css` does not load `brand-theme.css` (or `checkout-brand.css`) after it or links one missing from the built output, or no built commerce page loads `next-core.css` |
1695
+ | `campaigns-os record deploy --packet <p> --base-url <url>` | the packet's `deploy.preview_url` and `stages.deploy` completed with the URL in `outputs` and one evidence line per built page that answered (each page is requested under the URL first) | the packet is not `local-serve`, the URL is not a loopback origin naming the campaign's route root, a built page does not answer 2xx, polish is not recorded, the built output changed since build was recorded, or the theme gate is blocked |
1612
1696
 
1613
1697
  Each command also refuses a stage `next` has not reached: while doctor's
1614
1698
  prepare-build gate is set (`next` answers prepare-build) or while an earlier
1615
- stage in the order below is not terminal.
1699
+ stage in the order below is not terminal. The exception is the one `next`
1700
+ makes: on the local preview, a polish doctor carries forward (never recorded
1701
+ for this build) does not hold `record deploy` back, as it does not hold `next`;
1702
+ polish stays owed and QA reports it.
1616
1703
 
1617
1704
  Each command reads the same packet, Build Context and Assembly Report `next`
1618
1705
  reads (`--context` / `--report` override them the same way), validates what it
@@ -1634,7 +1721,9 @@ whenever the report's campaign identity does not match the packet, including
1634
1721
  for packets with no Design Source Package. Every command also adds
1635
1722
  `recorded_by`, and `completed_at` unless it records a blocked Polish, to the
1636
1723
  stage it records. `--dry-run` runs every check, takes no lock and writes nothing. A failed
1637
- check exits non-zero with the problems listed, one per line. Re-run `record
1724
+ check exits non-zero with the problems listed, one per line; a value outside a
1725
+ schema enum (for example an `adapter_decisions` policy) is listed with the
1726
+ values the schema allows and the value it got. Re-run `record
1638
1727
  build` after every page-kit build; a rebuild that changes the output needs
1639
1728
  `polish capture` and `record polish` again.
1640
1729
 
@@ -47,13 +47,37 @@ Guided questions are intentionally short and business-readable. They prioritize:
47
47
  2. Which palette/CTA style should commerce pages use?
48
48
  3. Which product variants/colors are actually sold?
49
49
  4. How should bundle pricing be presented?
50
- 5. What promo/savings/urgency language is approved?
50
+ 5. Should the starter template's own promo placeholders (demo countdown timers, promo banners, placeholder voucher codes, exit-pop offers) be filled from the campaign's promo codes and offers, or removed?
51
51
  6. Which payment methods/trust badges may appear?
52
52
  7. Are runtime/catalog names allowed to override provided display names?
53
53
  8. Are there regulated claims or forbidden copy areas?
54
54
 
55
55
  The CLI avoids SDK/page-kit jargon in questions. The implementation can resolve SDK attributes, responsive CSS, asset paths, routing, template copying, and QA reruns. Business choices should come from the brief or be escalated.
56
56
 
57
+ Question 5 is asked only when the CampaignSpec maps a surface that fills the template's promo placeholders: a `funnels[].promo_codes` roster, or a checkout page's enabled `exit_intent` or `promo_code_input`. It never asks for approval of the source design's own promo, proof or urgency copy, which is built as designed (see below). Without such a surface the guided draft sets `header_claim_source` and `timer_label` to `"none"`: the template's promo placeholders are removed.
58
+
59
+ ## Answering The Questions
60
+
61
+ An answer counts only once it is in a brief file that `start` or `prepare-build` reads. The guided draft is regenerated on every run, so an answer given in conversation, or typed into the normalized draft, does not stick.
62
+
63
+ 1. Copy `.campaign-runtime/input/campaign-build-brief.normalized.json` to `campaign-build-brief.json` in the target repo. A brief file replaces the guided draft whole, so starting from the copy keeps the fields the draft already filled; any question the file leaves open blocks as a prepared brief.
64
+ 2. Set the fields that close each open question:
65
+
66
+ | Question | Fields |
67
+ |---|---|
68
+ | `page_design_authority` | `design_authority.<page_id>.source` for each page named in the question |
69
+ | `brand_palette_cta` | `brand.commerce_palette_source`, `brand.cta_style` |
70
+ | `variant_media_rules` | `media.sold_variants`, `media.allow_other_variant_colors` |
71
+ | `bundle_pricing_presentation` | `offer_presentation.bundle_cards.primary_price` |
72
+ | `promo_urgency_copy` | `promo_urgency.header_claim_source` (`campaign_offers` or `none`), `promo_urgency.timer_label` (the template timer's label, or `none`) |
73
+ | `payment_methods_trust` | `commerce_surfaces.payment_methods_allowed` |
74
+ | `canonical_display_names` | `canonical_display.product_name_source` |
75
+ | `regulated_claims` | one of `campaign_intent.compliance.approved_benefit_language`, `.forbidden_claims`, `.approved_claims`, `.copy_rules` |
76
+
77
+ 3. Re-run `start` or `prepare-build` with the same arguments. The file is found in the target repo automatically, or pass `--brief <file>`. Doctor's `build_brief.guided_questions` warning names the open questions and their fields.
78
+
79
+ Re-running regenerates the Assembly Report. Before any stage has recorded evidence, that costs nothing. Once a stage has recorded evidence (setup, build, polish or later), the re-run is refused unless you pass `--force`, which resets those stages and clears their evidence. Answer the questions right after `start`.
80
+
57
81
  ## Risky Defaults
58
82
 
59
83
  Doctor blocks or asks when a prepared brief leaves high-impact questions unanswered, forbids alternate variant colors without naming sold variants, or contains direct contradictions such as the same payment method being both allowed and hidden.
package/docs/effects.md CHANGED
@@ -123,6 +123,10 @@ and `build` (the intake alias for prepare-build + doctor), `theme waive`,
123
123
  sidecar refresh `qa run` makes after it records the QA stage (the verdict
124
124
  itself carries QA's own read).
125
125
 
126
+ ### `checkpoint accept`
127
+
128
+ **Accepting a warning.** `campaigns-os checkpoint accept` records an operator's decision about a warning that a Campaigns OS check measured. It does not change readiness, does not remove the warning, does not change the status `next` reports, and cannot be used on review, unexercised, or excluded results. The warning must already be on record (in the doctor snapshot `next` writes, the Polish capture, or the full QA verdict) before it can be accepted. The accept is bound to the exact measured state and lapses when that state changes, or when its evidence is stale or cannot be reproduced. An optional expiry or review condition adds a second bound. Run it only with the operator's explicit decision, made after the warning was shown to them. A plausible name is not authorization. The command refuses placeholder and automation identities. Results are re-derived from the package's raw captures on every read, and a result whose capture is missing or does not match is listed as unexercised. These checks are tamper evidence, not proof of authorship: a hand-written record that copies every value and recomputes the checksum, or a hand-written result written together with a matching capture, is not detected.
129
+
126
130
  ## How to read a row
127
131
 
128
132
  ```jsonc
@@ -316,6 +320,8 @@ in for the destination — that the **declared destination is the one contacted*
316
320
 
317
321
  | Row | What the offline fixture cannot reach |
318
322
  | --- | --- |
323
+ | `checkpoint accept` | An accept-eligible warning: no check this version ships produces one yet. Proved: the refused accept writes nothing — no report, no stale stamp, no journal entry — and contacts nothing. |
324
+ | `checkpoint accept --dry-run` | An accept-eligible warning, as for `checkpoint accept`. Proved: the refused dry run writes nothing and contacts nothing. |
319
325
  | `login` | A reachable login gateway and a human at a browser. Proved: the failure path writes nothing at all — no credential, no journal entry. |
320
326
  | `logout` | A credential minted by a gateway login. Proved: the no-credential path writes nothing. |
321
327
  | `page-kit parity` | A `local-serve` deploy target and a page-kit renderer to build the two renders with. Proved: the refusal writes nothing but the journal entry. |
@@ -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.48.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
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.52.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.48.0`
29
+ Supported surface at generation time: `1.52.0`
30
30
 
31
31
  ## Forward compatibility
32
32
 
@@ -109,6 +109,16 @@ Run it only after `npm run qa:install-browser` in an environment that permits a
109
109
  loopback HTTP listener and Chromium. It is deliberately opt-in and is not part
110
110
  of `npm run check` or CI.
111
111
 
112
+ **Media weight and origin.** Polish capture records, for each captured route and viewport:
113
+
114
+ - which resources came from the page's own origin after redirects, with the redirect chain;
115
+ - their measured transfer bytes, which include response headers;
116
+ - for each image, its natural size, rendered size, `object-fit`, and the pixel ratio of the capture.
117
+
118
+ It warns when a video, or an image over 500,000 transfer bytes, comes from the page's own origin. It also warns when an image has at least twice the pixels needed at its rendered size and the captured pixel ratio (currently 1), for images of at least 250,000 natural pixels; `object-fit: cover` cropping is allowed for. Captures with cached or unmeasured transfers, partial transfers that cannot decide the result, image probes that were cut short, and never-loaded media are unexercised, never small. Another origin is reported as another origin: this check does not decide whether it is a CDN or what hosting costs. Pages Polish does not capture are listed as unexercised. The image probe runs after network observation ends and adds no requests. The record is checked against the page-load capture it came from on every read.
119
+
120
+ The size check covers `<img>` elements in the page, including those inside open and closed shadow roots; images inside an iframe, `<input type=image>`, `<object>`, SVG `<image>` and CSS background images are not checked for size, and only their transfer bytes are weighed.
121
+
112
122
  ### Collector response records (the producer's wire form)
113
123
 
114
124
  Inside the producer, the browser adapter's CDP collector emits one
@@ -39,8 +39,10 @@ the toolkit runs that loop in a fixed order:
39
39
  1. **Build in development.** The build stage runs page-kit in the development
40
40
  environment — `CPK_ENV=development npx campaign-build --json >
41
41
  .campaign-runtime/page-kit-build-summary.json` — into the target's normal
42
- `_site/`, and records `stages.assembly.evidence.build_environment:
43
- "development"` on the Assembly Report. `next build` names the command as
42
+ `_site/`, and `campaigns-os record build --packet <packet>
43
+ --build-environment development` records
44
+ `stages.assembly.evidence.build_environment: "development"` on the Assembly
45
+ Report. `next build` names the command as
44
46
  the `build_local_proof` action and in the build prompt; doctor warns
45
47
  (`local_proof.build_environment`) on a completed build that is not recorded
46
48
  as a development render. The starter templates gate every vendor loader on
@@ -55,9 +57,10 @@ the toolkit runs that loop in a fixed order:
55
57
  with it locally (the parity step renders it to a temp dir, and the deploy
56
58
  host renders it from the committed source).
57
59
  2. **Serve and prove.** Serve `_site/` on localhost (the `next deploy` handoff
58
- names the directory and any root-route rewrite), record the URL on
59
- `deploy.preview_url`, then run `polish capture`, `qa run --browser`, and the
60
- typed-card order paths against it.
60
+ names the directory and any root-route rewrite) and run `polish capture`
61
+ against it. Once polish is recorded, `record deploy --packet <p> --base-url
62
+ <url>` records the URL on `deploy.preview_url` and the deploy stage; then run
63
+ `qa run --browser` and the typed-card order paths against it.
61
64
  3. **Prove the pin on the production output.** Before committing, run
62
65
 
63
66
  ```bash
@@ -367,7 +370,11 @@ Only contract-governed claims are compared, and only against `Exact` normalized
367
370
  truth. Proven differences emit warn-severity `pricing` assertions named
368
371
  `price-claim-mismatch`, `cadence-disclosure-mismatch`, or
369
372
  `voucher-not-applied`. Decorative, ambiguous, stale, unresolved, or malformed
370
- claims remain silent. The verdict's top-level `commercial` section records
373
+ claims remain silent. A page showing a voucher the normalized plan cannot price
374
+ (no calculated pair for its code, for example a live voucher on an upsell)
375
+ leaves that page's price claims unresolved: the plan's only Exact truth for
376
+ the page is then the list price, which a voucher-priced page is expected to
377
+ differ from. The verdict's top-level `commercial` section records
371
378
  coverage, sanitized missing/unmatched/invalid capture evidence, proxy issues,
372
379
  and findings; the same findings are serialized deterministically into the flat
373
380
  `assertions` array consumed by existing QA tooling. A proven mismatch keeps the
@@ -1599,7 +1606,7 @@ the CampaignSpec instead:
1599
1606
  purchase multipliers (`ref` and `ref:2`). A uniquely referenced catalog
1600
1607
  package with its own `qty: 3` composition is still bought once (`ref`), not
1601
1608
  multiplied by three. **Order-bump rows — `packages[]` entries marked
1602
- `is_upsell: true` — are add-ons offered beside the selected tier, not tiers**:
1609
+ `is_order_bump: true` or `is_upsell: true` — are add-ons offered beside the selected tier, not tiers**:
1603
1610
  they never become a plan (a three-tier checkout with one bump plans three
1604
1611
  tiers, so `tiers:common` on a two-upsell funnel is 12 orders, not 16), and
1605
1612
  the runner prints a `[qa:test-order]` line naming the bump ref(s) it left
@@ -1858,6 +1865,34 @@ table in [build-packet.md](./build-packet.md#deploy-target) and
1858
1865
  [Local proof mode](#local-proof-mode-deploytarget-local-serve) above for the
1859
1866
  development build, the parity check, and the order they run in.
1860
1867
 
1868
+ ### Tracking parameters
1869
+
1870
+ **Tracking parameters.** During a browser test order, QA adds synthetic tracking values (`utm_*`, `affid`, `sub1`, `subaffiliate2`, and up to eight names from `analytics.params.tracking.preserve`) to the pages it loads before the campaign's own navigation starts. It reports two results:
1871
+
1872
+ - **URL preservation:** whether each value is still in the address after each page navigation it observed, through to the navigation after the order. A failing step is named only when QA observed both sides of it. A reload by QA itself, an unobserved step, or an attempt that ended early ends the measurement without a pass.
1873
+ - **Order attribution:** whether the matching attribution field reached the request of an order the store accepted.
1874
+
1875
+ Values from `<meta name="os-tracking-tag">` tags are checked against order metadata. When page script sets attribution itself, differences are review results. Only synthetic values and equality results are stored, and order URLs in QA evidence are now stored without their query. Click ids and `funnel` are not seeded, even when listed in `tracking.preserve`, and are listed as not tested.
1876
+
1877
+ Every string QA stores in its verdict is cut at its first query (a `?`, written literally or percent-encoded), every URL in it is also cut at its fragment, and a string longer than 16 KiB is truncated.
1878
+
1879
+ ## Content parameters
1880
+
1881
+ **Content parameters.** For each `analytics.params.content` entry, QA loads each applicable page twice in separate fresh browser contexts: once without the parameter and once with `?<name>=n`. It waits for the SDK to finish its display pass, then checks that the same elements whose `data-next-hide` or `data-next-show` refers to `param.<name>` are visible without the parameter and hidden with it. A parameter whose only handlers cannot hide anything with `n` is a warning. Compound conditions, conditions the SDK parses unreliably, content already hidden without the parameter, elements that change between the two loads, pages that fail to load, and pages where the SDK did not finish produce review or unexercised results, never a pass. Runs without `--browser` list these checks as not requested. No orders are placed.
1882
+
1883
+ ## Policy links
1884
+
1885
+ **Policy links.** For each configured `store_terms`, `store_privacy`, `store_returns`, `store_shipping`, and `store_contact` URL, QA checks two things:
1886
+
1887
+ - **Presence:** whether any visited page links to that exact destination, with trailing slashes and default ports normalized and query parameters kept. Presence is decided only when QA read the links on every page it visits; otherwise it is unexercised.
1888
+ - **Availability:** whether the URL reaches an HTML page within 5 redirects, using bounded GET requests that read headers only.
1889
+
1890
+ A redirect to the right page is fine. Missing links, links whose query differs, links whose declared footer label points elsewhere, 404s, server errors and redirects to the store's home page are warnings. Authentication, rate limiting, loops, non-HTML responses, unexpected statuses, configured values that are not absolute URLs, and redirects to other destinations are review results. Network failures are unexercised. A 200 response proves only that the page is reachable. `mailto:` contacts are checked for presence only. Runs without `--browser` list these checks as not requested. Link wording is used only as an English review hint.
1891
+
1892
+ ## Operator accepts (`checkpoint accept`)
1893
+
1894
+ **Accepting a warning.** `campaigns-os checkpoint accept` records an operator's decision about a warning that a Campaigns OS check measured. It does not change readiness, does not remove the warning, does not change the status `next` reports, and cannot be used on review, unexercised, or excluded results. The warning must already be on record (in the doctor snapshot `next` writes, the Polish capture, or the full QA verdict) before it can be accepted. The accept is bound to the exact measured state and lapses when that state changes, or when its evidence is stale or cannot be reproduced. An optional expiry or review condition adds a second bound. Run it only with the operator's explicit decision, made after the warning was shown to them. A plausible name is not authorization. The command refuses placeholder and automation identities. Results are re-derived from the package's raw captures on every read, and a result whose capture is missing or does not match is listed as unexercised. These checks are tamper evidence, not proof of authorship: a hand-written record that copies every value and recomputes the checksum, or a hand-written result written together with a matching capture, is not detected.
1895
+
1861
1896
  ## Launch Readiness Note
1862
1897
 
1863
1898
  Campaigns OS can prove the campaign build, SDK wiring, browser behavior, and
@@ -1885,6 +1920,13 @@ use the declared topology instead of a single happy path:
1885
1920
  5. Use `full` when you want every actual terminal path, raising the flood cap to
1886
1921
  the exact planned count when necessary.
1887
1922
 
1923
+ `next` names step 4 at the QA stage. When the checkout page the default run
1924
+ drives declares an order bump (`is_order_bump: true` or `is_upsell: true` rows), it lists a second QA
1925
+ command, `qa_run_bump`, beside `qa_run`: the same `--test-order common` run
1926
+ with `--cart <base>:1,<bump>:1`, where the base is the first selector tier the
1927
+ checkout declares. The QA stage prompt and the human `next` output carry the
1928
+ same command.
1929
+
1888
1930
  Record order numbers, `ref_id` values, and expected line-item shapes in the
1889
1931
  handoff. If the browser console shows an SDK module-load error but the SDK
1890
1932
  fallback loads and checkout/order proof passes, keep it as platform warning
@@ -1927,19 +1969,32 @@ expectations, and commerce-ref validation. A doctor-clean non-packet run means
1927
1969
  matches a spec". Treat it as a residue/visual gate, not equivalent to a
1928
1970
  packet-driven QA pass.
1929
1971
 
1930
- ### Per-page credential declarations
1972
+ ### Per-page credential binding
1931
1973
 
1932
1974
  Canonical `qa run` emits `page-binding:<page_id>` in the existing `api-metadata`
1933
1975
  family, with `campaigns-os-page-binding/v0` evidence (typed in the verdict schema).
1934
- `match` means the statically declared credential equals the expected credential;
1976
+ `match` means the page's credential equals the expected credential;
1935
1977
  `mismatch` is a blocker. `unknown` requires manual review. All three carry
1936
1978
  `identity: not_verified`: credential equality never proves a unique Campaign App
1937
1979
  ID, and no App ID is inferred from `campaignId` or `next-campaign-id`.
1938
1980
 
1939
1981
  Expected data reuses the commercial QA resolver (packet, then spec, then an
1940
1982
  explicit supported environment source). Conflicting authored values are unknown.
1941
- The SDK loads `window.nextConfig.apiKey` before `next-api-key` at boot, so meta
1942
- wins at runtime; this check deliberately reports differing declarations as a
1983
+
1984
+ With `--browser`, the row comes from the key the Campaign Cart SDK actually
1985
+ sent. The SDK sends it as `Authorization` on every Campaigns API request, and
1986
+ the browser pass reads that header on each page it loads. The values are
1987
+ compared in memory and dropped: the evidence records
1988
+ `observation: sdk_request`, `source_kinds: ["sdk_request"]`, and `match` when
1989
+ every request carried the expected credential or `mismatch` when any carried
1990
+ another. This works whatever the
1991
+ page's scripts look like. A page that sent no Campaigns API request, or a run
1992
+ with no single expected credential, keeps the static row described below.
1993
+
1994
+ Without `--browser`, or for such a page, the row is a static read of what the
1995
+ page declares (`observation: static_declaration`). The SDK loads
1996
+ `window.nextConfig.apiKey` before `next-api-key` at boot, so meta wins at
1997
+ runtime; the static read deliberately reports differing declarations as a
1943
1998
  conflict rather than certifying one. It does not observe SDK execution.
1944
1999
 
1945
2000
  The bounded HTML loader is reused. HTML is parsed without execution; JavaScript
@@ -8,7 +8,7 @@
8
8
 
9
9
  How a checkout of this repository at one commit becomes a usable installed runtime, and how a consumer decides whether a prepared one is still trustworthy. Everything below is generated from `contracts/runtime-recipe.campaigns-os-node-v1.json`, which is the only authority for these values.
10
10
 
11
- Recipe kind `campaigns-os-node-v1`, revision `1.0.2`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.48.0`.
11
+ Recipe kind `campaigns-os-node-v1`, revision `1.0.2`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.52.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -10,7 +10,7 @@ Omit `--json` for the concise human report. Exit 0 means source-compatible; exit
10
10
 
11
11
  The manifest is generated and owned by Campaign Cart, from its storage registry. The scanner has no independent SDK key list and fetches no remote code. Supply `docs/compatibility/storage-migrations.v1.json` from an unmodified checkout of a Campaign Cart release tag, v0.4.40 or later. The report records the full file SHA-256, SDK version, supported target range, registry/extractor/input digest, and Git provenance. When the supplied bytes equal their repository HEAD blob, provenance is `verified-git-blob` with commit and repository path. Proposed, modified, or copied manifests are labeled `unverified-local-file`; the label never certifies a release. Review/pin SDK provenance separately before acting on findings. A release manifest covers the target range it declares, which may end below its own SDK version: the v0.4.40 manifest, for example, covers 0.4.38 only. Targets outside the manifest's supported SDK range report unknown. A manifest whose SDK version is below its supported maximum is invalid.
12
12
 
13
- `--target` must name the Git root. `--scope` is mandatory: comma-separated literal repository-relative directory/file paths; use `.` only when the complete repository is intended. Add shared JavaScript directories explicitly. `--exclude` uses the same syntax and records explicit exclusions. Archives receive no implicit exemption. Only Git-tracked `.html`, `.htm`, `.js`, `.mjs`, and `.cjs` files in the selected scope are scanned, using current working-tree bytes and per-file digests; untracked files and built dependencies are outside this evidence. A selected HTML page's local script outside the selected files reports unknown. Relative scripts affected by an HTML `<base href>` also report unknown; review their actual dependency paths. Remote SDK and third-party scripts are not fetched or analyzed.
13
+ `--target` must name the Git root. `--scope` is mandatory: comma-separated literal repository-relative directory/file paths; use `.` only when the complete repository is intended. Add shared JavaScript directories explicitly. `--exclude` uses the same syntax and records explicit exclusions. Archives receive no implicit exemption. Only Git-tracked `.html`, `.htm`, `.js`, `.mjs`, and `.cjs` files in the selected scope are scanned, using current working-tree bytes and per-file digests; untracked files and built dependencies are outside this evidence. A selected HTML page's local script outside the selected files reports unknown. In a Page Kit campaign (a page under `src/<slug>/`), a script src written `{{ 'js/x.js' | campaign_asset }}` resolves to `src/<slug>/assets/js/x.js`, as Page Kit serves it, and a page's frontmatter `scripts:` entries resolve the same way, since a layout's `{% for script in scripts %}` loop loads them through that filter. Any other `campaign_asset` value, a path that resolves outside the assets folder, and frontmatter that is not valid YAML or whose `scripts` is not a list of paths report unknown. Relative scripts affected by an HTML `<base href>` also report unknown; review their actual dependency paths. Remote SDK and third-party scripts are not fetched or analyzed.
14
14
 
15
15
  Acorn parses JavaScript ASTs; parse5 identifies executable inline HTML scripts and source offsets. Findings inventory storage area, operation, key, file/line/column, reason, and SDK-owned public replacement when supplied. The scanner recognizes direct `localStorage`/`sessionStorage`, `window`/`globalThis`/`self` properties, simple constant browser-global/storage aliases and storage destructuring, literal `getItem`/`setItem`/`removeItem` calls (including bracket method notation), property access/assignment/deletion, and lexical constant string indirection. Comments, ordinary strings, JSON script blocks and public store calls do not become storage reads. A literal legacy SDK key at or after its manifest migration boundary is incompatible. Guessed prefixes cannot establish compatibility.
16
16