@nextcommerce/campaigns-os 1.47.0 → 1.50.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 (53) hide show
  1. package/CHANGELOG.md +307 -0
  2. package/README.md +30 -3
  3. package/agents/claude/CLAUDE.md +6 -3
  4. package/agents/codex/AGENTS.md +6 -4
  5. package/agents/copilot/copilot-instructions.md +3 -2
  6. package/agents/cursor/campaigns-os.mdc +3 -3
  7. package/compatibility.json +1 -1
  8. package/contracts/commerce-surface-catalog.json +17 -17
  9. package/contracts/effects.v1.json +173 -0
  10. package/contracts/release-ledger.json +798 -0
  11. package/contracts/supported-surface.json +4 -4
  12. package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
  13. package/docs/brand-theme-bridge.md +12 -6
  14. package/docs/build-packet.md +9 -4
  15. package/docs/campaign-build-brief.md +25 -28
  16. package/docs/local-setup.md +7 -2
  17. package/docs/orientation-contract-reference.md +1 -1
  18. package/docs/qa-and-test-orders.md +70 -9
  19. package/docs/runtime-readiness.md +1 -1
  20. package/docs/skills-revision.md +10 -10
  21. package/package.json +1 -1
  22. package/schemas/campaign-runtime-build-packet.v0.schema.json +4 -0
  23. package/schemas/campaigns-os-qa-verdict.v0.schema.json +8 -3
  24. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  25. package/skills/campaign-readback-classification/SKILL.md +3 -3
  26. package/skills/campaign-run-evidence/SKILL.md +9 -4
  27. package/skills/contribution-intake/SKILL.md +3 -3
  28. package/skills/next-campaigns-build/SKILL.md +5 -4
  29. package/skills/next-campaigns-os/SKILL.md +3 -3
  30. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  31. package/skills/next-campaigns-polish/SKILL.md +6 -5
  32. package/skills/next-campaigns-qa/SKILL.md +3 -3
  33. package/skills.json +10 -10
  34. package/src/brand-theme.mjs +13 -2
  35. package/src/cli.mjs +17 -12
  36. package/src/content-residue.mjs +18 -90
  37. package/src/doctor/checks.mjs +21 -29
  38. package/src/doctor/inspect.mjs +13 -3
  39. package/src/doctor/next-step.mjs +4 -0
  40. package/src/gate-actions.mjs +8 -0
  41. package/src/install-mode.mjs +0 -8
  42. package/src/invocation.mjs +3 -1
  43. package/src/local-preview-policy.mjs +92 -0
  44. package/src/page-kit-sdk-version.mjs +8 -1
  45. package/src/polish-node.mjs +26 -2
  46. package/src/progress-node.mjs +5 -1
  47. package/src/qa-binding-evidence.mjs +22 -1
  48. package/src/qa-browser.mjs +92 -14
  49. package/src/qa-node.mjs +43 -6
  50. package/src/readback.mjs +19 -10
  51. package/src/source-prep.mjs +1 -1
  52. package/src/stage-record.mjs +303 -22
  53. 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.47.0",
3
+ "surface_version": "1.50.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": "75cdd0bc33788d8d6af10c995f362987725f622bf42700f1b70b39bbd87e6878"
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": "3793b1de3b77cc31dffe9176f47e8e99ea6134aa",
58
+ "sha": "37a8d945db4c360739ded98348814cfd632da94f",
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
  },
@@ -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
@@ -970,8 +970,9 @@ starter palette is acceptable>" --waived-by "<named human>"`, optionally
970
970
  (status and severity — never `fail`) and keeps the shipped palette visible in
971
971
  the verdict; or hand-author the brand
972
972
  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
973
+ frontmatter styles, rebuild, `record build`, and `record theme`, which records
974
+ `report.theme.status: applied` with `load_order: after-next-core` from the
975
+ built pages. Nothing waives the gate on the operator's
975
976
  behalf. See [Brand Theme Bridge](./brand-theme-bridge.md) for both lanes in
976
977
  full.
977
978
 
