@nextcommerce/campaigns-os 1.47.0 → 1.48.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 (44) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +30 -3
  3. package/agents/claude/CLAUDE.md +2 -0
  4. package/agents/codex/AGENTS.md +1 -0
  5. package/agents/copilot/copilot-instructions.md +1 -0
  6. package/agents/cursor/campaigns-os.mdc +1 -1
  7. package/compatibility.json +1 -1
  8. package/contracts/commerce-surface-catalog.json +17 -17
  9. package/contracts/release-ledger.json +465 -0
  10. package/contracts/supported-surface.json +1 -1
  11. package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
  12. package/docs/build-packet.md +1 -1
  13. package/docs/campaign-build-brief.md +25 -28
  14. package/docs/local-setup.md +7 -2
  15. package/docs/orientation-contract-reference.md +1 -1
  16. package/docs/qa-and-test-orders.md +49 -2
  17. package/docs/runtime-readiness.md +1 -1
  18. package/docs/skills-revision.md +10 -10
  19. package/package.json +1 -1
  20. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  21. package/skills/campaign-readback-classification/SKILL.md +3 -3
  22. package/skills/campaign-run-evidence/SKILL.md +9 -4
  23. package/skills/contribution-intake/SKILL.md +3 -3
  24. package/skills/next-campaigns-build/SKILL.md +4 -3
  25. package/skills/next-campaigns-os/SKILL.md +3 -3
  26. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  27. package/skills/next-campaigns-polish/SKILL.md +4 -4
  28. package/skills/next-campaigns-qa/SKILL.md +3 -3
  29. package/skills.json +10 -10
  30. package/src/cli.mjs +6 -3
  31. package/src/content-residue.mjs +18 -90
  32. package/src/doctor/checks.mjs +21 -29
  33. package/src/doctor/inspect.mjs +13 -3
  34. package/src/doctor/next-step.mjs +4 -0
  35. package/src/gate-actions.mjs +8 -0
  36. package/src/local-preview-policy.mjs +92 -0
  37. package/src/page-kit-sdk-version.mjs +8 -1
  38. package/src/polish-node.mjs +26 -2
  39. package/src/progress-node.mjs +5 -1
  40. package/src/qa-binding-evidence.mjs +1 -1
  41. package/src/qa-browser.mjs +62 -13
  42. package/src/qa-node.mjs +33 -4
  43. package/src/readback.mjs +19 -10
  44. package/src/source-prep.mjs +1 -1
@@ -96,6 +96,32 @@ recapture. A hosted target (`netlify`, `cloudflare-pages`, …) is unaffected:
96
96
  its build stage renders production as before and `page-kit parity` refuses the
97
97
  packet (`local_proof.parity.not_local_serve`).
98
98
 
99
+ ### Missing evidence carried forward on the local preview
100
+
101
+ On a `local-serve` packet served from a loopback host (`localhost`,
102
+ `127.0.0.1`, `[::1]`, for both `deploy.preview_url` and `--base-url`), some
103
+ missing evidence is carried forward as a warning instead of blocking the
104
+ loop. The campaign must still prove its commerce there: store and campaign
105
+ binding, routes, SDK loading, prices and a typed-card order. Only these
106
+ checks are carried forward:
107
+
108
+ | Check | When |
109
+ | --- | --- |
110
+ | `polish.evidence_missing`, `polish.report_missing` | Polish was never recorded for this build. |
111
+ | `polish.hidden_eager_media.no_capturable_routes` | Every mapped page is template stock (`skip_reason`), so polish capture has no design route to capture. |
112
+ | `polish.hidden_eager_media.capture_malformed` | Only when no page-load capture was recorded at all. |
113
+ | Template-residue severity | With `theme_gate.nothing_generatable`, the starter template is the design, so residue findings are warnings rather than blockers. |
114
+
115
+ A carried-forward gate has status `carried_forward`. Doctor reports it as a
116
+ warning starting "Carried forward on the local preview"; `next` moves past
117
+ polish to deploy and QA; QA records it as a `warn` row, so the verdict is at
118
+ best `ready_with_exceptions`. The evidence is reported as missing, never as
119
+ passed. Any other check keeps its meaning. A hosted preview or production
120
+ packet, and a `local-serve` packet served from any other host, gets the strict
121
+ gates. `record polish` and the waiver commands also keep them strict. Progress
122
+ snapshots record a carried-forward gate as `not_applicable`, because their
123
+ schema has no carried-forward state.
124
+
99
125
  ## Resolve
