@nextcommerce/campaigns-os 1.54.0 → 1.57.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 (111) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/CONTEXT.md +19 -5
  3. package/README.md +26 -12
  4. package/campaign-spec/README.md +15 -1
  5. package/campaign-spec/dist/checkout-flow.d.ts +90 -0
  6. package/campaign-spec/dist/checkout-flow.js +229 -0
  7. package/campaign-spec/dist/index.d.ts +1 -0
  8. package/campaign-spec/dist/index.js +6 -1
  9. package/campaign-spec/dist/normalize.d.ts +3 -2
  10. package/campaign-spec/dist/normalize.js +7 -5
  11. package/campaign-spec/dist/routing.d.ts +6 -3
  12. package/campaign-spec/dist/routing.js +6 -3
  13. package/campaign-spec/dist/rules/checkout-forward-target.d.ts +17 -0
  14. package/campaign-spec/dist/rules/checkout-forward-target.js +48 -0
  15. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.d.ts +12 -0
  16. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.js +37 -0
  17. package/campaign-spec/dist/rules/exit-intent-validation.js +2 -1
  18. package/campaign-spec/dist/rules/index.d.ts +4 -0
  19. package/campaign-spec/dist/rules/index.js +12 -0
  20. package/campaign-spec/dist/rules/one-checkout-per-path.d.ts +25 -0
  21. package/campaign-spec/dist/rules/one-checkout-per-path.js +84 -0
  22. package/campaign-spec/dist/rules/pre-payment-forward-target.d.ts +12 -0
  23. package/campaign-spec/dist/rules/pre-payment-forward-target.js +42 -0
  24. package/campaign-spec/dist/rules/promo-code-input-validation.js +2 -1
  25. package/campaign-spec/dist/rules/schema-version.d.ts +9 -4
  26. package/campaign-spec/dist/rules/schema-version.js +10 -5
  27. package/campaign-spec/dist/types.d.ts +20 -3
  28. package/campaign-spec/dist/types.js +3 -2
  29. package/compatibility.json +2 -2
  30. package/contracts/fixtures/runtime-recipe/accept/current.json +6 -1
  31. package/contracts/fixtures/runtime-recipe/accept/minimal.json +6 -1
  32. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  33. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +6 -1
  34. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +6 -1
  35. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +6 -1
  36. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +6 -1
  37. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +6 -1
  38. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +1 -1
  39. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +6 -1
  40. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +6 -1
  41. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +6 -1
  42. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +5 -0
  43. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +6 -1
  44. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +6 -1
  45. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +6 -1
  46. package/contracts/release-ledger.json +1146 -0
  47. package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
  48. package/contracts/supported-surface.json +9 -5
  49. package/docs/brand-theme-bridge.md +10 -0
  50. package/docs/build-packet.md +86 -26
  51. package/docs/campaigns-os-build-flow.md +1 -1
  52. package/docs/effects.md +2 -5
  53. package/docs/local-setup.md +1 -1
  54. package/docs/orientation-contract-reference.md +1 -1
  55. package/docs/qa-and-test-orders.md +133 -29
  56. package/docs/runtime-readiness.md +8 -3
  57. package/docs/skills-revision.md +10 -10
  58. package/docs/supported-surface.md +1 -1
  59. package/docs/versioning.md +4 -1
  60. package/docs/workflow-findings-sidecar.md +57 -2
  61. package/package.json +4 -1
  62. package/schemas/campaign-runtime-build-context.v0.schema.json +3 -1
  63. package/schemas/campaign-runtime-build-packet.v0.schema.json +3 -1
  64. package/schemas/campaign-spec.v5.schema.json +586 -0
  65. package/schemas/campaigns-os-run-record.v0.schema.json +20 -15
  66. package/skills/campaign-lifecycle-orientation/SKILL.md +10 -8
  67. package/skills/campaign-readback-classification/SKILL.md +3 -3
  68. package/skills/campaign-run-evidence/SKILL.md +3 -3
  69. package/skills/contribution-intake/SKILL.md +3 -3
  70. package/skills/next-campaigns-build/SKILL.md +6 -4
  71. package/skills/next-campaigns-os/SKILL.md +3 -3
  72. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  73. package/skills/next-campaigns-polish/SKILL.md +21 -6
  74. package/skills/next-campaigns-qa/SKILL.md +5 -4
  75. package/skills.json +10 -10
  76. package/src/adapter-decision-contract.mjs +27 -13
  77. package/src/brand-theme.mjs +44 -8
  78. package/src/build-brief.mjs +14 -5
  79. package/src/campaign-identity.mjs +33 -4
  80. package/src/cli-helpers.mjs +129 -0
  81. package/src/cli.mjs +156 -111
  82. package/src/commercial-journey.mjs +25 -2
  83. package/src/consent.mjs +3 -2
  84. package/src/design-source-package.mjs +1 -1
  85. package/src/design-source-publication.mjs +8 -8
  86. package/src/deviation.mjs +2 -1
  87. package/src/doctor/checks.mjs +212 -19
  88. package/src/doctor/inspect.mjs +4 -2
  89. package/src/doctor/next-step.mjs +46 -16
  90. package/src/doctor-sidecar.mjs +5 -3
  91. package/src/gate-actions.mjs +21 -1
  92. package/src/input-refresh.mjs +6 -6
  93. package/src/invocation.mjs +4 -3
  94. package/src/lifecycle.mjs +88 -6
  95. package/src/local-preview-policy.mjs +116 -12
  96. package/src/login.mjs +2 -1
  97. package/src/polish-browser.mjs +8 -2
  98. package/src/polish-node.mjs +9 -4
  99. package/src/polish-readability.mjs +2 -2
  100. package/src/qa-browser.mjs +741 -120
  101. package/src/qa-cart-entry.mjs +107 -20
  102. package/src/qa-node.mjs +179 -72
  103. package/src/qa-test-order-topology.mjs +118 -1
  104. package/src/qa-verdict-discovery.mjs +21 -4
  105. package/src/qa-verdict-publish.mjs +2 -1
  106. package/src/run-record.mjs +46 -34
  107. package/src/runtime-state-ignore.mjs +7 -5
  108. package/src/sdk-markup.mjs +41 -1
  109. package/src/source-html-intake.mjs +46 -4
  110. package/src/stage-ledger.mjs +18 -4
  111. package/src/stage-record.mjs +53 -16