@@ -1237,7 +1238,9 @@ loopback host (`127.0.0.1`, `[::1]`) with a ready line naming the
1237
1238
  `http://localhost:<port>/` fallback, and warns (`deploy.local_serve_url`) when
1238
1239
  the recorded URL is neither.
1239
1240
  `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
1241
+ instead of a ship-to-host one, and the served URL is recorded with
1242
+ `campaigns-os record deploy --packet <p> --base-url <url>` (the record command
1243
+ table below). The directory to serve is `_site/`; for a
1241
1244
  root-served campaign (`campaign.route_root: "/"`) the handoff adds that pages
1242
1245
  are served at site-root paths while assets keep the `/<public_route_slug>/`
1243
1246
  prefix, so `_site/` needs the same rewrite of root-level page routes onto
@@ -1453,7 +1456,7 @@ Each `manifest.pages[]` entry MAY carry a `source_hash` field — the sha256 hex
1453
1456
  Behavior:
1454
1457
 
1455
1458
  - Optional on the producer side. Producers that don't emit `source_hash` (pre-Slice-6 manifests, template-stock, hand-authored) keep working; doctor's drift check is silent without a hash to compare.
1456
- - Warning severity only. A drift never blocks a build — the operator decides whether to re-run the producer to refresh the manifest or accept the local edits.
1459
+ - Warning severity only. A drift never blocks a build. The hash doctor compares is the one intake recorded in the packet, so editing the manifest alone does not clear the warning: re-running `start` or `prepare-build` with `--force` records the current file, and also clears recorded stage evidence. A revision made after build belongs in the page-kit source under `src/<route>/`; the source HTML stays the design provenance.
1457
1460
  - The warning names the file path and includes both hashes (truncated to 12 chars) so the operator can confirm which file diverged without re-running the producer.
1458
1461
 
1459
1462
  ### Reference AI-generated producer
@@ -1609,6 +1612,8 @@ hand-editing `.campaign-runtime/` JSON:
1609
1612
  | `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
1613
  | `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 |
1611
1614
  | `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 |
1615
+ | `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` |
1616
+ | `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
1617
 
1613
1618
  Each command also refuses a stage `next` has not reached: while doctor's
1614
1619
  prepare-build gate is set (`next` answers prepare-build) or while an earlier
@@ -62,34 +62,31 @@ Doctor warns when generated guided drafts still need answers, a brief allows pay
62
62
 
63
63
  Existing template residue, theme, pricing, and built-output checks continue to run. The brief gives those checks business intent instead of replacing them.
64
64
 
65
- ## Content Claims Are Reviewed, Not Enforced
66
-
67
- The doctor scans built output for content residue and raises some of it as
68
- warnings: every content anti-pattern under the warning code
69
- `content_residue.anti_pattern` (the finding ids include `invented_counts`
70
- for invented counts and ratings, `verified_buyer_chrome` for "Verified
71
- Buyer" and similar review chrome, `byline_persona`, `borrowed_authority`,
72
- `press_marquee`, and `science_theater`; all of them are warning-only), and
73
- promo copy claiming a discount above the CampaignSpec maximum (`template_contract.discount_claim_residue`, or
74
- `template_contract.discount_claim_unverified` when the spec sets no maximum).
75
-
76
- These stay warnings on purpose, and nothing downstream reads them. There is no
77
- blocker, no `blocked_stages` entry, no QA assertion, and no test-order gate
78
- keyed on any of them. A build carrying all of them can pass doctor, pass QA,
79
- and deploy.
80
-
81
- The toolkit flags the copy; it does not adjudicate it. **Responsibility for
82
- every claim on the page — proof counts, review chrome, discount percentages,
83
- and the rest — sits with the operator and the client, not with Campaigns OS.**
84
- Use the brief to record which claims are approved and which language is
85
- forbidden (see the high-impact questions above), and treat a content warning as
86
- a prompt to check the brief, not as a gate that will stop the build if you
87
- ignore it.
88
-
89
- The hard content checks are separate and do block: the needs-merchant-input
90
- marker (`content_residue.needs_merchant_input`) and countdown chrome rendered
91
- without verified offer urgency on a brief-backed build
92
- (`content_residue.unverified_urgency`).
65
+ ## The Merchant's Own Claims Are Not Reviewed
66
+
67
+ Proof and urgency content the merchant supplies is the merchant's
68
+ responsibility, not Campaigns OS's: reviews and testimonials, ratings and
69
+ counts, "Verified Purchase" labels, recent-purchase popups, stock counters,
70
+ countdowns, guarantees and press mentions. The doctor does not scan for it and
71
+ QA does not assert on it. When the prepared source design carries these
72
+ elements, the build reproduces them as designed. The starter templates ship
73
+ without some of them; that is not a reason to drop the source's.
74
+
75
+ What the doctor does scan built output for is template residue: the starter
76
+ templates' own demo strings and bracket-style stubs
77
+ (`content_residue.demo_residue`, a warning). It also warns on promo copy
78
+ claiming a discount above the CampaignSpec maximum
79
+ (`template_contract.discount_claim_residue`, or
80
+ `template_contract.discount_claim_unverified` when the spec sets no maximum),
81
+ because that copy disagrees with the campaign's own pricing. Nothing downstream
82
+ blocks on either.
83
+
84
+ Two content checks do block: the needs-merchant-input marker
85
+ (`content_residue.needs_merchant_input`), and, on a brief-backed build only,
86
+ starter countdown chrome the brief payload does not verify
87
+ (`content_residue.unverified_urgency`). Use the brief to record which claims
88
+ are approved and which language is forbidden (see the high-impact questions
89
+ above).
93
90
 
94
91
  ## QA Policy Scope
95
92
 
@@ -1,11 +1,16 @@
1
1
  # Local campaign setup
2
2
 
3
- For a new campaign, choose its working folder and run this from that folder:
3
+ For a new campaign, create an empty working folder and run this from it:
4
4
 
5
5
  ```sh