100
126
 
101
127
  Use resolve before a full run:
@@ -482,7 +508,15 @@ the three checks and each `evidence.checks[]` entry carries `kind`
482
508
  (`family_shell` or `sdk_wiring`).
483
509
  The upsell and checkout bundle price checks count the SDK's
484
510
  `[data-next-bundle-display*='price']` alongside the contract's price rows; a
485
- hidden, zero-size or empty bundle-display node does not count.
511
+ hidden, zero-size or empty bundle-display node does not count. A checkout that
512
+ shows no price because QA opened it directly, with an empty SDK cart and no
513
+ package selection of its own, is `skipped` rather than failed:
514
+ `pricing.checkout_price_visible` records `cart_count` and
515
+ `checkout_selection_surface` and says so. That checkout's cart is filled on
516
+ an earlier page, for example by a landing link carrying `forcePackageId`, and
517
+ the test order enters it from there. A checkout with its own package
518
+ selection, or a filled cart, still fails when no price shows. If the cart
519
+ probe itself fails, the row stays failed and records `empty_cart_probe_error`.
486
520
  Promoted template families must also have
487
521
  `contracts/template-brand-contract.<family>.v0.json`; QA emits a blocker if the
488
522
  selected family is missing its brand/residue/pricing contract instead of
@@ -1749,6 +1783,17 @@ shared real inbox rather than a synthetic one). When neither is set, the runner
1749
1783
  falls back to a single stable synthetic address — still one reused customer, but
1750
1784
  not deliverable.
1751
1785
 
1786
+ Reusing one customer has one cost. The platform refuses an order whose customer,
1787
+ items and total match one it accepted or is still processing in the last 30
1788
+ minutes ("Duplicate order detected, order not created"). A successful test order
1789
+ does not hold that window, so the paths of one run do not collide. Two QA runs
1790
+ against the same campaign at once do, and so can a rerun soon after an attempt
1791
+ that died mid-submit. QA reports it on the path's `browser-test-order` row as
1792
+ `order create rejected: HTTP 400: Duplicate order detected …` followed by
1793
+ `duplicate_order` and the remedy: re-run with a different `--test-email-prefix`
1794
+ (or `--test-email`), or wait. The shipping address is not part of the match, so
1795
+ changing `--test-address1` does not help.
1796
+
1752
1797
  The browser driver intentionally behaves like a user:
1753
1798
 
1754
1799
  - package selection uses rendered `[data-next-package-id]` controls when
@@ -1919,7 +1964,9 @@ ASCII whitespace and compared case-insensitively as the browser does; classic
1919
1964
  `built_output.script_syntax` in [the Build Packet doc](build-packet.md).
1920
1965
 
1921
1966
  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
1967
+ loader or index are inspected only on the page's origin. Three Campaign Cart
1968
+ paths count as the SDK and are never fetched: `dist/loader.js` (the one the
1969
+ starter templates load), `dist/index.js` and `public/loader.js`. Each page admits at most
1923
1970
  6 such references; each run fetches at most 24 distinct URLs (deduplicated),
1924
1971
  256 KiB per response and 6 MiB aggregate, 5 seconds per request including body
1925
1972
  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.48.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.48.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.48.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.48.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.48.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.48.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.47.0+skills.1",