package/CHANGELOG.md CHANGED
@@ -2,6 +2,154 @@
2
2
 
3
3
  Notable supported-surface changes are recorded here.
4
4
 
5
+ ## [1.57.0] - 2026-10-08
6
+
7
+ ### Changed
8
+
9
+ - Publishes the untagged `1.56.0` changes: CampaignSpec 5.0, checkout steps, and the updated build and QA contracts.
10
+ - Publishes `1.56.0+agent.1`: `--version` and `-v`, and the Git ignore entry for runtime progress records.
11
+ - Publishes `1.56.0+agent.2`: hosted all-template previews carry missing Polish and page-load evidence forward as warnings.
12
+ - Publishes `1.56.0+agent.3`: doctor and QA find an entry page at Page Kit's emitted route when the declared root is absent.
13
+ - Publishes `1.56.0+agent.5`: doctor warns when a variant and quantity checkout does not fit the selected template family.
14
+ - Publishes `1.56.0+agent.6`: committed handoff artifacts retain repository-relative paths.
15
+
16
+ ## [1.56.0+agent.6] - 2026-10-08
17
+
18
+ ### Fixed
19
+
20
+ Committed Build Packet and `.campaign-runtime/` handoff artifacts now retain filesystem paths in relative form after `doctor --write`, `next`, and stage updates. Packet input fields resolve from the packet file's directory, output paths from the target repository, source page paths from the source root, package references from their context or report file, and normalized brief input paths from the brief file. Other committed sidecar paths and stored commands resolve from the target repository root. Outside inputs use ordinary relative paths from those bases, including source manifest paths in decision evidence. Machine-local ignored run state keeps its existing path behavior. `doctor --strip-paths` remains available for older packets. QA verdict discovery resolves recorded outputs from the target root.
21
+
22
+ ## [1.56.0+agent.5] - 2026-10-08
23
+
24
+ ### Fixed
25
+
26
+ Doctor warns once per checkout path when selectable packages form a variant and quantity matrix but the selected certified template family declares one main package and no configurable variant-slot checkout surface. The matrix requires at least two variants, with at least one offered at multiple quantities. Variant identity comes from attributes shared by every selectable row carrying a quantity, or from variant names shared by those rows; rows with missing or blank quantities are ignored when detecting the matrix. SKUs, order bumps, upsells, pack-size rows, and quantity tiers of one variant do not trigger the warning. Paths follow CampaignSpec route targets, including `.html` and `page_url` routes. If any checkout path has a `select` or `checkout_step` page, every warning ranks multi-step families first. Each warning names the matching certified variant-slot families in ranked order, preferring the selected family's base. It says the family is chosen at intake with `--template-family`; after stage evidence is recorded, it explains that changing the family requires destructive intake with `--force`. The warning does not block intake or change the selected family.
27
+
28
+ ## [1.56.0+agent.3] - 2026-10-08
29
+
30
+ ### Fixed
31
+
32
+ Doctor recognizes an entry page declared at the campaign root when Page Kit emitted its recorded page file at a different route and the root output is absent. Root declarations include `page_url` empty, `/`, `/<slug>/`, and `<slug>`. A template-stock entry counts as built instead of "not built yet"; one warning names the unserved root, the emitted route, and the concrete redirect or `permalink: /<slug>/` repair. The warning uses the public route (`/` or `/<slug>/`), while built-page and previewable routes retain their relative Page Kit format. `qa resolve` prints the locally emitted entry URL under its existing slug-prefixed `base_url`; `base_url` and non-entry page URLs keep their previous resolution, including a `--base-url` subpath. An explicit non-root permalink on the entry source wins over a root output or stale file-route output for QA, and doctor warns if the root output is absent. When files named for both the page ID and page type exist, doctor and QA prefer the page ID file's permalink or built file route, then the page type file's. Without a permalink, a present root output keeps the entry at `base_url` and suppresses the warning. Neither doctor nor QA substitutes an unrelated built page.
33
+
34
+ ## [1.56.0+agent.2] - 2026-10-08
35
+
36
+ ### Changed
37
+
38
+ When every mapped page is template stock, a non-loopback hosted preview at `deploy.preview_url` carries missing Polish and hidden eager-media page-load evidence forward as warnings if its origin or campaign route path differs from `deploy.production_url` after QA composes the campaign slug onto each URL; a query or fragment alone does not qualify. Doctor and `next` move past Polish, and QA records warning rows with a verdict no better than `ready_with_exceptions`. QA against the production campaign page and hosted campaigns with a capturable design route retain strict evidence gates. If the preview URL is missing or resolves to the production campaign page, `next` names `qa policy set --packet <packet> --preview-url <url>` even when a production URL is set. Once recorded, `next` names browser and typed-card QA with `qa run --packet <packet> --base-url <preview-url> --browser --test-order common`. The hosted preview satisfies the deploy handoff without a deploy record; after a current-build QA verdict, `next` reaches closeout. `record deploy` remains local-only. The Polish prompt and actions do not suggest a capture when no design route exists. Bundled skills advance to `1.56.0+skills.2`.
39
+
40
+ ## [1.56.0+agent.1] - 2026-10-08
41
+
42
+ ### Fixed
43
+
44
+ - `campaigns-os --version` and `campaigns-os -v` print only the installed package version, with no campaign runtime writes (campaigns-os#644).
45
+ - Campaign runtime progress snapshots and their remit records are ignored by Git. A target with an older managed `.gitignore` block gains `.campaign-runtime/progress/` on its next `start`, `prepare-build`, `install-agent-context` or `run start`. A rule does not untrack files already committed; remove those with `git rm -r --cached .campaign-runtime/progress/` (campaigns-os#645).
46
+
47
+ ## [1.56.0] - 2026-10-08
48
+
49
+ ### Added
50
+
51
+ CampaignSpec 5.0 (`schemas/campaign-spec.v5.schema.json`, also a package export) makes `checkout` mean the page that takes payment and adds the `checkout_step` page type: a page with a checkout form that collects details and moves on through `next_page` without placing an order. `select` stays the bundle picker before checkout. All three render as SDK page type `checkout`. A 4.2 or 4.3 spec, including a Map saved before this release, still validates against the v4 schema and is upgraded when read: each `checkout` page whose resolved forward target is another `checkout` becomes `checkout_step`. The input is never rewritten, so spec hashes still cover the bytes as written. A campaign may hold several Checkouts, one per split-test path (campaigns-os#641).
52
+
53
+ Four new spec rules run in doctor through `spec.validation`. Each has a fixture under `campaign-spec/fixtures/`, and none of them reads page order or page names:
54
+
55
+ - `CheckoutForwardTarget` (error): a Checkout's resolved forward target is not an upsell, downsell or thank-you page.
56
+ - `PrePaymentForwardTarget` (error): a `select` or `checkout_step` page forwards straight to an upsell, downsell or thank-you page.
57
+ - `OneCheckoutPerPath` (error): a path from an entry page reaches an upsell or downsell without passing exactly one Checkout. In partial build scope, the case with no Checkout is a warning.
58
+ - `CheckoutStepReachesCheckout` (error): a `checkout_step` page is not on a path to a Checkout.
59
+
60
+ `RouteFieldIgnoredForPageType` now also warns about a `success_url` or `on_accept` on a `checkout_step`.
61
+
62
+ ### Changed
63
+
64
+ - Build: `prepare-build` projects `checkout_step` pages to Page Kit `page_type: checkout`, with the default route `checkout-step/`. Page Kit frontmatter in the Build Packet and Build Context gains two keys. `success_url` goes on every page that feeds a Checkout and holds that Checkout's post-payment destination, for the `next-success-url` meta tag the SDK reads for express orders. `step_number` goes on each `checkout_step` and is its position on its path, for the step form's `data-next-step-number`. The build prompt and the `next-campaigns-build` skill now describe the select-page handoff the templates use: the swap-mode bundle selector writes the cart, the SDK persists it, and a checkout button or link moves on. `forcePackageId` is only for a page with no selector. Step forms carry `data-next-checkout-step` and `data-next-step-number`.
65
+ - QA: test orders drive the Checkout on the tested path instead of the first page typed `checkout`. On a multi-step path the runner enters the cart on the select page or the first step. It submits each `checkout_step` form and checks the page lands on the step's declared next page, recording one `checkout_step_submitted` ladder rung per step. The order is submitted only on the Checkout. On a page with bundle cards, the cards plus a checkout button now count as a cart-entry control (`control_kind: checkout_button`), and `--select-package` clicks the matching `data-next-bundle-card`. `tiers` plans read tiers and bumps from every page of each Checkout's path. The legacy API test order takes its success URL from the Checkout. The theme gate covers `select` and `checkout_step` pages.
66
+ - Doctor and the Build Brief treat exit intent, promo-code input and order bumps on `select` and `checkout_step` pages like those on a Checkout. The payment-method residue scan also reads built `select` and `checkout_step` pages, where express buttons render. The commercial journey summary prices the cart on the checkout-flow page that carries it.
67
+ - The runtime recipe enumerates the five new `campaign-spec` sources (revision `1.0.3`).
68
+
69
+ ## [1.55.0] - 2026-10-07
70
+
71
+ ### Changed
72
+
73
+ Run Records now carry the optional `qa_verdict_publish.reason` from the QA publish decision and `closed_by` from the command path that closed the record. The reason distinguishes loopback, portal managed, consent, flag, default, and local spec decisions; the closer distinguishes `run end`, QA auto-end, stale sweep, and direct `run-record`. The unused `agent_usage` field and its `--agent-*` input flags are removed. Re-emitting an older record with `agent_usage` replaces it with a fresh record without carrying forward its prior remit state. `qa publish` posts its verdict but cannot stamp that older record as published.
74
+
75
+ QA observations in a Run Record now carry the verdict's `run_id`, the build fingerprint recorded for that verdict, and whether it differs from the build recorded in the Assembly Report. A rebuild without `record build` still reads `stale: false` when those recorded fingerprints match; doctor's `built_output.fingerprint_stale` checks the output on disk. Currency is unknown when either fingerprint is missing or the report's QA stage belongs to a different verdict.
76
+
77
+ Lifecycle stages now carry prompt `wait_ms` and, on a blocked doctor or QA exit, up to five distinct `finding_codes`. Run lifecycle separates exit 2 invocations in `needs_input_count` from other non-zero exits in `failure_count`. Legacy journal entries keep wait unknown, while new commands without prompts record zero. Duration continues to include prompt wait.
78
+
79
+ ## [1.54.0+agent.10] - 2026-10-07
80
+
81
+ ### Fixed
82
+
83
+ Generated brand themes keep sale, discount, badge, rating, star, strike, border and announcement colours out of primary and CTA roles when those names describe the selector's subject. Brand and button names keep their role. Header, navigation and button colours continue to map as they do on main, including selectors after CSS at-rules. When a design has no CTA token or button background, the existing CTA fallback warning asks the operator to confirm an accent colour that may be decorative.
84
+
85
+ ## [1.54.0+agent.9] - 2026-10-07
86
+
87
+ ### Changed
88
+
89
+ After a page-kit build, `record build --adapter-decision <key>=<value>[,<key>=<value>...]` records the choices that are true for that build on the Assembly Report. Put all pairs in one flag: a repeated flag keeps only the last list. The build prompt now asks for actual values instead of supplying a completed status. Help and the Build Packet list the allowed values. `wrapper_policy` is selected at intake with `prepare-build --wrapper-policy` or the source-html manifest option; `record build` refuses it and names those options. Doctor checks leftover document wrappers against the effective recorded conversion status, and its adapter warnings name the recording command. The packet and Build Context are unchanged by `record build`. `template_files_copied` remains object-valued proof outside the flag.
90
+
91
+ ## [1.54.0+agent.8] - 2026-10-07
92
+
93
+ ### Changed
94
+
95
+ - `next` asks for Polish when a theme waiver or changed build leaves it owed on a local preview. It asks for deploy until that stage is recorded, even if the Build Packet already has a preview URL.
96
+ - After a changed build, `next` asks for QA again when the last QA verdict names the old build, even if its recorded stage still says completed.
97
+ - `--help` commands leave lifecycle and deviation journals untouched. Re-running `start` later in a campaign is treated as re-intake rather than a deviation.
98
+
99
+ ## [1.54.0+agent.7] - 2026-10-07
100
+
101
+ ### Added
102
+
103
+ `qa run --currency <code>` runs QA in one of a campaign's currencies. The run adds `?currency=<CODE>` to each funnel's entry URL, so the HTTP fetch, the `--browser` load and the reported `entry_urls` carry it, and each test-order path's first page load (the checkout probe, or the entry page or checkout when that load comes first) carries it too. Campaign Cart keeps the currency for the rest of that tab's session, so the runner adds it to no other load (an upsell page or a receipt reload goes without it), while a later load of the entry URL still carries it because that URL is tagged. Query and tracking parameters already on the URL are kept. The code must be three letters and is upper-cased (`gbp` runs as `GBP`); anything else, including no value, an empty value or a list, is refused before any request. The flag takes one code per run; given twice, the last one is used. `qa parity --currency` is refused, and so is `--currency` with `--legacy-api-test-order`, whose direct API order would be placed in the default currency. Before this, a run could prove only the default currency, and testing another took a hand-made copy of the built site that forced the parameter. The stored verdict removes URL queries, so the currency is recorded as `evidence.currency` on the entry page's `http:<page_id>` assertion; an entry URL that does not parse gets `evidence.currency_not_applied` with the reason instead. The verdict schema is unchanged, and a run without the flag is unchanged.
104
+
105
+ ## [1.54.0+agent.6] - 2026-10-07
106
+
107
+ ### Fixed
108
+
109
+ Doctor now blocks receipt order lines hidden by a cart-state visibility condition, including when only the CampaignSpec identifies the receipt page. When the campaign API key comes directly from the Build Packet or CampaignSpec, or from the environment variable named by Build Packet `campaign.api_key_source`, doctor blocks built pages with a different key even if those pages agree with one another. A mismatch names each wrong key source and the pages using it, so the built keys can be replaced before test orders begin.
110
+
111
+ ## [1.54.0+agent.5] - 2026-10-07
112
+
113
+ ### Fixed
114
+
115
+ - `qa run --test-order` orders a multi-unit bundle card the way the checkout shows it. A starter bundle card declares its package and per-package quantity in `data-next-bundle-items` (`bundle-2x` is `[{"packageId":1,"quantity":2}]`), and the runner now reads them there:
116
+ - `--select-package 1:2` finds the card whose items carry package 1 at quantity 2 even when no inner node repeats the package id. A card matches when any one of its declared items is that package at that quantity, so a kit card declaring `[{"packageId":1,"quantity":2},{"packageId":4,"quantity":1}]` is found by `4:1`; when a card declares the item alone as well, that card is preferred. Before, it failed `selected_bundle` with "no rendered card exposes that package or bundle identity".
117
+ - `--cart 1:2` clicks that same card. Before, it clicked the first `[data-next-package-id="1"]`, which on the starter partial is the one-unit card's inner node, so the order carried one unit. Without `--select-package`, a `--cart` ref the selected card already carries at that quantity clicks nothing, and a ref that no bundle card declares is clicked as before. The cards are read again before each ref, so a card a selector renders after an earlier `--cart` click is found.
118
+ - Beside `--select-package`, `--cart` never clicks another card in the selector group of the card `--select-package` chose, for bundle cards and plain package cards alike; a card in no `[data-next-bundle-selector]`, `[data-next-selector-id]` or `[data-next-cart-selector]` container counts as in that group, since its own wrapper does not say which cards it swaps with. A `--cart` ref that card carries is left as it is; a ref an order bump or a card in another selector group carries is applied through that control; any other ref is left unapplied and named in the `selected_bundle` detail (`--cart 3:1 not applied: only another card in the selector --select-package chose carries it`), and the order is reconciled against the card actually selected. Before, `--select-package 1:2 --cart 3:1` switched to the package-3 card while `selected_bundle` still reported the two-unit card, and `--select-package 1:2 --cart 1:2,2:1` reset the order to one unit.
119
+ - The `browser-order-display-parity` row reconciles the order against the packages and quantities every selected bundle card declares, however many cards are selected. Before, it counted the card's `data-next-bundle-id` as a displayed package and resolved lines at one unit per package, so a correct two-unit order failed with "displayed but never charged: 1".
120
+ - Without `--select-package`, the same row now fails when an explicit `--cart <ref:qty>` is not in the order at that quantity: the order carried another quantity, as when `--cart 1:2` produced a one-unit order, or no line for the package at all, as when `--cart 1:2,3:1` on a swap-mode selector kept only the `3:1` card. Its message names the requested and persisted units, or says the order carries no line for the package. A bare `--cart <ref>` is not judged. The row's message now also names `--select-package` quantity mismatches, which already failed the row but were not named in its message.
121
+ - Checkouts built from plain package cards (`[data-next-package-id]`, no bundle cards) are selected and reconciled as before, except that `--cart` beside `--select-package` no longer switches to another card in the chosen card's selector group, and without `--select-package` an explicit `--cart <ref:qty>` the order does not carry at that quantity fails the display-parity row.
122
+
123
+ ## [1.54.0+agent.4] - 2026-10-07
124
+
125
+ ### Fixed
126
+
127
+ `qa run` now calls `ready_with_exceptions` a passing proof, lists each exception once with its id and page (and severity when present), and tells the agent to report exceptions to the operator. `next` gives the same guidance when QA completed with exceptions while keeping the closeout action.
128
+
129
+ The completed-pipeline prompt and Build Packet guide now point to `record build` for repeated build work and distinguish local-serve recording from hosted deploy instructions. The starter-palette advisory prints the named-human argument required by a recorded `theme waive`, while help and QA guidance distinguish it from the one-run `--theme-waive` form. QA prompts, help, and guides ask once, up front, before typed-card test orders unless the operator already said they are fine for this campaign. Those orders leave real store records even though no transaction or money movement occurs. `ready` and `blocked` QA text keeps its existing finding format.
130
+
131
+ ## [1.54.0+agent.3] - 2026-10-07
132
+
133
+ ### Fixed
134
+
135
+ The `qa run --browser` primary-CTA row (`browser-primary-cta:<page>`) no longer fails `missing_route_cta` on checkout and upsell pages that Campaign Cart routes. Since 1.53.0 those pages are checked, but the check found a route only through a control's own link, and these pages have none: a checkout advances through the submit button of its `<form data-next-checkout="form">`, and an upsell through its `data-next-upsell-action="add"` and `"skip"` controls, which are `href="#"` links. So every SDK-routed checkout and upsell failed, the starter templates' own pages included, while the `route-link:*` rows and test orders passed on the same pages. QA now takes the route a page declares for these controls, from the same page fields the `route-link:*` rows read. On a checkout, the checkout form's submit button leads to the page's next route, whether it is `type="submit"` or has no `type` attribute, which submits the form just the same; express-checkout wallet buttons are not. On an upsell or downsell, the add control leads to the page's accept route and the skip control to its decline route, but only inside a `data-next-upsell` container: Campaign Cart binds upsell actions nowhere else, so an action outside one routes nothing. The control is checked as the primary CTA when that route is the page's next route, and its contrast, size and text are measured as before. On checkout, upsell and downsell pages these declared controls are the only controls a form can route. Any other form's `action`, for example a newsletter sign-up beside the checkout form, routes nothing there, even when it is the next route. So a page whose only route to the next page is such a form still fails `missing_route_cta`. On every other page a form's `action` is now the route of that form's own submit button only, and never inside the checkout form, which Campaign Cart submits itself. So bundle cards, wallet buttons and other controls inside a form no longer become CTA candidates because the form has an action; before, they did, and the row turned to `manual_review`. A page with no control leading to its next route still fails `missing_route_cta`.
136
+
137
+ QA now recognises `data-next-upsell-action="accept"` and `data-next-upsell-action="decline"` on upsell pages. Campaign Cart treats `accept` exactly like `add` and `decline` exactly like `skip`, but QA only looked for `add` and `skip`. So on a page written with `accept` / `decline`, the test-order upsell step failed with `Missing upsell control`, the `browser-upsell-control:<page>:accept` and `:decline` rows read `not found`, the primary-CTA row failed `missing_route_cta`, and the `route-link:<page>:accept` and `:decline` rows fell to `manual_review` when the route URL was not in the page's HTML. QA now takes either spelling everywhere it looks for an upsell control: the test-order upsell step and its timeout evidence, the upsell-page readiness wait, the rendered upsell-control rows, the primary-CTA route rule, and the static `route-link:*` rows, including the check for a `data-upsell-proxy` button, which forwards to the in-offer action with its own spelling; Polish readability's contrast check also takes either spelling for its `upsell_accept` and `upsell_decline` roles. Pages written with `add` / `skip` are checked as before, and when a page has neither spelling, QA's messages still name `add` and `skip`. Other values are still not upsell controls.
138
+
139
+ ## [1.54.0+agent.2] - 2026-10-07
140
+
141
+ ### Changed
142
+
143
+ Repository CI and contributor tooling only; the published package is unchanged. CI runs the unit and browser lanes as two shards each on separate runners, and a newer push to a pull request cancels that pull request's older run; pushes to `main` always finish. `npm run check:fast` runs the checks of CI's types and contracts lanes and the three PR-only release gates against `origin/main` in under a minute, and `npm run hooks:install` installs it as a pre-push hook for the clone and its worktrees (`docs/small-pr-review-path.md`).
144
+
145
+ ## [1.54.0+agent.1] - 2026-10-07
146
+
147
+ ### Changed
148
+
149
+ - When `start`, `prepare-build` or `build` refuses to overwrite a report that already carries stage evidence, the message now says that, if the brief or CampaignSpec changed, `record brief` or `record spec` binds it while keeping that evidence. Before, it offered only `--force`, which resets the recorded stages.
150
+ - README: the build steps and the `record` command list now include `record theme`, `record deploy` (bound to the build it probed), `record brief` and `record spec`. The SDK examples take `--target-sdk <x.y.z>` instead of 0.4.38. The `tooling status` note no longer says npm has no dist-tag, the Node floor reads 20.19.0 as in `engines`, and `--force` is described as moving completed records to stage history.
151
+ - `docs/quickstart.md`: a QA run against a local-address base URL keeps its verdict local by default, whatever the telemetry consent.
152
+
5
153
  ## [1.54.0] - 2026-10-07
6
154
 
7
155
  Ships the same-surface changes 1.53.0+agent.1 through +agent.11 (+agent.6 and +agent.8 were never used), including the `qa run --test-order` fix for Campaign Cart SDK 0.4.41 card fields (+agent.5) and the starter catalog, SDK attribute index and support policy at 0.4.41 (+agent.10, +agent.11).
package/CONTEXT.md CHANGED
@@ -22,9 +22,10 @@ The deterministic decision point that blocks `next polish|deploy|qa` and
22
22
  `qa run` when theme inspect proved a brand theme is generatable, the campaign
23
23
  ships commerce pages, and the brand layer is neither applied (after
24
24
  `next-core.css`) nor explicitly waived. Evaluated once in the doctor
25
- (`derived.theme_gate`) and consumed identically by `next` and QA. A waiver
26
- (`theme waive` / `--theme-waive`) is the only sanctioned bypass and is recorded
27
- on the Assembly Report with a reason.
25
+ (`derived.theme_gate`) and consumed identically by `next` and QA. A waiver is
26
+ the only sanctioned bypass: `theme waive --reason "<why>" --waived-by "<named
27
+ human>"` records it on the Assembly Report; `qa run --theme-waive "<reason>"`
28
+ waives the gate for that one run only.
28
29
  _Avoid_: advisory warning, recommendation, soft check
29
30
 
30
31
  **Template Brand Contract**:
@@ -36,6 +37,17 @@ applied brand tokens, and pricing-surface modes. QA reads it to fail "still
36
37
  visually the starter template" deterministically.
37
38
  _Avoid_: style guide, design tokens doc, theme report
38
39
 
40
+ **Checkout (payment page)**:
41
+ The page on a checkout path that takes payment: CampaignSpec `type: "checkout"`
42
+ (campaigns-os#641). It forwards to an upsell, downsell or thank-you page, and
43
+ every path from an entry page to an upsell passes exactly one. Pages ahead of
44
+ it render the same SDK checkout surface without placing an order: a `select`
45
+ page (the bundle picker) and `checkout_step` pages (a split checkout's
46
+ information and shipping forms). A campaign may hold several Checkouts, one
47
+ per split-test path; every consumer asks for "the Checkout on this path"
48
+ (`campaign-spec/checkout-flow.ts`), never for the first page typed checkout.
49
+ _Avoid_: first checkout, checkout page (for a step), final step
50
+
39
51
  **Template Reference**:
40
52
  The canonical baseline for a template family: its safe runtime structure,
41
53
  expected screenshots or render references, default assets, starter residue
@@ -676,8 +688,10 @@ and your username. The patterns are the value, not the raw dumps."
676
688
 
677
689
  **Domain Expert**: "No. Campaigns OS should show QA as the Expected Proof Step.
678
690
  Browser QA still needs the deployed URL (and SDK-origin allowlist confirmation
679
- for non-localhost), but typed-card test orders have no approval gate — they use
680
- global test cards that bypass the gateway; depth is the only control."
691
+ for non-localhost). Typed-card test orders use global test cards that bypass
692
+ the gateway and move no money, but each leaves a real store order record.
693
+ Unless the operator has already said test orders are fine for this campaign,
694
+ ask once, up front, before placing them; then proceed. There is no permission flag."
681
695
 
682
696
  **Developer**: "Build and polish finished, but there is no QA verdict."
683
697
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ This toolkit gives campaign developers and AI coding tools a clear path for asse
13
13
  7. Hand off to `next-campaigns-build`.
14
14
  8. Run build/lint and record the build with `campaigns-os record build`, then install the Campaigns OS Playwright browser once with `campaigns-os qa install-browser`.
15
15
  9. Run `next-campaigns-polish`, serve the current build, run the mandatory `campaigns-os polish capture` producer, then record Polish with `campaigns-os record polish --evidence <file>`.
16
- 10. Deploy a preview.
16
+ 10. Deploy a preview. For a local preview (`--deploy-target local-serve`), serve the current build and record it with `campaigns-os record deploy --base-url <loopback route root>`; a later `record build` of different output makes the deploy owed again. `record deploy` takes only a loopback route root; for a hosted preview, deploy through your host and follow `next` for what to record.
17
17
  11. Run `next-campaigns-qa` against the tested URL.
18
18
  12. Record launch blockers and follow-up work.
19
19
 
@@ -289,8 +289,8 @@ npm run campaigns-os -- prepare-build --spec <spec.json> --source <html-dir> --t
289
289
  npm run campaigns-os -- doctor --packet <page-kit-repo>/campaign-runtime.build.json
290
290
  npm run campaigns-os -- page-kit sync --packet <page-kit-repo>/campaign-runtime.build.json --dry-run
291
291
  npm run campaigns-os -- checkpoint waive --packet <packet.json> --gate source_html.producer_provenance --page <page_id> --reason "<why>" --waived-by "<named human>" --review-condition "<trigger>" --dry-run
292
- npm run campaigns-os -- sdk storage-check --target <campaign-git-root> --target-sdk 0.4.38 --manifest <sdk-storage-manifest.json> --scope <campaign,shared> --json
293
- npm run campaigns-os -- sdk repin --target <static-campaign-repo> --target-sdk 0.4.38
292
+ npm run campaigns-os -- sdk storage-check --target <campaign-git-root> --target-sdk <x.y.z> --manifest <sdk-storage-manifest.json> --scope <campaign,shared> --json
293
+ npm run campaigns-os -- sdk repin --target <static-campaign-repo> --target-sdk <x.y.z>
294
294
  npm run campaigns-os -- standardize --target <page-kit-repo-or-cpk-repo> --json
295
295
  npm run campaigns-os -- theme inspect --packet <page-kit-repo>/campaign-runtime.build.json --json
296
296
  npm run campaigns-os -- theme generate --packet <page-kit-repo>/campaign-runtime.build.json --json
@@ -302,6 +302,10 @@ npm run qa:install-browser
302
302
  npm run campaigns-os -- next polish --packet <packet.json> --report <assembly-report.json>
303
303
  npm run campaigns-os -- polish capture --packet <packet.json> --base-url <served-current-build-url>
304
304
  npm run campaigns-os -- record polish --packet <packet.json> --evidence <polish-evidence.json>
305
+ npm run campaigns-os -- record theme --packet <packet.json>
306
+ npm run campaigns-os -- record deploy --packet <packet.json> --base-url <loopback-route-root>
307
+ npm run campaigns-os -- record brief --packet <packet.json> --brief <campaign-build-brief.yaml>
308
+ npm run campaigns-os -- record spec --packet <packet.json>
305
309
  npm run campaigns-os -- next qa --packet <packet.json> --report <assembly-report.json>
306
310
  npm run campaigns-os -- qa resolve --packet <packet.json>
307
311
  npm run campaigns-os -- qa run --packet <packet.json> --base-url <preview-url> --browser --test-order common
@@ -320,14 +324,24 @@ output path alone does not create or refresh the file. Build/QA producer
320
324
  commands continue to record their own stages.
321
325
 
322
326
  Record a stage's completion with `record setup`, `record build` (after every
323
- rebuild) and `record polish --evidence <file>` rather than hand-editing
324
- `.campaign-runtime/build-context.json` or `.campaign-runtime/assembly-report.json`.
325
- Each validates what it would write, refuses a stage `next` has not reached, and
326
- writes nothing on failure; `--dry-run` runs the checks without writing. See
327
+ rebuild), `record polish --evidence <file>`, `record theme` (once the brand layer
328
+ is linked) and, for a local preview, `record deploy --base-url <loopback route
329
+ root>`, rather than hand-editing `.campaign-runtime/build-context.json` or
330
+ `.campaign-runtime/assembly-report.json`. A recorded deploy is bound to the
331
+ build it probed, so a later `record build` of different output makes it owed
332
+ again. When the brief or the CampaignSpec changes after stages are recorded, save
333
+ it with `record brief` or `record spec`: an unchanged or reformatted save keeps
334
+ every stage, a material change moves the superseded build, Polish and QA records
335
+ to stage history instead of discarding them, and `next` names the stage that is
336
+ owed again. Re-running
337
+ `start` or `prepare-build` over recorded stages is refused without `--force`.
338
+ Each `record` command validates what it would write, refuses a stage `next` has
339
+ not reached, and writes nothing on failure; `--dry-run` runs the checks without
340
+ writing. See
327
341
  [Build Packet](docs/build-packet.md) and [Polish evidence](docs/polish-evidence.md).
328
342
 
329
343
  Do not use `prepare-build --force` merely to refresh a catalog path: doctor
330
- already resolves the running toolkit's catalog, and force clears stage evidence.
344
+ already resolves the running toolkit's catalog, and `--force` resets the recorded stages (completed build, Polish and QA records move to stage history).
331
345
 
332
346
 
333
347
  `qa run` automatically checks contract-governed authored price, recurring
@@ -339,8 +353,8 @@ Run `tooling status` before a build session. It names the install mode — a
339
353
  pinned package (`npx`, or a consumer's `node_modules`) or a git checkout — and
340
354
  checks that the package metadata, CLI entrypoint, and installed Campaigns OS
341
355
  skills agree. For a checkout it also reports branch, upstream, and ahead/behind;
342
- for a package install the pinned commit is the freshness answer, and there is
343
- no npm dist-tag to compare against. Neither mode makes agent skills current on
356
+ for a package install the pinned version is the freshness answer, and the
357
+ status does not compare it with npm's `latest` dist-tag. Neither mode makes agent skills current on
344
358
  its own: when skills are stale, run the refresh command the status output
345
359
  prints (it names each stale platform, through the same prefix you ran
346
360
  `tooling status` with), then read the `SKILL.md` files it lists under `Read now`
@@ -393,7 +407,7 @@ registry — so a spec rule is authored once and reaches internal teams and
393
407
  third-party agencies alike. The rules are authored in TypeScript with no heavy
394
408
  dependencies and compiled to plain ESM (`npm run build:spec`, on `prepare`) and a
395
409
  stable subpath export `@nextcommerce/campaigns-os/campaign-spec`, so consumers run
396
- them on `engines.node` (>=20) with no type-stripping or build step of their own.
410
+ them on `engines.node` (>=20.19.0) with no type-stripping or build step of their own.
397
411
  See [`campaign-spec/README.md`](campaign-spec/README.md).
398
412
 
399
413
  ## Docs
@@ -422,7 +436,7 @@ See [`campaign-spec/README.md`](campaign-spec/README.md).
422
436
 
423
437
  ## Status
424
438
 
425
- Developer preview. Build output still needs the normal proof gates: build/lint evidence, polish plus package-owned page-load capture against the served current build, preview deploy or local dev URL, Playwright browser QA, and typed-card test-order proof via `--test-order common` (global test cards bypass the gateway and create no transactions; no approval needed — depth is the only control). Localhost on any port is a Campaigns App Development domain, so SDK calls are allowed and analytics are suppressed there; non-localhost preview/production origins still need SDK origin allowlist confirmation.
439
+ Developer preview. Build output still needs the normal proof gates: build/lint evidence, polish plus package-owned page-load capture against the served current build, preview deploy or local dev URL, Playwright browser QA, and typed-card test-order proof via `--test-order common`. Global test cards bypass the gateway and create no transactions, but each test order leaves a real store order record. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing test orders; then proceed. There is no permission flag. Localhost on any port is a Campaigns App Development domain, so SDK calls are allowed and analytics are suppressed there; non-localhost preview/production origins still need SDK origin allowlist confirmation.
426
440
 
427
441
  Launch readiness is separate from Campaigns OS proof. Before real shoppers see a campaign, confirm the production storefront URL, live payment methods, shipping markets, legal/support URLs, analytics expectations, and merchant-side configuration.
428
442
 
@@ -16,7 +16,9 @@ rules are pure TypeScript over a normalized spec with no heavy dependencies.
16
16
  campaign-spec/
17
17
  index.ts # public interface
18
18
  types.ts # CampaignSpec, Rule, Violation, Tag, Severity
19
- normalize.ts # v4.3 authoring → canonical v4.2 funnels[] shape
19
+ normalize.ts # authoring → canonical funnels[] shape; upgrades v4.2/v4.3 to v5
20
+ checkout-flow.ts # which page takes payment; v4 → v5 upgrade; path helpers
21
+ routing.ts # a page's outgoing edges
20
22
  rules/
21
23
  index.ts # preset RuleSet constants
22
24
  cycle-detection.ts # one file per rule
@@ -131,6 +133,18 @@ IIFE) and expose the public interface on `window` to run export-time validation
131
133
  client-side. The rules have no Node-only or live-data dependencies, so the same
132
134
  registry that backs the CLI doctor runs unchanged in the browser.
133
135
 
136
+ ## CampaignSpec 5.0: the page that takes payment
137
+
138
+ `checkout` is the page that takes payment; `checkout_step` is a checkout form
139
+ that collects details and moves on through `next_page`; `select` is the bundle
140
+ picker before them. `normalize()` upgrades a 4.2/4.3 spec to that vocabulary
141
+ on read (each `checkout` whose resolved forward target is another `checkout`
142
+ becomes `checkout_step`) and never mutates its input. Four rules hold the
143
+ shape — `CheckoutForwardTarget`, `PrePaymentForwardTarget`,
144
+ `CheckoutStepReachesCheckout` and `OneCheckoutPerPath` — and none of them
145
+ reads page order or page names. `paymentPageFrom`, `checkoutPathFrom` and
146
+ `paymentPagesForFunnel` answer "which Checkout is on this path".
147
+
134
148
  ## See also
135
149
 
136
150
  - `../CONTEXT.md` — domain vocabulary.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * checkout-flow — which page takes payment, and the pages that lead to it.
3
+ *
4
+ * From CampaignSpec v5 a page's `type` says which page takes payment
5
+ * (campaigns-os#641). Three types carry the shopper into a payment:
6
+ *
7
+ * | Type | Checkout form | Places order | Forward field |
8
+ * |-----------------|---------------|--------------|---------------|
9
+ * | `select` | no | no | `next_page` |
10
+ * | `checkout_step` | yes | no | `next_page` |
11
+ * | `checkout` | yes | yes | `success_url` |
12
+ *
13
+ * All three render as SDK page type `checkout`. A campaign may hold several
14
+ * Checkouts (one per split-test path); each path reaches exactly one.
15
+ *
16
+ * Every consumer that asks "which page on this path takes payment" asks here,
17
+ * never "the first page typed checkout". The one place the payment page is
18
+ * worked out from links rather than read from `type` is `upgradeCampaignSpec`,
19
+ * which retypes a v4 spec on read.
20
+ */
21
+ import type { Funnel, Page } from './types.ts';
22
+ /** The CampaignSpec lineage this module writes when it upgrades a v4 spec. */
23
+ export declare const CURRENT_SCHEMA_VERSION: "5.0";
24
+ /** The page that takes payment. */
25
+ export declare const PAYMENT_PAGE_TYPE: "checkout";
26
+ /** A checkout form that collects details and moves on without placing an order. */
27
+ export declare const CHECKOUT_STEP_PAGE_TYPE: "checkout_step";
28
+ /** Pages that lead into a Checkout without taking payment themselves. */
29
+ export declare const PRE_PAYMENT_PAGE_TYPES: readonly ["select", "checkout_step"];
30
+ /**
31
+ * Every page type the SDK renders as `checkout`: the pre-payment pages and the
32
+ * Checkout itself. Payment-page features (exit intent, promo code input,
33
+ * checkout theme gates) apply to all of them.
34
+ */
35
+ export declare const CHECKOUT_FLOW_PAGE_TYPES: readonly ["select", "checkout_step", "checkout"];
36
+ /** Where a Checkout may send the shopper once payment succeeds. */
37
+ export declare const POST_PAYMENT_PAGE_TYPES: readonly ["upsell", "downsell", "thankyou"];
38
+ /** Post-purchase offer pages: reaching one means an order was already placed. */
39
+ export declare const POST_PURCHASE_OFFER_PAGE_TYPES: readonly ["upsell", "downsell"];
40
+ export declare function isPaymentPage(page: Page | null | undefined): boolean;
41
+ export declare function isCheckoutStepPage(page: Page | null | undefined): boolean;
42
+ export declare function isPrePaymentPage(page: Page | null | undefined): boolean;
43
+ export declare function isCheckoutFlowPage(page: Page | null | undefined): boolean;
44
+ /**
45
+ * The page a route target names within `pages`, or null when it names none
46
+ * (an absolute URL, a fragment, a route outside this funnel, or a typo that
47
+ * RouteTargetResolves reports). Page IDs win over `page_url` matches.
48
+ */
49
+ export declare function resolveRouteTargetPage(pages: readonly Page[], target: string | null | undefined): Page | null;
50
+ /** The page this page's forward link resolves to, within `pages`. */
51
+ export declare function forwardTargetPage(pages: readonly Page[], page: Page | null | undefined): Page | null;
52
+ /**
53
+ * The pages from `start` forward to the Checkout that takes payment, both
54
+ * included, following each page's forward link. Null when the walk leaves the
55
+ * funnel, loops, ends, or meets a post-payment page before any Checkout.
56
+ *
57
+ * `start` may itself be the Checkout (the path is then that page alone).
58
+ */
59
+ export declare function checkoutPathFrom(pages: readonly Page[], start: Page | null | undefined): Page[] | null;
60
+ /** The Checkout reached by walking forward from `start`, or null. */
61
+ export declare function paymentPageFrom(pages: readonly Page[], start: Page | null | undefined): Page | null;
62
+ /**
63
+ * The funnel's entry pages: those flagged `is_entry`, or when none is flagged,
64
+ * those no other page routes to apart from orphaned upsell, downsell and
65
+ * thank-you pages (falling back to the lowest `order` when that leaves none). Order and names are never consulted beyond that
66
+ * last-resort tie-break.
67
+ */
68
+ export declare function entryPages(funnel: Funnel | null | undefined): Page[];
69
+ /**
70
+ * The Checkout on the path through `page`'s funnel that starts at an entry
71
+ * page, for callers that hold a funnel rather than a starting page. When the
72
+ * funnel has several entry paths with different Checkouts, returns the first
73
+ * entry's; use `paymentPagesForFunnel` to see all of them.
74
+ */
75
+ export declare function paymentPageForFunnel(funnel: Funnel | null | undefined): Page | null;
76
+ /** Every distinct Checkout reached from the funnel's entry pages, in entry order. */
77
+ export declare function paymentPagesForFunnel(funnel: Funnel | null | undefined): Page[];
78
+ /**
79
+ * Upgrade a CampaignSpec to the v5 page vocabulary. Returns a new object; the
80
+ * input is never mutated, so callers that hash or persist the raw bytes keep
81
+ * the bytes they read.
82
+ *
83
+ * v5 specs pass through unchanged. For any other lineage, each `checkout` page
84
+ * whose resolved forward target is also a `checkout` is retyped
85
+ * `checkout_step` — the v4 `checkout → checkout` chain was the only way to say
86
+ * "this form moves on without taking payment". A supported v4 `schema_version`
87
+ * becomes `5.0`; a missing or unsupported one is left for SchemaVersion to
88
+ * report.
89
+ */
90
+ export declare function upgradeCampaignSpec<T>(input: T): T;