6
- npm install --save-exact next-campaign-page-kit@0.2.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.47.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.50.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
7
7
  ```
8
8
 
9
+ `npm init -y` gives the folder its own `package.json`. Without one, npm
10
+ installs into the nearest parent folder that has a `package.json` or
11
+ `node_modules`, so a campaign folder created inside another project would add
12
+ page-kit and the toolkit to that project instead.
13
+
9
14
  Review the release source/provenance before installation as described in
10
15
  `AGENTS.md`. npm installs the dependencies first; `--no-install` then runs only
11
16
  the project's installed CLI. Page-kit stays a runtime dependency and the
@@ -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.47.0`
29
+ Supported surface at generation time: `1.50.0`
30
30
 
31
31
  ## Forward compatibility
32
32
 
@@ -55,9 +55,10 @@ the toolkit runs that loop in a fixed order:
55
55
  with it locally (the parity step renders it to a temp dir, and the deploy
56
56
  host renders it from the committed source).
57
57
  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.
58
+ names the directory and any root-route rewrite) and run `polish capture`
59
+ against it. Once polish is recorded, `record deploy --packet <p> --base-url
60
+ <url>` records the URL on `deploy.preview_url` and the deploy stage; then run
61
+ `qa run --browser` and the typed-card order paths against it.
61
62
  3. **Prove the pin on the production output.** Before committing, run
62
63
 