95
+ "on_disk": "1.48.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.47.0+skills.1)"
97
+ "message": "match (1.48.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.48.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.48.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.48.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.48.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-lifecycle-orientation
3
- version: 1.0.19
3
+ version: 1.0.22
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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.22
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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.22
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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.22
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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.27
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os
3
- version: 1.0.39
3
+ version: 1.0.42
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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-os-setup
3
- version: 2.0.22
3
+ version: 2.0.25
4
4
  description: Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly installed as next-campaigns-setup; renamed 2026-08 to stop colliding with the published NextCommerceCo/skills scaffolder of that name.
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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-polish
3
- version: 1.1.23
3
+ version: 1.1.26
4
4
  description: Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign.
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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
@@ -56,7 +56,7 @@ Responsibilities:
56
56
  - Compare prepared source design against built campaign pages.
57
57
  - Scan the prepared source assets for brand marks such as `logo*.png`, `logo*.svg`, and obvious header/logo images before leaving starter-template logos in place.
58
58
  - Scan **visible** rendered text inside the family's content/commerce surfaces (the selectors enumerated in `contracts/template-brand-contract.<family>.v0.json`) for placeholder/residue copy the build should have replaced — the *literal starter defaults*: lorem-ipsum, the unmodified starter headings (`Product Name` / `Package Title` / `Your headline`), `[VERIFY …]` author notes, `TODO` markers. Match the literal starter strings, not any authored copy that merely contains those words, and skip `<script>` / `<style>` / JSON-LD / `data-*` attributes. Treat a surviving literal starter default on a content/commerce surface as a polish blocker — replace it from the prepared source / CampaignSpec; do not draft substitute copy (if the design's authored copy is genuinely missing, flag it rather than invent it). This is *copy* residue, complementary to the computed-style / asset residue gated by `next-campaigns-qa` + the brand contract — not a duplicate of it.
59
- - Flag template *defaults* left where they disagree with the prepared design — e.g. the same benefit icon repeated across a grid *when the design uses distinct icons*, or a guarantee badge/term that disagrees with the design — as polish defects, not just logos. Judge against the prepared source, not taste: copy or imagery the source/CampaignSpec does not supply (e.g. a placeholder testimonial name/quote/role) is residue; "looks generic" on its own is not.
59
+ - Flag template *defaults* left where they disagree with the prepared design — e.g. the same benefit icon repeated across a grid *when the design uses distinct icons*, or a guarantee badge/term that disagrees with the design — as polish defects, not just logos. Judge against the prepared source, not taste: copy or imagery the source/CampaignSpec does not supply (e.g. a placeholder testimonial name/quote/role) is residue; "looks generic" on its own is not. Copy the source does supply, including its proof and urgency elements (reviews, "Verified Purchase" labels, recent-purchase popups, stock counters, countdowns, guarantees), is the merchant's content: keep it as designed and do not record it as an issue or an unconfirmed claim.
60
60
  - **Brand-bleed (cloned-source de-brand) pass.** When a campaign is cloned from a proven sibling, the sibling's brand defaults ride along. Inspect the built pages and assets for residual cross-brand bleed and clear it before recording: (1) a residual promo/sale banner or coupon code/copy from the source campaign (including a baked-in *fake* code); (2) a prior-campaign / sibling favicon left in place; (3) scaffold or non-design fonts the design did not specify (e.g. starter `Plus Jakarta`); (4) hardcoded non-token colors — any brand color literal that should be a token, such as next-core's `#C670FE` "Most Popular" pill. Clear each from the prepared source / CampaignSpec and brand theme (tokens, not literals); flag — do not invent — anything the design genuinely doesn't supply. Treat surviving bleed as a polish blocker. This complements the favicon/logo and copy-residue checks above; it is the cross-brand contamination angle, not a duplicate.
61
61
  - Read the assembly report decisions before polishing. Do not reintroduce source-HTML elements that build intentionally dropped because CampaignSpec/API did not support them, such as unavailable payment methods.
62
62
  - **Remove or rename a contract-listed payment-chrome asset; never edit one in place.** The assets named in `contracts/template-brand-contract.<family>.v0.json` under `default_residue.payment_chrome.assets` are keyed by QA on the *referenced basename*, not on their contents. Stripping the unsupported marks from inside a shared file such as `upsell-payment-logos.svg` leaves the reference in place, so QA reports residue for an asset that no longer carries any — and on 2026-09-06 the repair loop's remedy for that report deleted a cards-only trust strip that was correct. Delete the asset, or write a new one under a new name and repoint the reference. QA downgrades an edited-in-place asset to `manual_review` rather than a blocker, but that is a safety net for a mistake, not the supported way to do this.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-qa