63
64
  ```bash
@@ -96,6 +97,32 @@ recapture. A hosted target (`netlify`, `cloudflare-pages`, …) is unaffected:
96
97
  its build stage renders production as before and `page-kit parity` refuses the
97
98
  packet (`local_proof.parity.not_local_serve`).
98
99
 
100
+ ### Missing evidence carried forward on the local preview
101
+
102
+ On a `local-serve` packet served from a loopback host (`localhost`,
103
+ `127.0.0.1`, `[::1]`, for both `deploy.preview_url` and `--base-url`), some
104
+ missing evidence is carried forward as a warning instead of blocking the
105
+ loop. The campaign must still prove its commerce there: store and campaign
106
+ binding, routes, SDK loading, prices and a typed-card order. Only these
107
+ checks are carried forward:
108
+
109
+ | Check | When |
110
+ | --- | --- |
111
+ | `polish.evidence_missing`, `polish.report_missing` | Polish was never recorded for this build. |
112
+ | `polish.hidden_eager_media.no_capturable_routes` | Every mapped page is template stock (`skip_reason`), so polish capture has no design route to capture. |
113
+ | `polish.hidden_eager_media.capture_malformed` | Only when no page-load capture was recorded at all. |
114
+ | Template-residue severity | With `theme_gate.nothing_generatable`, the starter template is the design, so residue findings are warnings rather than blockers. |
115
+
116
+ A carried-forward gate has status `carried_forward`. Doctor reports it as a
117
+ warning starting "Carried forward on the local preview"; `next` moves past
118
+ polish to deploy and QA; QA records it as a `warn` row, so the verdict is at
119
+ best `ready_with_exceptions`. The evidence is reported as missing, never as
120
+ passed. Any other check keeps its meaning. A hosted preview or production
121
+ packet, and a `local-serve` packet served from any other host, gets the strict
122
+ gates. `record polish` and the waiver commands also keep them strict. Progress
123
+ snapshots record a carried-forward gate as `not_applicable`, because their
124
+ schema has no carried-forward state.
125
+
99
126
  ## Resolve
100
127
 
101
128
  Use resolve before a full run:
@@ -482,7 +509,15 @@ the three checks and each `evidence.checks[]` entry carries `kind`
482
509
  (`family_shell` or `sdk_wiring`).
483
510
  The upsell and checkout bundle price checks count the SDK's
484
511
  `[data-next-bundle-display*='price']` alongside the contract's price rows; a
485
- hidden, zero-size or empty bundle-display node does not count.
512
+ hidden, zero-size or empty bundle-display node does not count. A checkout that
513
+ shows no price because QA opened it directly, with an empty SDK cart and no
514
+ package selection of its own, is `skipped` rather than failed:
515
+ `pricing.checkout_price_visible` records `cart_count` and
516
+ `checkout_selection_surface` and says so. That checkout's cart is filled on
517
+ an earlier page, for example by a landing link carrying `forcePackageId`, and
518
+ the test order enters it from there. A checkout with its own package
519
+ selection, or a filled cart, still fails when no price shows. If the cart
520
+ probe itself fails, the row stays failed and records `empty_cart_probe_error`.
486
521
  Promoted template families must also have
487
522
  `contracts/template-brand-contract.<family>.v0.json`; QA emits a blocker if the
488
523
  selected family is missing its brand/residue/pricing contract instead of
@@ -1749,6 +1784,17 @@ shared real inbox rather than a synthetic one). When neither is set, the runner
1749
1784
  falls back to a single stable synthetic address — still one reused customer, but
1750
1785
  not deliverable.
1751
1786
 
1787
+ Reusing one customer has one cost. The platform refuses an order whose customer,
1788
+ items and total match one it accepted or is still processing in the last 30
1789
+ minutes ("Duplicate order detected, order not created"). A successful test order
1790
+ does not hold that window, so the paths of one run do not collide. Two QA runs
1791
+ against the same campaign at once do, and so can a rerun soon after an attempt
1792
+ that died mid-submit. QA reports it on the path's `browser-test-order` row as
1793
+ `order create rejected: HTTP 400: Duplicate order detected …` followed by
1794
+ `duplicate_order` and the remedy: re-run with a different `--test-email-prefix`
1795
+ (or `--test-email`), or wait. The shipping address is not part of the match, so
1796
+ changing `--test-address1` does not help.
1797
+
1752
1798
  The browser driver intentionally behaves like a user:
1753
1799
 
1754
1800
  - package selection uses rendered `[data-next-package-id]` controls when
@@ -1882,19 +1928,32 @@ expectations, and commerce-ref validation. A doctor-clean non-packet run means
1882
1928
  matches a spec". Treat it as a residue/visual gate, not equivalent to a
1883
1929
  packet-driven QA pass.
1884
1930
 
1885
- ### Per-page credential declarations
1931
+ ### Per-page credential binding
1886
1932
 
1887
1933
  Canonical `qa run` emits `page-binding:<page_id>` in the existing `api-metadata`
1888
1934
  family, with `campaigns-os-page-binding/v0` evidence (typed in the verdict schema).
1889
- `match` means the statically declared credential equals the expected credential;
1935
+ `match` means the page's credential equals the expected credential;
1890
1936
  `mismatch` is a blocker. `unknown` requires manual review. All three carry
1891
1937
  `identity: not_verified`: credential equality never proves a unique Campaign App
1892
1938
  ID, and no App ID is inferred from `campaignId` or `next-campaign-id`.
1893
1939
 
1894
1940
  Expected data reuses the commercial QA resolver (packet, then spec, then an
1895
1941
  explicit supported environment source). Conflicting authored values are unknown.
1896
- The SDK loads `window.nextConfig.apiKey` before `next-api-key` at boot, so meta
1897
- wins at runtime; this check deliberately reports differing declarations as a
1942
+
1943
+ With `--browser`, the row comes from the key the Campaign Cart SDK actually
1944
+ sent. The SDK sends it as `Authorization` on every Campaigns API request, and
1945
+ the browser pass reads that header on each page it loads. The values are
1946
+ compared in memory and dropped: the evidence records
1947
+ `observation: sdk_request`, `source_kinds: ["sdk_request"]`, and `match` when
1948
+ every request carried the expected credential or `mismatch` when any carried
1949
+ another. This works whatever the
1950
+ page's scripts look like. A page that sent no Campaigns API request, or a run
1951
+ with no single expected credential, keeps the static row described below.
1952
+
1953
+ Without `--browser`, or for such a page, the row is a static read of what the
1954
+ page declares (`observation: static_declaration`). The SDK loads
1955
+ `window.nextConfig.apiKey` before `next-api-key` at boot, so meta wins at
1956
+ runtime; the static read deliberately reports differing declarations as a
1898
1957
  conflict rather than certifying one. It does not observe SDK execution.
1899
1958
 
1900
1959
  The bounded HTML loader is reused. HTML is parsed without execution; JavaScript
@@ -1919,7 +1978,9 @@ ASCII whitespace and compared case-insensitively as the browser does; classic
1919
1978
  `built_output.script_syntax` in [the Build Packet doc](build-packet.md).
1920
1979
 
1921
1980
  External executable scripts other than the recognized jsDelivr Campaign Cart
1922
- loader/index are inspected only on the page's origin. Each page admits at most
1981
+ loader or index are inspected only on the page's origin. Three Campaign Cart
1982
+ paths count as the SDK and are never fetched: `dist/loader.js` (the one the
1983
+ starter templates load), `dist/index.js` and `public/loader.js`. Each page admits at most
1923
1984
  6 such references; each run fetches at most 24 distinct URLs (deduplicated),
1924
1985
  256 KiB per response and 6 MiB aggregate, 5 seconds per request including body
1925
1986
  read (at most 30 seconds of sequential config requests per page). Redirects,
@@ -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.47.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.50.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -16,7 +16,7 @@ that the copy on disk moved.
16
16
  `skills.json` carries one top-level field:
17
17
 
18
18
  ```json
19
- "bundle_revision": "1.47.0+skills.1"
19
+ "bundle_revision": "1.50.0+skills.1"
20
20
  ```
21
21
 
22
22
  The spelling is `<package version>+skills.<n>`:
@@ -25,7 +25,7 @@ The spelling is `<package version>+skills.<n>`:
25
25
  skills ship with (`check-skill-versions.mjs` fails if the two disagree);
26
26
  - `<n>` is a plain counter, not a semver component. It says "this is the *n*th
27
27
  skill-text revision published against that package version" and it **resets
28
- with the prefix**. `1.47.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.50.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
29
29
 
30
30
  It is one identity for the bundle as a whole, on purpose. Per-skill versions
31
31
  still exist and still gate per-skill changes, but an agent that loaded one skill
@@ -37,7 +37,7 @@ The first body line of every bundled `SKILL.md`, immediately after the
37
37
  frontmatter, is exactly:
38
38
 
39
39
  ```
40
- Bundle revision: 1.47.0+skills.1
40
+ Bundle revision: 1.50.0+skills.1
41
41
  ```
42
42
 
43
43
  followed by a short paragraph telling the agent to run the check below at the
@@ -48,7 +48,7 @@ text the agent is actually reading, not from a file it would have to go and open
48
48
  ## The check