3
- version: 1.3.23
3
+ version: 1.3.26
4
4
  description: Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof.
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.48.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.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
package/skills.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "campaigns-os-skills",
3
3
  "description": "Skills bundled with Campaigns OS. `skills.sh` installs these into the shared agent skill directories (~/.claude/skills, ~/.codex/skills), so each one is a versioned package: bump the version whenever a package changes. `bundle_revision` identifies the bundle as a whole — `<package version>+skills.<n>`, where the prefix is this package's version and `<n>` counts the skill-text revisions published against it. It advances whenever ANY bundled skill changes, every SKILL.md states it on its first body line, and `campaigns-os tooling status --skills-revision <value>` compares the value an agent read from a skill against the bundle on disk. See docs/skills-revision.md.",
4
- "bundle_revision": "1.47.0+skills.1",
4
+ "bundle_revision": "1.48.0+skills.1",
5
5
  "homepage": "https://github.com/NextCommerceCo/campaigns-os",
6
6
  "retired_skills": [
7
7
  {
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "id": "next-campaigns-os",
18
18
  "name": "Campaigns OS Lifecycle",
19
- "version": "1.0.39",
19
+ "version": "1.0.42",
20
20
  "path": "skills/next-campaigns-os/SKILL.md",
21
21
  "domain": "campaigns",
22
22
  "description": "Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth."
@@ -24,7 +24,7 @@
24
24
  {
25
25
  "id": "next-campaigns-os-setup",
26
26
  "name": "Campaigns OS Setup",
27
- "version": "2.0.22",
27
+ "version": "2.0.25",
28
28
  "path": "skills/next-campaigns-os-setup/SKILL.md",
29
29
  "domain": "campaigns",
30
30
  "description": "Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly next-campaigns-setup; renamed to release that name to the published NextCommerceCo/skills scaffolder."
@@ -32,7 +32,7 @@
32
32
  {
33
33
  "id": "next-campaigns-build",
34
34
  "name": "Campaign Build",
35
- "version": "1.0.24",
35
+ "version": "1.0.27",
36
36
  "path": "skills/next-campaigns-build/SKILL.md",
37
37
  "domain": "campaigns",
38
38
  "description": "Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts."
@@ -40,7 +40,7 @@
40
40
  {
41
41
  "id": "next-campaigns-polish",
42
42
  "name": "Campaign Polish",
43
- "version": "1.1.23",
43
+ "version": "1.1.26",
44
44
  "path": "skills/next-campaigns-polish/SKILL.md",
45
45
  "domain": "campaigns",
46
46
  "description": "Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign."
@@ -48,7 +48,7 @@
48
48
  {
49
49
  "id": "next-campaigns-qa",
50
50
  "name": "Campaign QA",
51
- "version": "1.3.23",
51
+ "version": "1.3.26",
52
52
  "path": "skills/next-campaigns-qa/SKILL.md",
53
53
  "domain": "campaigns",
54
54
  "description": "Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof."
@@ -56,7 +56,7 @@
56
56
  {
57
57
  "id": "campaign-lifecycle-orientation",
58
58
  "name": "Campaign Lifecycle Orientation",
59
- "version": "1.0.19",
59
+ "version": "1.0.22",
60
60
  "path": "skills/campaign-lifecycle-orientation/SKILL.md",
61
61
  "domain": "campaigns",
62
62
  "description": "Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing their state: the shared vocabulary, doctor as a gate, the stage record, and the store-theme/Page Kit two-worlds trap."
@@ -64,7 +64,7 @@
64
64
  {
65
65
  "id": "campaign-run-evidence",
66
66
  "name": "Campaign Run Evidence",
67
- "version": "1.0.19",
67
+ "version": "1.0.22",
68
68
  "path": "skills/campaign-run-evidence/SKILL.md",
69
69
  "domain": "campaigns",
70
70
  "description": "Interpret existing doctor, QA verdict and proof-depth evidence without claiming more proof than the artifacts contain, and keep funnel proof apart from merchant launch readiness."
@@ -72,7 +72,7 @@
72
72
  {
73
73
  "id": "campaign-readback-classification",
74
74
  "name": "Campaign Readback Classification",
75
- "version": "1.0.19",
75
+ "version": "1.0.22",
76
76
  "path": "skills/campaign-readback-classification/SKILL.md",
77
77
  "domain": "campaigns",
78
78
  "description": "Classify a selected campaign from the campaigns-os-readback/v2 fields (artifacts, staleness.stale_keys, clean, doctor, divergences, skip_cascades) and write a read-only handoff without turning diagnosis into permission."
@@ -80,7 +80,7 @@
80
80
  {
81
81
  "id": "contribution-intake",
82
82
  "name": "Contribution Intake",
83
- "version": "1.0.19",
83
+ "version": "1.0.22",
84
84
  "path": "skills/contribution-intake/SKILL.md",
85
85
  "domain": "campaigns",
86
86
  "description": "Turn a suggestion about the agent surface into a classified, evidence-checked and redacted proposal, filed on this repository tracker only with attended approval."
package/src/cli.mjs CHANGED
@@ -296,15 +296,18 @@ Usage:
296
296
  campaigns-os start (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
297
297
  [--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
298
298
  [--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
299
- [--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>] [--no-run-session] [--force] # --force overwrites an assembly report that carries stage evidence (destructive; prints the cleared stage keys) and regenerates a stale Design Source Package an earlier intake synthesized and nobody changed
299
+ [--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
300
+ [--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # --force overwrites an assembly report that carries stage evidence (destructive; prints the cleared stage keys) and regenerates a stale Design Source Package an earlier intake synthesized and nobody changed
300
301
  campaigns-os prepare-build (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
301
302
  [--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
302
303
  [--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
303
- [--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>] [--no-run-session] [--force]
304
+ [--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
305
+ [--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force]
304
306
  campaigns-os build (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
305
307
  [--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
306
308
  [--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
307
- [--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>] [--no-run-session] [--force] # intake alias for prepare-build + doctor
309
+ [--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
310
+ [--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # intake alias for prepare-build + doctor
308
311
  campaigns-os doctor --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--strip-paths] [--write] [--no-write] [--doctor-out <path>] [--proxy-base <url>] [--no-live-refs] [--json] # inspection by default; --doctor-out requires --write; --no-write wins. When the packet's built _site/<route>/ exists and a public Campaigns API key resolves (packet, its local CampaignSpec, or the declared campaign-key env var), doctor makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check each built page's shipping and package refs against the live campaign; --proxy-base overrides the canonical proxy (https, or a loopback host over http). --no-live-refs skips the read and records not_run with reason disabled. No key, no built page, or a failed read records derived.live_campaign_refs as not_run with its reason. Only doctor and qa run make this read; other commands that run doctor record not_read
309
312
  campaigns-os doctor --built <page-kit-target-repo> --family <family> [--slug <slug>] [--base-url <url>] [--emit-packet [path]] [--json] # L7: doctor a built _site/ with no Build Packet
310
313
  campaigns-os bundle check --packet <campaign-runtime.build.json> [--require-qa] [--json] # validate the canonical migration/readback JSON bundle; never substitutes markdown
@@ -1,23 +1,22 @@
1
1
  // Rendered-output content-residue scan + proof-attestation gate.
2
2
  //
3
3
  // Scans BUILT campaign output (_site HTML), not frontmatter: layout- or
4
- // script-rendered proof/urgency chrome only exists after the build, which is
5
- // why frontmatter-level checks missed the hardcoded rating/countdown chrome.
4
+ // script-rendered chrome only exists after the build, which is why
5
+ // frontmatter-level checks missed the hardcoded starter countdown chrome.
6
6
  //
7
- // Pattern provenance: the generic anti-pattern classes distilled from the
8
- // 2026-07 winning-campaign content audit (invented counts, fabricated
9
- // verified-buyer chrome, fictional bylines, borrowed authority, science
10
- // theater, scarcity theater, fake comparisons, unlinked press marquees).
11
- // Only GENERIC patterns and the public starter-template demo strings live
12
- // here; merchant-specific residue fingerprints are deliberately not carried
13
- // in this public package.
7
+ // What it looks for is template residue, never the merchant's own copy: the
8
+ // public starter-template demo strings, bracket-style demo stubs, and the
9
+ // literal needs-merchant-input marker. Proof and urgency content the merchant
10
+ // supplies (reviews, ratings, "Verified Purchase" labels, stock counters,
11
+ // countdowns) is the merchant's responsibility and is not scanned. No
12
+ // merchant-specific fingerprints are carried in this public package.
14
13
  //
15
- // Posture (fail closed, two tiers):
16
- // - hard: the literal needs-merchant-input marker, and urgency chrome
17
- // rendered without verified offer urgency — blockers (collect-inputs).
18
- // - review: anti-pattern hits and demo/placeholder residue — warnings that
19
- // feed the review/attestation queue; a hit means "remove or demand
20
- // brief/source evidence", never "make it more plausible".
14
+ // Posture (two tiers):
15
+ // - hard: the literal needs-merchant-input marker, and starter countdown
16
+ // chrome on a brief-backed build whose brief does not verify the offer
17
+ // urgency — blockers (collect-inputs).
18
+ // - review: demo/placeholder residue — warnings; a hit means an unreplaced
19
+ // demo slot or a stale template.
21
20
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
22
21
  import { join, relative } from "node:path";
23
22
 
@@ -55,63 +54,6 @@ export const DEMO_RESIDUE_TERMS = Object.freeze([
55
54
  "Sell-Out Risk: High",
56
55
  ]);
57
56
 
58
- // Generic anti-pattern classes (review tier). Each id is stable so the
59
- // attestation/review UX can key on it.
60
- export const CONTENT_ANTI_PATTERNS = Object.freeze([
61
- {
62
- id: "invented_counts",
63
- antiPattern: 1,
64
- rule: "Counts/ratings/percent-recommend claims require a real, brief-sourced basis.",
65
- // Count branch requires a proof-scaled number (comma groups, 4+ digits, or
66
- // a trailing +) so ordinary commerce quantities ("Choose 3 pairs and
67
- // save") never enter the review queue; "pairs"/"sold" dropped as nouns
68
- // for the same reason. Small invented counts are a knowingly accepted gap.
69
- regex: /\b(?:\d{1,3}(?:,\d{3})+|\d{4,}|\d+\+)\s+(?:reviews?|ratings?|customers?|users?|famil(?:y|ies)|wearers?|people)\b|\b(?:4\.[5-9]|5\.0)\s*(?:\/\s*5|stars?)|\b(?:9[0-9]|100)%\b[^<]{0,60}\b(?:recommend|reported|said|would)\b/i,
70
- },
71
- {
72
- id: "verified_buyer_chrome",
73
- antiPattern: 2,
74
- rule: "'Verified' labels must resolve to a real approved review source.",
75
- regex: /Verified\s+(?:Buyer|Customer|Purchase)|What\s+(?:Our\s+)?Customers\s+(?:Think|Say)|Real\s+(?:People|Customers)[^<]{0,20}Real\s+(?:Results|Relief)|5[- ]Star\s+Review/i,
76
- },
77
- {
78
- id: "byline_persona",
79
- antiPattern: 3,
80
- rule: "Advertorial identities must be real, brief-supplied, and authorized.",
81
- regex: /Mom\s+of\s+Two|Consumer\s+Report|Review\s+Team|Wellness\s+Educator|Licensed\s+(?:Physiotherapist|Professional)/i,
82
- },
83
- {
84
- id: "borrowed_authority",
85
- antiPattern: 4,
86
- rule: "Expert/clinician/institution references require name, credential, permission, and source.",
87
- regex: /\bDr\.\s+[A-Z]|\bM\.?D\.?\b|doctor[- ]recommended|clinically\s+(?:recognized|recommended)|expert\s+(?:says|recommends)/i,
88
- },
89
- {
90
- id: "press_marquee",
91
- antiPattern: 8,
92
- rule: "Press mentions require a brief-supplied working URL for the exact merchant and product.",
93
- regex: /As\s+Seen\s+(?:On|In)|Featured\s+(?:On|In)/i,
94
- },
95
- {
96
- id: "science_theater",
97
- antiPattern: 9,
98
- rule: "Study/clinical/certification claims require citation metadata and approved wording.",
99
- regex: /peer[- ]reviewed|science[- ]backed|backed\s+by\s+science|stud(?:y|ies)\s+(?:show|prove|confirm)|researchers\s+found|clinically\s+(?:proven|shown|tested)|NASA[- ]developed/i,
100
- },
101
- {
102
- id: "scarcity_theater",
103
- antiPattern: 10,
104
- rule: "Urgency renders only from a real, approved promotion window or live inventory source.",
105
- regex: /ENDS\s+AT\s+MIDNIGHT|Offer\s+Expires|Deal\s+Ending|Only\s+\d+\s+(?:Units\s+)?Left|Stock\s+(?:Levels?\s+)?Low|\d+%\s+Sold|Sell[- ]?Out\s+Risk|supplies\s+are\s+limited/i,
106
- },
107
- {
108
- id: "fake_comparison",
109
- antiPattern: 11,
110
- rule: "Tested-N/showdown framing requires a brief-supplied comparison matrix and test record.",
111
- regex: /(?:we\s+)?tested\s+\d+\s+(?:contenders|products|devices|gloves|combinations|brands)|only\s+one\s+(?:survived|worked|stood)|Competitor\s+[12]\b/i,
112
- },
113
- ]);
114
-
115
57
  const ENTITIES = new Map([
116
58
  ["&amp;", "&"], ["&lt;", "<"], ["&gt;", ">"], ["&quot;", '"'],
117
59
  ["&#39;", "'"], ["&apos;", "'"], ["&nbsp;", " "], ["&#8217;", "’"], ["&#8212;", "—"],
@@ -168,13 +110,12 @@ function excerptAt(html, index, span = 80) {
168
110
  }
169
111
 
170
112
  // Pure scan of one rendered HTML document. Returns { hard: [], review: [] };
171
- // each finding: { id, tier, rule?, excerpt }. Hard checks run on the markup
172
- // view (attributes count, comments/scripts do not); review checks run on the
173
- // visible-text view for precision, except exact-string demo terms and bracket
174
- // stubs which run on markup so attribute residue is still caught.
113
+ // each finding: { id, tier, rule?, excerpt }. The hard checks and the demo
114
+ // terms run on the markup view (attributes count, comments and script/style
115
+ // bodies do not), so attribute residue is still caught; bracket stubs run on
116
+ // the same view with tags stripped, so CSS attribute selectors never match.
175
117
  export function scanRenderedHtml(html, { urgencyVerified = false } = {}) {
176
118
  const markup = markupView(typeof html === "string" ? html : "");
177
- const text = visibleText(typeof html === "string" ? html : "");
178
119
  const hard = [];
179
120
  const review = [];
180
121
 
@@ -205,19 +146,6 @@ export function scanRenderedHtml(html, { urgencyVerified = false } = {}) {
205
146
  }
206
147
  }
207
148
 
208
- for (const pattern of CONTENT_ANTI_PATTERNS) {
209
- if (pattern.id === "scarcity_theater" && urgencyVerified) continue;
210
- const match = pattern.regex.exec(text);
211
- if (match) {
212
- review.push({
213
- id: pattern.id,
214
- tier: "review",
215
- antiPattern: pattern.antiPattern,
216
- rule: pattern.rule,
217
- excerpt: excerptAt(text, match.index),
218
- });
219
- }
220
- }
221
149
  return { hard, review };
222
150
  }
223
151