49
49
 
50
50
  ```bash
51
- npx --no-install campaigns-os tooling status --skills-revision 1.47.0+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.50.0+skills.1
52
52
  ```
53
53
 
54
54
  The value is compared against the bundle revision of the **CLI the command runs
@@ -90,20 +90,20 @@ reports the choice as `skills.scope` (`requested`, `installed_platforms`, or
90
90
  "revision_check": "match",
91
91
  "skills_revision": {
92
92
  "status": "match",
93
- "requested": "1.47.0+skills.1",
93
+ "requested": "1.50.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.47.0+skills.1",
95
+ "on_disk": "1.50.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.47.0+skills.1)"
97
+ "message": "match (1.50.0+skills.1)"
98
98
  }
99
99
  ```
100
100
 
101
101
  The text view prints one named line, as a header above the rest of the status:
102
102
 
103
103
  ```
104
- Skills revision: match (1.47.0+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.47.0+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.47.0+skills.1)
104
+ Skills revision: match (1.50.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.50.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.50.0+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.47.0",
3
+ "version": "1.50.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -336,6 +336,8 @@
336
336
  "template_family": {
337
337
  "enum": [
338
338
  "undecided",
339
+ "apollo",
340
+ "apollo-mv-single-step",
339
341
  "olympus",
340
342
  "limos",
341
343
  "demeter",
@@ -343,6 +345,8 @@
343
345
  "olympus-mv-two-step",
344
346
  "shop-single-step",
345
347
  "shop-three-step",
348
+ "arjuna",
349
+ "karna",
346
350
  "custom"
347
351
  ]
348
352
  },
@@ -146,7 +146,11 @@
146
146
  "const": "campaigns-os-page-binding/v0"
147
147
  },
148
148
  "observation": {
149
- "const": "static_declaration"
149
+ "enum": [
150
+ "static_declaration",
151
+ "sdk_request"
152
+ ],
153
+ "description": "static_declaration: the key the page's HTML and same-origin config scripts declare. sdk_request: the key the Campaign Cart SDK sent to the Campaigns API in the browser pass (qa run --browser)."
150
154
  },
151
155
  "outcome": {
152
156
  "enum": [
@@ -174,7 +178,8 @@
174
178
  "enum": [
175
179
  "meta",
176
180
  "inline",
177
- "config_script"
181
+ "config_script",
182
+ "sdk_request"
178
183
  ]
179
184
  }
180
185
  },
@@ -182,7 +187,7 @@
182
187
  "const": "not_verified"
183
188
  }
184
189
  },
185
- "description": "Credential-free static declaration comparison, not observed execution or Campaign App identity. No keys, hashes, fragments, source URLs or inferred resource IDs."
190
+ "description": "Credential-free comparison of the page's key with the expected key: the static declaration, or the key the SDK sent in the browser pass. Not Campaign App identity. No keys, hashes, fragments, source URLs or inferred resource IDs."
186
191
  },
187
192
  "pageUrlRef": {
188
193
  "type": "object",
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-lifecycle-orientation
3
- version: 1.0.19
3
+ version: 1.0.24
4
4
  description: Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing any stage or changing any state.
5
5
  ---
6
6
 
7
- Bundle revision: 1.47.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.47.0+skills.1`
7
+ Bundle revision: 1.50.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-readback-classification
3
- version: 1.0.19
3
+ version: 1.0.24
4
4
  description: Classify a selected campaign from the readback projection's v2 fields and write a read-only handoff without turning diagnosis into permission.
5
5
  ---
6
6
 
7
- Bundle revision: 1.47.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.47.0+skills.1`
7
+ Bundle revision: 1.50.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-run-evidence
3
- version: 1.0.19
3
+ version: 1.0.24
4
4
  description: Interpret existing Campaigns OS doctor, QA and proof-depth evidence without claiming more proof than the artifacts contain.
5
5
  ---
6
6
 
7
- Bundle revision: 1.47.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.47.0+skills.1`
7
+ Bundle revision: 1.50.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -65,7 +65,12 @@ Saved-Map QA may publish it under the existing consent and flag controls; a
65
65
  publication failure does not erase the local one. Local-spec packet verdicts
66
66
  stay local even with `--post-verdict`, and `qa publish` refuses those packets.
67
67
  The readback projects `.campaign-runtime/qa-verdict.json` when
68
- that sidecar has been copied into the campaign repository. A markdown QA
68
+ that sidecar has been copied into the campaign repository. The sidecar is a
69
+ committable projection: it always empties `test_orders`, `entry_urls`,
70
+ `page_urls` and `tested_urls`, so an empty `test_orders` there says nothing
71
+ about ordering. Its typed-card proof is the `browser-test-order:<path>`
72
+ assertions, each with a `browser-order-total-parity:<path>` row beside it; the
73
+ full verdict under `qa-output/` keeps the order records. A markdown QA
69
74
  report, a ledger or a gate script is not a verdict and must not be scanned for
70
75
  a disposition, a run id or a blocker. Where two JSON verdicts exist, interpret
71
76
  the one the projection loaded; do not walk a report looking for a later rerun.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: contribution-intake
3
- version: 1.0.19
3
+ version: 1.0.24
4
4
  description: Turn a suggestion about the agent surface into a classified, evidence-checked proposal and, only with attended approval, one issue on this repository's tracker.
5
5
  ---
6
6
 
7
- Bundle revision: 1.47.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.47.0+skills.1`
7
+ Bundle revision: 1.50.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-build
3
- version: 1.0.24
3
+ version: 1.0.29
4
4
  description: Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts.
5
5
  ---
6
6
 
7
- Bundle revision: 1.47.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.47.0+skills.1`
7
+ Bundle revision: 1.50.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -86,6 +86,7 @@ Build rules:
86
86
  - Preserve SDK-owned checkout/cart/upsell/receipt/payment/address/totals/submit surfaces.
87
87
  - If `doctor` (tier `A`: inspection that writes nothing, plus one read-only live campaign request once the site is built and a public campaign key resolves; `--write` is also tier `A` and writes doctor output; `--built` is tier `B`) reports `derived.scope.mode = "partial"`, build the pages listed in `derived.scope.built_pages` from their prepared source. Keep every out-of-scope route unbuilt by default, including pages marked `template_stock: true`; never publish placeholder presell/landing pages that remain on another host. Materialize a stock page only with explicit per-page operator opt-in (including a required pre-checkout `select` stand-in). For opted-in pages use the locked family's own page for that role with its dependent `_includes/`, `_layouts/`, and assets, wire it from CampaignSpec, and build a required select step first. Do not attest stock screenshots as design source. Built stock pages rejoin preview QA once their HTML exists. Carry remaining `skip_reason` declarations into the report and label the preview route/visual-testable rather than full-funnel launch-ready.
88
88
  - For `landing` and `presell` pages, prefer the prepared source HTML when `source_html.pages[].path` points at a real standalone page. Preserve the design/content through a passthrough page-kit layout, inject the SDK loader/config as needed, and repoint CTAs into the CampaignSpec flow. Treat `source_html.pages[].path` and `context.page_map[].source_path` as source provenance. Treat `source_html.pages[].page_kit`, `context.page_map[].page_kit`, and `context.page_map[].output_path` as the Page Kit target file, route, CPK `page_type`, and frontmatter projection.
89
+ - Reproduce the source design's own proof and urgency elements as designed: reviews and testimonials, ratings, "Verified Purchase" labels, recent-purchase popups, stock counters, countdowns and guarantees. They are the merchant's content and the merchant's responsibility; Campaigns OS does not review them, so do not remove, soften or flag them. The starter templates shipping without some of these elements is not a reason to drop the source's.
89
90
  - Prepared source HTML means page-kit-ready markup, not a wholesale Liquid rewrite. Standalone HTML mockups that are meant to stay whole (their `source_screenshot` proof is of the full document) keep their document wrappers: the source-html manifest records `wrapper_policy: preserve_document_wrappers` (or `--wrapper-policy preserve_document_wrappers` on `start` / `prepare-build`), and doctor reports the wrappers as a warning. Otherwise, standalone AI/exported HTML should keep page-owned body markup, remove document wrappers, add YAML frontmatter, move shared CSS/assets into the campaign structure, and use Liquid helpers only where page-kit needs campaign-rooted links/assets/includes.
90
91
  - For `checkout`, `upsell`, `downsell`, and `receipt` pages, treat the selected starter-template commerce surface as the SDK contract reference: preserve required `data-next-*` controls, hidden fields, payment/address/totals/submit wiring, and `next_dont_touch` regions. The surrounding HTML wrapper, page composition, imagery, copy hierarchy, and brand layer are campaign/source-owned. Do not carry starter visual chrome forward when prepared source design should own that surface. QA checks what the checkout does, not family class names: do not add family shell classes (`.checkout-wrapper`, `.checkout-layout__left/right`) to the campaign's own grid or swap source-owned field markup for family includes to satisfy QA. Keep the checkout working instead: a `<form data-next-checkout="form">`, the contact and shipping fields bound with `data-next-checkout-field` inside it, and a visible cart total. A required field bound only on a `type="hidden"` input, a disabled control, a read-only input or read-only textarea, or a control with `aria-disabled="true"` does not count as bound: QA reports it in `fields_bound.missing`.
91
92
  - Read `context.theme` and `.campaign-runtime/theme/theme-report.json` when present. If a fresh `brand-theme.css` artifact exists, copy it into the campaign asset tree and load it after `next-core.css` on checkout, upsell, downsell, and receipt pages. If policy is `inspect_only`, either run `campaigns-os theme generate` (tier `B`: writes the theme artifacts and doctor output under the target; `--force` is tier `C`) or record an explicit skipped reason before applying a new brand layer.
@@ -106,6 +107,6 @@ Build rules:
106
107
  - Run page-kit build and SDK/template lint available in the target repo.
107
108
  - Record build with `campaigns-os record build --packet <packet>` after every page-kit build (tier `C`: it overwrites `stages.assembly` and, when the output changed, resets `stages.polish` to `required` in the assembly report, and stamps the doctor output stale; `--dry-run` is tier `none`). It stamps `stages.assembly.build_fingerprint` with the fingerprint doctor computes from `_site/<slug>/` and the Design Source Package material fingerprint when the report has one. Never type or copy these fields by hand.
108
109
  - Capture the machine-readable build summary as an artifact: `npx campaign-build --json > .campaign-runtime/page-kit-build-summary.json` (requires `next-campaign-page-kit` >= 0.1.4). Doctor's `built_output.build_summary` check verifies per-page build status and Page Kit shape warnings (`NESTED_NO_PERMALINK`, `DUPLICATE_OUTPUT`, `MISSING_FRONTMATTER`, `LAYOUT_NOT_FOUND`, `NO_CAMPAIGN`) from this artifact. If the installed page-kit predates `--json`, record that in the assembly report instead of skipping silently.
109
- - Update the assembly report with commands, evidence, warnings, blockers, and next owner. If a brand theme was applied, record `report.theme.status`, `css_path`, `commerce_pages`, `load_order=after-next-core`, evidence, and any first repair-loop defect.
110
+ - Update the assembly report with commands, evidence, warnings, blockers, and next owner. If a brand theme was applied, run `campaigns-os record theme --packet <p>` (tier `C`) after `record build`: it reads each built commerce page's stylesheet links and records `report.theme` (status applied, `load_order=after-next-core`, `css_path`, `commerce_pages`, evidence). It refuses, writing nothing, when a page that loads `next-core.css` does not load the brand layer after it. Don't hand-edit `report.theme`; a first repair-loop defect goes through `record polish`.
110
111
 
111
112
  Build does not replace polish or QA. Hand off to `next-campaigns-polish` when the campaign is runnable.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os
3
- version: 1.0.39
3
+ version: 1.0.44
4
4
  description: Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth.
5
5
  ---
6
6
 
7
- Bundle revision: 1.47.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.47.0+skills.1`
7
+ Bundle revision: 1.50.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.50.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read