@nextcommerce/campaigns-os 1.55.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 (93) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CONTEXT.md +11 -0
  3. package/campaign-spec/README.md +15 -1
  4. package/campaign-spec/dist/checkout-flow.d.ts +90 -0
  5. package/campaign-spec/dist/checkout-flow.js +229 -0
  6. package/campaign-spec/dist/index.d.ts +1 -0
  7. package/campaign-spec/dist/index.js +6 -1
  8. package/campaign-spec/dist/normalize.d.ts +3 -2
  9. package/campaign-spec/dist/normalize.js +7 -5
  10. package/campaign-spec/dist/routing.d.ts +6 -3
  11. package/campaign-spec/dist/routing.js +6 -3
  12. package/campaign-spec/dist/rules/checkout-forward-target.d.ts +17 -0
  13. package/campaign-spec/dist/rules/checkout-forward-target.js +48 -0
  14. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.d.ts +12 -0
  15. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.js +37 -0
  16. package/campaign-spec/dist/rules/exit-intent-validation.js +2 -1
  17. package/campaign-spec/dist/rules/index.d.ts +4 -0
  18. package/campaign-spec/dist/rules/index.js +12 -0
  19. package/campaign-spec/dist/rules/one-checkout-per-path.d.ts +25 -0
  20. package/campaign-spec/dist/rules/one-checkout-per-path.js +84 -0
  21. package/campaign-spec/dist/rules/pre-payment-forward-target.d.ts +12 -0
  22. package/campaign-spec/dist/rules/pre-payment-forward-target.js +42 -0
  23. package/campaign-spec/dist/rules/promo-code-input-validation.js +2 -1
  24. package/campaign-spec/dist/rules/schema-version.d.ts +9 -4
  25. package/campaign-spec/dist/rules/schema-version.js +10 -5
  26. package/campaign-spec/dist/types.d.ts +20 -3
  27. package/campaign-spec/dist/types.js +3 -2
  28. package/compatibility.json +2 -2
  29. package/contracts/fixtures/runtime-recipe/accept/current.json +6 -1
  30. package/contracts/fixtures/runtime-recipe/accept/minimal.json +6 -1
  31. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  32. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +6 -1
  33. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +6 -1
  34. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +6 -1
  35. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +6 -1
  36. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +6 -1
  37. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +1 -1
  38. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +6 -1
  39. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +6 -1
  40. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +6 -1
  41. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +5 -0
  42. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +6 -1
  43. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +6 -1
  44. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +6 -1
  45. package/contracts/release-ledger.json +705 -0
  46. package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
  47. package/contracts/supported-surface.json +8 -4
  48. package/docs/build-packet.md +52 -8
  49. package/docs/local-setup.md +1 -1
  50. package/docs/orientation-contract-reference.md +1 -1
  51. package/docs/qa-and-test-orders.md +78 -18
  52. package/docs/runtime-readiness.md +8 -3
  53. package/docs/skills-revision.md +10 -10
  54. package/docs/supported-surface.md +1 -1
  55. package/docs/versioning.md +4 -1
  56. package/package.json +2 -1
  57. package/schemas/campaign-runtime-build-context.v0.schema.json +3 -1
  58. package/schemas/campaign-runtime-build-packet.v0.schema.json +3 -1
  59. package/schemas/campaign-spec.v5.schema.json +586 -0
  60. package/skills/campaign-lifecycle-orientation/SKILL.md +10 -8
  61. package/skills/campaign-readback-classification/SKILL.md +3 -3
  62. package/skills/campaign-run-evidence/SKILL.md +3 -3
  63. package/skills/contribution-intake/SKILL.md +3 -3
  64. package/skills/next-campaigns-build/SKILL.md +6 -4
  65. package/skills/next-campaigns-os/SKILL.md +3 -3
  66. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  67. package/skills/next-campaigns-polish/SKILL.md +21 -6
  68. package/skills/next-campaigns-qa/SKILL.md +4 -3
  69. package/skills.json +10 -10
  70. package/src/adapter-decision-contract.mjs +1 -1
  71. package/src/build-brief.mjs +14 -5
  72. package/src/cli-helpers.mjs +129 -0
  73. package/src/cli.mjs +121 -53
  74. package/src/commercial-journey.mjs +25 -2
  75. package/src/design-source-package.mjs +1 -1
  76. package/src/design-source-publication.mjs +7 -7
  77. package/src/doctor/checks.mjs +196 -13
  78. package/src/doctor/inspect.mjs +4 -2
  79. package/src/doctor/next-step.mjs +31 -11
  80. package/src/doctor-sidecar.mjs +5 -3
  81. package/src/gate-actions.mjs +21 -1
  82. package/src/input-refresh.mjs +6 -6
  83. package/src/local-preview-policy.mjs +110 -12
  84. package/src/polish-node.mjs +3 -3
  85. package/src/qa-browser.mjs +226 -34
  86. package/src/qa-cart-entry.mjs +78 -14
  87. package/src/qa-node.mjs +35 -33
  88. package/src/qa-test-order-topology.mjs +118 -1
  89. package/src/qa-verdict-discovery.mjs +21 -4
  90. package/src/runtime-state-ignore.mjs +7 -5
  91. package/src/source-html-intake.mjs +46 -4
  92. package/src/stage-ledger.mjs +4 -2
  93. package/src/stage-record.mjs +3 -3
@@ -3,7 +3,7 @@
3
3
  "_growth_note": "When a bound below is genuinely reached, the answer is to find out why before raising it. A dependency install that exceeds its bound on a warm machine is a supply-chain change, not a slow morning; an output inventory that exceeds its file or byte bound is a build that went wrong, not a package that grew 50x overnight. Raising a bound is the fallback, it advances recipe_revision, and it owes a release-ledger entry. Widening the accepted npm range follows the same path, and its trigger is external and checkable: widen when a Node release line ships that npm major by default, not when a particular machine happens to have it installed.",
4
4
  "schema": "campaigns-os-runtime-recipe/v1",
5
5
  "recipe_kind": "campaigns-os-node-v1",
6
- "recipe_revision": "1.0.2",
6
+ "recipe_revision": "1.0.3",
7
7
  "refusal_reason_code": "runtime_recipe_refused",
8
8
  "fail_closed": true,
9
9
  "unperformable_check_disposition": "failed",
@@ -106,6 +106,7 @@
106
106
  "fingerprint_algorithm": "sha256",
107
107
  "files": [
108
108
  "campaign-spec/analytics-vocabulary.ts",
109
+ "campaign-spec/checkout-flow.ts",
109
110
  "campaign-spec/index.ts",
110
111
  "campaign-spec/normalize.ts",
111
112
  "campaign-spec/package.json",
@@ -113,7 +114,9 @@
113
114
  "campaign-spec/rules/analytics-contract-shape.ts",
114
115
  "campaign-spec/rules/assembly-hints-shape.ts",
115
116
  "campaign-spec/rules/campaign-metadata.ts",
117
+ "campaign-spec/rules/checkout-forward-target.ts",
116
118
  "campaign-spec/rules/checkout-has-success-url.ts",
119
+ "campaign-spec/rules/checkout-step-reaches-checkout.ts",
117
120
  "campaign-spec/rules/cycle-detection.ts",
118
121
  "campaign-spec/rules/design-source-shape.ts",
119
122
  "campaign-spec/rules/downsell-without-upsell.ts",
@@ -124,9 +127,11 @@
124
127
  "campaign-spec/rules/funnel-weight-sum.ts",
125
128
  "campaign-spec/rules/index.ts",
126
129
  "campaign-spec/rules/offer-ref-integrity.ts",
130
+ "campaign-spec/rules/one-checkout-per-path.ts",
127
131
  "campaign-spec/rules/package-pricing-sanity.ts",
128
132
  "campaign-spec/rules/page-count.ts",
129
133
  "campaign-spec/rules/page-id-uniqueness.ts",
134
+ "campaign-spec/rules/pre-payment-forward-target.ts",
130
135
  "campaign-spec/rules/promo-code-input-validation.ts",
131
136
  "campaign-spec/rules/promo-codes-shape.ts",
132
137
  "campaign-spec/rules/route-field-ignored-for-page-type.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act — hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here — src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) — is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
3
- "surface_version": "1.55.0",
3
+ "surface_version": "1.57.0",
4
4
  "package_exports": [
5
5
  "./commercial-journey",
6
6
  "./commercial-parity",
@@ -8,6 +8,7 @@
8
8
  "./campaign-spec",
9
9
  "./legacy-migration",
10
10
  "./schemas/campaign-spec.v4.schema.json",
11
+ "./schemas/campaign-spec.v5.schema.json",
11
12
  "./schemas/campaigns-os-legacy-migration-inventory.v0.schema.json",
12
13
  "./schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json",
13
14
  "./schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json",
@@ -51,7 +52,7 @@
51
52
  "sha256": "53269453dfdaac4b286124d445b60b0c46205dfbaf8037ac61b30254a32df53a"
52
53
  },
53
54
  "contracts/runtime-recipe.campaigns-os-node-v1.json": {
54
- "sha256": "90aaaaade90cf2525ea1aa05be5cb2d02b57e6e59f9278a946efa8a087946fd3"
55
+ "sha256": "ab0537a4756310d3a35b400d3932e46695a820f1b8b398f52f6fd0906098bc1d"
55
56
  },
56
57
  "schemas/campaign-build-brief.v1.schema.json": {
57
58
  "sha256": "d524003184131398dc15fdb84385af1be0de0c69d6c5ede7278689a5b6101b81"
@@ -63,14 +64,17 @@
63
64
  "sha256": "625a6f2ed6bee726f9cddad750e64a54a7e03744c777f5a70afe4a39afdd9848"
64
65
  },
65
66
  "schemas/campaign-runtime-build-context.v0.schema.json": {
66
- "sha256": "f310c1b7844d9b1994864d15858ff0c6f17efc1e5285ba193d181bdb2dd4be2f"
67
+ "sha256": "1f9bd6cced44a8ba6b29cc650f2b071db67ad710c5a9e9dfe47287eac11e0ebf"
67
68
  },
68
69
  "schemas/campaign-runtime-build-packet.v0.schema.json": {
69
- "sha256": "5002529c5979c4d8fef7e298de82ebdf625ffbd11f5284ba684d96d173788d62"
70
+ "sha256": "f0952f64fe9f5ae6147649e5e99b24f215c8d080fca4e330d73f54e49a00d0e2"
70
71
  },
71
72
  "schemas/campaign-spec.v4.schema.json": {
72
73
  "sha256": "af56c3de638d3c4e1fd5df8c2d733754d9ad16caa23b5bec92ce3a6f71bd0271"
73
74
  },
75
+ "schemas/campaign-spec.v5.schema.json": {
76
+ "sha256": "2923382e94ac51f6a19a3f9dfd0993d8c30fab33cd9be3d6901f314e02f5d960"
77
+ },
74
78
  "schemas/campaigns-os-legacy-migration-inventory.v0.schema.json": {
75
79
  "sha256": "d6edc4572900d15fdaf6b8c59916b0d885303c1d398bdadeb7c58285522c9049"
76
80
  },
@@ -19,7 +19,8 @@ The current schema is `schemas/campaign-runtime-build-packet.v0.schema.json`.
19
19
 
20
20
  A saved Map is optional for a prepared-HTML build. The coding agent authors an
21
21
  ordinary CampaignSpec from the brief, source design and configured campaign's
22
- real commerce values, following `schemas/campaign-spec.v4.schema.json`. The
22
+ real commerce values, following `schemas/campaign-spec.v5.schema.json` (a 4.2/4.3
23
+ spec still reads, upgraded on read; see `docs/versioning.md`). The
23
24
  operator supplies the selected store/campaign, public Campaigns API key, intended
24
25
  pages and commercial choices, plus store contact details and policy URLs. Verify
25
26
  the store/campaign binding, and read the package, offer and shipping references
@@ -1020,13 +1021,25 @@ than hand-adding the field. Note that a committed artifact set always reads
1020
1021
  stale to the readback once HEAD moves past it — freshness proof is a
1021
1022
  regeneration at HEAD, not a property a commit can preserve.
1022
1023
 
1023
- Commit durable packet/context/report artifacts when they represent a real build handoff. The rest of `.campaign-runtime/` is machine-local and is not for the campaign repository: `run-session.json`, `command-lifecycle.jsonl`, `agent-deviations.jsonl`, `workflow-findings.jsonl`, `run-records/`, `fetched-specs/`, `polish-evidence/`, `evidence/`, and `*.log`/`*.tmp` are per-machine, append-only, or carry live URLs and absolute paths, and so are the full QA verdicts `qa run` writes under the target's `qa-output/`. `start`, `prepare-build`, `install-agent-context`, and `run start` write a managed ignore block for exactly that set into the target's `.gitignore` once (keyed on its marker line; edit the list beneath it freely). The readback bundle (`build-context.json`, `assembly-report.json`, `doctor-output.json`, `qa-verdict.json`), `input/`, `theme/`, `agent-context/`, and `setup-handoff.json` are deliberately not ignored. The Campaigns API key is a public, browser-side, domain-allowlisted key and may already be present in the local CampaignSpec as `campaign.campaigns_api_key`; do not duplicate it into the packet unless the spec is unavailable. Do not commit raw private API responses, backend secrets, or temporary media exports.
1024
+ Commit durable packet/context/report artifacts when they represent a real build handoff. The rest of `.campaign-runtime/` is machine-local and is not for the campaign repository: `run-session.json`, `command-lifecycle.jsonl`, `agent-deviations.jsonl`, `workflow-findings.jsonl`, `run-records/`, `fetched-specs/`, `progress/`, `polish-evidence/`, `evidence/`, and `*.log`/`*.tmp` are per-machine, append-only, or carry live URLs and absolute paths, and so are the full QA verdicts `qa run` writes under the target's `qa-output/`. `start`, `prepare-build`, `install-agent-context`, and `run start` write a managed ignore block for that set into the target's `.gitignore` (keyed on its marker line; edit the list beneath it freely). An existing block gains `qa-output/` and `.campaign-runtime/progress/` when they are missing, since those hold live URLs, order references and remit records. The readback bundle (`build-context.json`, `assembly-report.json`, `doctor-output.json`, `qa-verdict.json`), `input/`, `theme/`, `agent-context/`, and `setup-handoff.json` are deliberately not ignored. The Campaigns API key is a public, browser-side, domain-allowlisted key and may already be present in the local CampaignSpec as `campaign.campaigns_api_key`; do not duplicate it into the packet unless the spec is unavailable. Do not commit raw private API responses, backend secrets, or temporary media exports.
1024
1025
 
1025
1026
  Packet-mode `doctor` is inspection-only by default and preserves retained evidence and active run journals, even when lifecycle capture is configured. Use `--write` to intentionally record fresh evidence; `--no-write` takes precedence. Inspection still reports current blockers and keeps the same exit status.
1026
1027
 
1027
1028
  `campaigns-os doctor --packet <packet> --write` restates its outcome on the Assembly Report's `stages.doctor` (status, command, outputs, blockers, warnings, `checked_at`). A re-run that reaches the same outcome leaves the report's bytes unchanged rather than refreshing the timestamp alone, so a digest taken of the report — a Run Record's `assembly_report` sha256 — keeps verifying across repeated doctor runs; a changed outcome still rewrites the file.
1028
1029
 
1029
- `campaigns-os start` / `campaigns-os prepare-build` writes packet, context, report, and generated doctor-output paths as relative paths by default, including sibling CampaignSpec/source directories such as `../campaign-source`. `campaigns-os doctor` continues to accept older absolute-path packets; use `campaigns-os doctor --packet <packet> --write --strip-paths` when regenerating a commit-ready doctor output from an older packet. Committed handoff artifacts should not contain machine-local absolute paths unless no relative form is possible.
1030
+ `campaigns-os start` / `campaigns-os prepare-build` writes portable packet, context, report, and generated doctor-output paths. Resolve each path from its reader's base:
1031
+
1032
+ | Field | Base | Writer |
1033
+ |---|---|---|
1034
+ | Build Packet `spec.local_path`, `design_source_package.path`, `source_html.root`, `build_brief.input_path`, `build_brief.normalized_path`, `assembly.target_repo`, `assembly.commerce_catalog.path` | Build Packet file's directory, including when `--out` places it under `config/` | `start` / `prepare-build`; `record brief` and stage records retain this base for older absolute values |
1035
+ | Build Packet `source_html.pages[].path`; Build Context `page_map[].source_path` | Source HTML root | `start` / `prepare-build`; later context writes retain it |
1036
+ | Build Packet and Build Context `source_html.pages[].page_kit.target_path` / `page_map[].page_kit.target_path` | Page Kit output directory | `start` / `prepare-build`; later packet/context writes retain it |
1037
+ | Build Packet `assembly.output_dir` and `source_html.pages[].page_kit.output_path`; Build Context `page_map[].output_path` and `page_map[].page_kit.output_path` | Target repository root | `start` / `prepare-build`; later packet/context writes retain it |
1038
+ | Build Context and Assembly Report `design_source_package.path` | Directory of that context or report file (normally `.campaign-runtime/`) | `start` / `prepare-build`; later report writes retain it |
1039
+ | Normalized Campaign Build Brief `_meta.input_path` | Directory of the normalized brief file | `start` / `prepare-build`; `record brief` and `record spec` retain it |
1040
+ | Other listed Build Context, Assembly Report, and doctor-output filesystem paths | Target repository root | Intake, `doctor --write`, `next`, and stage writes |
1041
+
1042
+ Outside source inputs use ordinary relative paths from the applicable base; keep them at the recorded locations when moving the repository. Manifest paths embedded in decision evidence use relative paths from the target repository root. A stored command such as `next.command` uses a relative packet path and runs from the target repository root. `campaigns-os doctor` continues to accept older absolute-path packets; `--strip-paths` remains available when inspecting an older packet. Machine-local run sessions and journals remain ignored and may use absolute paths.
1030
1043
 
1031
1044
  `start` / `prepare-build` also run the [Brand Theme Bridge](./brand-theme-bridge.md)
1032
1045
  in `inspect_only` mode. The optional theme evidence lives in `context.theme`,
@@ -1463,10 +1476,29 @@ Fresh `prepare-build` output also writes `source_html.pages[].page_kit` for mapp
1463
1476
  - `target_path` is the page file relative to `assembly.output_dir` (`checkout.html`, `receipt.html`, etc.).
1464
1477
  - `output_path` is the same target file relative to `assembly.target_repo`.
1465
1478
  - `public_route` is the rendered campaign-rooted route Page Kit should produce.
1466
- - `page_type` is the CPK runtime/analytics vocabulary (`product`, `checkout`, `upsell`, `receipt`), not the richer CampaignSpec or producer page type. CampaignSpec `select` pages project as CPK `checkout` because they are pre-checkout runtime selection surfaces.
1467
- - `frontmatter` names the Page Kit frontmatter fields the build should write or preserve.
1479
+ - `page_type` is the CPK runtime/analytics vocabulary (`product`, `checkout`, `upsell`, `receipt`), not the richer CampaignSpec or producer page type. CampaignSpec `select` and `checkout_step` pages project as CPK `checkout` because they render the SDK checkout surface ahead of the page that takes payment.
1480
+ - `frontmatter` names the Page Kit frontmatter fields the build should write or preserve. Beside `page_type`, `permalink`, `next_url` and `decline_url`, two keys wire multi-step checkout paths: `success_url`, on every page that leads into a Checkout (a `select` page, a `checkout_step`, or a landing page whose forward links reach one), is that Checkout's post-payment destination and feeds the page's `next-success-url` meta tag, which the SDK reads for express orders; `step_number`, on a `checkout_step` only, is its position on its path from 1, for the step form's `data-next-step-number`. The step form's `data-next-checkout-step` is the page's `next_url`.
1468
1481
  - `permalink_required` is true when Page Kit's filename-derived route would not match `public_route`.
1469
1482
 
1483
+ An entry page (`is_entry: true`) declares the campaign root when `page_url` is
1484
+ empty, `/`, `/<slug>/`, or `<slug>`.
1485
+ If Page Kit emits its mapped target file, or a materialised template-stock
1486
+ family page, at that file's route without a root output, doctor recognizes the
1487
+ built page and warns that the root is unserved. The warning names both routes:
1488
+ add a redirect from the root to the emitted route or set
1489
+ `permalink: /<slug>/` on the entry page, the campaign root as Page Kit emits it.
1490
+ It uses `campaign.route_root`: a root-served campaign names `/` and
1491
+ `/<file-route>/`; a slug-served campaign names `/<slug>/` and
1492
+ `/<slug>/<file-route>/`. The built files stay under `_site/<slug>/` in both cases.
1493
+ `qa resolve --packet campaign-runtime.build.json --base-url <preview-url>` then
1494
+ prints the locally emitted entry route. An explicit permalink on that entry's
1495
+ Page Kit source file takes precedence, even if stale output remains at the
1496
+ file route. Doctor still warns if that permalink serves a non-root route and
1497
+ the root output is absent. With no permalink, a root `index.html` takes
1498
+ precedence over the recorded file route; with a non-root permalink, a present
1499
+ root output suppresses the warning while QA keeps the permalink Entry URL.
1500
+ Another page's `index.html` cannot stand in for the entry.
1501
+
1470
1502
  The Design Source Package should reference this projection without confusing it
1471
1503
  with Surface Identity. Surface Identity is the campaign-facing join key; Page Kit
1472
1504
  `page_type`, public routes, output paths, CampaignSpec/Map Builder page IDs,
@@ -1761,7 +1793,19 @@ prepare-build gate is set (`next` answers prepare-build) or while an earlier
1761
1793
  stage in the order below is not terminal. The exception is the one `next`
1762
1794
  makes: on the local preview, a polish doctor carries forward (never recorded
1763
1795
  for this build) does not hold `record deploy` back, as it does not hold `next`;
1764
- polish stays owed and QA reports it.
1796
+ polish stays owed and QA reports it. If every mapped page is template stock on
1797
+ a hosted preview, doctor carries the same missing-evidence shape forward.
1798
+ `record deploy` remains local-only: record an unset hosted preview URL with
1799
+ `campaigns-os qa policy set --packet <p> --preview-url <url>`, then run
1800
+ `campaigns-os next --packet <p>`. The hosted preview must be a non-loopback
1801
+ HTTP(S) URL whose origin or QA-resolved campaign route path differs from `deploy.production_url`;
1802
+ query and fragment text does not make the production page a preview. With
1803
+ `deploy.preview_url` recorded, run `campaigns-os next qa --packet <p>` and
1804
+ `campaigns-os qa run --packet <p> --base-url <preview-url> --browser
1805
+ --test-order common`. The recorded hosted preview satisfies the deploy step
1806
+ for this all-template shape. After QA records a verdict for the current build,
1807
+ `campaigns-os next --packet <p>` reaches closeout without a hosted deploy
1808
+ record. QA against the packet's production URL retains the strict gate.
1765
1809
 
1766
1810
  Each command reads the same packet, Build Context and Assembly Report `next`
1767
1811
  reads (`--context` / `--report` override them the same way), validates what it
@@ -1791,7 +1835,7 @@ build` after every page-kit build; a rebuild that changes the output needs
1791
1835
  rebuilt output, `next` asks for QA again if the last QA record names the
1792
1836
  previous build fingerprint, even while its stage status still says completed.
1793
1837
 
1794
- Stage order: `setup → build → polish → deploy → qa`. The picker walks this list and returns the first stage whose recorded status isn't terminal (`completed`, `completed_with_warnings`, `skipped`). During Polish, install the package-owned browser first, then run `campaigns-os polish capture` against the served current build before recording a terminal `stages.polish.status` or proceeding to deploy/QA; the producer attaches package-owned `visual_review.page_load` evidence and never marks the stage complete itself.
1838
+ Stage order: `setup → build → polish → deploy → qa`. The picker walks this list and returns the first stage whose recorded status isn't terminal (`completed`, `completed_with_warnings`, `skipped`), except that a carried-forward all-template hosted preview with a recorded `deploy.preview_url` satisfies deploy without a report record. During Polish, when a design route exists, install the package-owned browser first, then run `campaigns-os polish capture` against the served current build before recording a terminal `stages.polish.status` or proceeding to deploy/QA; the producer attaches package-owned `visual_review.page_load` evidence and never marks the stage complete itself. Template-stock campaigns with no design route have no capture to run and follow the preview policy above.
1795
1839
 
1796
1840
  | Stage | Report key | Owner |
1797
1841
  |---|---|---|
@@ -1824,7 +1868,7 @@ Result shape (with `--json`):
1824
1868
  Terminal states:
1825
1869
 
1826
1870
  - **`stage: "doctor-blocked"`** — doctor returned errors. Resolve the blockers and re-run `campaigns-os doctor` to confirm before calling `next` again.
1827
- - **`stage: "done"`** — every stage is in a terminal status. Pipeline is complete. To repeat build work, do the work and use `record build`; it makes downstream stages owed as needed. Use `record setup` or `record polish` after repeating those stages, and `qa run` for QA. `record deploy` records a local-serve target; for a hosted deploy, record the URL and stage outcome as the deploy prompt describes. Then call `next` again.
1871
+ - **`stage: "done"`** — every stage is terminal, or a carried-forward all-template hosted preview satisfies deploy with a recorded `deploy.preview_url` and current-build QA verdict. Pipeline is complete. To repeat build work, do the work and use `record build`; it makes downstream stages owed as needed. Use `record setup` or `record polish` after repeating those stages, and `qa run` for QA. `record deploy` records a local-serve target; other hosted deploys record the URL and stage outcome as the deploy prompt describes. Then call `next` again.
1828
1872
  - **`stage_blocked: true`** — the picker returned a stage whose recorded status is `blocked`. Don't run the prompt as-is; clear the blocker first.
1829
1873
 
1830
1874
  The legacy form `campaigns-os next <stage>` (e.g. `next build`) still works and is the way to force a specific stage when you want to override the picker.
@@ -3,7 +3,7 @@
3
3
  For a new campaign, create an empty working folder and run this from it:
4
4
 
5
5
  ```sh
6
- npm init -y && npm install --save-exact next-campaign-page-kit@0.2.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.55.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
6
+ npm init -y && npm install --save-exact next-campaign-page-kit@0.2.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.57.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
7
7
  ```
8
8
 
9
9
  `npm init -y` gives the folder its own `package.json`. Without one, npm
@@ -26,7 +26,7 @@ Ledger schema id: `campaigns-os-release-ledger/v1`
26
26
  Change policy version: `1.0.0`
27
27
  Reason-code vocabulary version: `1.0.0`
28
28
  Limits version: `1.0.0`
29
- Supported surface at generation time: `1.55.0`
29
+ Supported surface at generation time: `1.57.0`
30
30
 
31
31
  ## Forward compatibility
32
32
 
@@ -101,7 +101,7 @@ recapture. A hosted target (`netlify`, `cloudflare-pages`, …) is unaffected:
101
101
  its build stage renders production as before and `page-kit parity` refuses the
102
102
  packet (`local_proof.parity.not_local_serve`).
103
103
 
104
- ### Missing evidence carried forward on the local preview
104
+ ### Missing evidence carried forward on previews
105
105
 
106
106
  On a `local-serve` packet served from a loopback host (`localhost`,
107
107
  `127.0.0.1`, `[::1]`, for both `deploy.preview_url` and `--base-url`), some
@@ -110,20 +110,28 @@ loop. The campaign must still prove its commerce there: store and campaign
110
110
  binding, routes, SDK loading, prices and a typed-card order. Only these
111
111
  checks are carried forward:
112
112
 
113
- | Check | When |
114
- | --- | --- |
115
- | `polish.evidence_missing`, `polish.report_missing` | Polish was never recorded for this build. |
116
- | `polish.hidden_eager_media.no_capturable_routes` | Every mapped page is template stock (`skip_reason`), so polish capture has no design route to capture. |
117
- | `polish.hidden_eager_media.capture_malformed` | Only when no page-load capture was recorded at all. |
118
- | Template-residue severity | With `theme_gate.nothing_generatable`, the starter template is the design, so residue findings are warnings rather than blockers. |
113
+ | Check | Local loopback preview | Hosted preview |
114
+ | --- | --- | --- |
115
+ | `polish.evidence_missing`, `polish.report_missing` | Carried when Polish was never recorded. | Carried only with the all-template `no_capturable_routes` checkpoint. |
116
+ | `polish.hidden_eager_media.no_capturable_routes` | Carried when every mapped page is template stock (`skip_reason`). | Carried when every mapped page is template stock and QA uses the packet's non-loopback `deploy.preview_url`, distinct from `deploy.production_url` by origin or QA-resolved campaign route path. |
117
+ | `polish.hidden_eager_media.capture_malformed` | Carried only when no page-load capture was recorded at all. | Blocked. |
118
+ | Template-residue severity | With `theme_gate.nothing_generatable`, residue findings warn. | Unchanged. |
119
119
 
120
120
  A carried-forward gate has status `carried_forward`. Doctor reports it as a
121
- warning starting "Carried forward on the local preview"; `next` moves past
122
- polish to deploy and QA; QA records it as a `warn` row, so the verdict is at
121
+ warning starting "Carried forward on the local preview" or "Carried forward on the hosted preview"; `next` moves past
122
+ polish to deploy and QA, and the recorded all-template hosted preview satisfies deploy without a deploy record; QA records it as a `warn` row, so the verdict is at
123
123
  best `ready_with_exceptions`. The evidence is reported as missing, never as
124
- passed. Any other check keeps its meaning. A hosted preview or production
125
- packet, and a `local-serve` packet served from any other host, gets the strict
126
- gates. `record polish` and the waiver commands also keep them strict. Progress
124
+ passed. On a hosted all-template preview, first record an unset preview URL
125
+ with `campaigns-os qa policy set --packet <packet> --preview-url <url>`, then
126
+ run `campaigns-os next --packet <packet>`. Run `campaigns-os next qa --packet <packet>`
127
+ then `campaigns-os qa run --packet <packet> --base-url <preview-url> --browser --test-order common` directly. After QA records the current-build verdict, run `campaigns-os next --packet <packet>` for closeout;
128
+ `record deploy` only accepts loopback local-serve URLs; a pending hosted deploy
129
+ record does not suppress these QA actions when the all-template checkpoint is
130
+ carried forward. The hosted exception
131
+ requires a distinct packet `deploy.preview_url`; a query or fragment on the production page does not count as distinct. QA with `--base-url` equal to
132
+ the packet's `deploy.production_url` stays blocked. A `local-serve` packet
133
+ served from any other host also gets strict gates. `record polish` and the
134
+ waiver commands keep them strict. Progress
127
135
  snapshots record a carried-forward gate as `not_applicable`, because their
128
136
  schema has no carried-forward state.
129
137
 
@@ -179,8 +187,9 @@ line and the printed per-URL rows say which URLs those were rather than leaving
179
187
  an operator to infer it from `counts.unreachable`. It is `null` only when every
180
188
  probed URL resolved, or when nothing was probed at all.
181
189
 
182
- Resolve appends `campaign.public_route_slug` unconditionally — the packet is
183
- the authority on where a campaign is served, and no flag overrides it. When
190
+ Resolve appends `campaign.public_route_slug` to the preview base for `base_url`
191
+ and page URLs, including when `campaign.route_root` is `/`. A subpath on
192
+ `--base-url` stays ahead of the slug. When
184
193
  every derived route is dead, one extra probe of the host without that slug
185
194
  separates the two causes and says which it found:
186
195
  `route_probe.route_root_mismatch` (the host serves the campaign under a
@@ -308,6 +317,21 @@ for that entry URL. If Entry URLs are empty, still point at a deleted preview, o
308
317
  fail their own HTTP assertion, fix `--base-url` or the packet deploy URL before
309
318
  continuing.
310
319
 
320
+ For an `is_entry` page with `page_url` empty, `/`, `/<slug>/`, or `<slug>`, `qa resolve` uses that page's
321
+ explicit Page Kit permalink first, then the campaign root when
322
+ `_site/<slug>/index.html` exists. If neither applies and Page Kit emitted the
323
+ entry at its recorded page-file route, the printed Entry URL uses that locally
324
+ emitted route. Doctor warns that the Map still points at the unserved root;
325
+ add a redirect from the root to the emitted route or set `permalink: /<slug>/`
326
+ on the entry page. A non-root permalink also gets the warning when root output
327
+ is absent; QA keeps that permalink as its Entry URL. A present root output
328
+ suppresses the warning. The default
329
+ probe checks the printed URL on the host. For either `campaign.route_root: "/"`
330
+ or the default route root, the file route is joined under `base_url` as
331
+ `/<slug>/<file-route>/` (after any preview subpath). The root output, when
332
+ present, uses `base_url` itself. The route-root declaration still determines
333
+ the public route named in doctor's unserved-root warning.
334
+
311
335
  `--base-url` can be either the deploy host or the campaign root. If the Build Packet says `campaign.public_route_slug = "roadside-ready"`, both of these resolve pages under `/roadside-ready/`:
312
336
 
313
337
  ```bash
@@ -1446,7 +1470,21 @@ timeout still leaves the ladder up to the point of failure. Ladder entries carry
1446
1470
  object. Evidence is resolved even when the step fails or times out, because the
1447
1471
  failing path is the one worth reading.
1448
1472
 
1449
- Four steps write structured evidence today.
1473
+ Five steps write structured evidence today.
1474
+
1475
+ **Multi-step checkout paths (campaigns-os#641).** The ladder drives the
1476
+ Checkout — the page that takes payment (`type: "checkout"`) — on the tested
1477
+ path, never the first page typed checkout. When `checkout_step` pages lead
1478
+ into it, the runner lands on the first step: through a `select` page ahead of
1479
+ it when there is one (`entered_via_landing` with `control_kind:
1480
+ checkout_button`), otherwise the step fills the cart itself.
1481
+ `customer_fields_filled` fills only the fields that step renders, one
1482
+ `checkout_step_submitted` rung per step follows, and the order is submitted
1483
+ only on the Checkout. With
1484
+ split-test paths in one campaign, each Checkout reached from an entry page is
1485
+ its own path; `tiers` plans read declared tiers and bumps from every page of
1486
+ that path (the select page or first step declares the cart), and coupons from
1487
+ its steps and Checkout.
1450
1488
 
1451
1489
  **`entered_via_landing` — cart entry.** The first rung of the ladder, before
1452
1490
  `opened_checkout`. A checkout renders its customer form whether or not the SDK
@@ -1490,7 +1528,13 @@ checkout — never a receipt or an offer page), navigates there, waits for the
1490
1528
  SDK, and clicks the cart-entry control: an SDK add-to-cart control
1491
1529
  (`[data-next-action="add-to-cart"]`, the only attribute the SDK activates the
1492
1530
  feature on), or a link into the checkout URL carrying `?forcePackageId=`,
1493
- which is what the certified `shop-single-step` landing renders. A control
1531
+ which is what the certified `shop-single-step` landing renders, or, on a
1532
+ page with bundle cards (`[data-next-bundle-card]`, a `select` page), a checkout
1533
+ button: the SDK's `[data-next-action="checkout"]` or a plain link into the
1534
+ checkout URL. There the swap-mode selector already wrote the cart and the SDK
1535
+ persists it, so the runner keeps the pre-selected card, or with
1536
+ `--select-package <ref>` clicks the bundle card carrying that ref first,
1537
+ strictly, before the button. A control
1494
1538
  spelled any other way is not a cart entry, so a page that offers nothing else
1495
1539
  fails the step by name (`cart_entry_control_missing`) rather than clicking a
1496
1540
  control the SDK never wired and waiting out the navigation budget. A visible
@@ -1508,8 +1552,9 @@ cart away. `opened_checkout` then records the arrival instead of re-opening.
1508
1552
 
1509
1553
  Evidence: `landing_url`, `landing_page_id`, `landing_page_type`,
1510
1554
  `landing_resolution` (`routes_into_checkout`, `entry_page_fallback`,
1511
- `first_page_fallback`), `control_text`, `control_kind` (`add_to_cart` or
1512
- `checkout_link`), `package_id`, `sdk_ready`, `arrived_url`, the
1555
+ `first_page_fallback`), `control_text`, `control_kind` (`add_to_cart`,
1556
+ `checkout_link`, or `checkout_button`), `package_id`, `bundle_card_selection`
1557
+ when `--select-package` clicked a bundle card, `sdk_ready`, `arrived_url`, the
1513
1558
  `checkout_selection_surface` probe result, `selection_surface_probe`
1514
1559
  (`loaded`, `reused`, or `failed`), and `selection_surface_probe_error` when it
1515
1560
  failed. The failure codes are
@@ -1545,6 +1590,21 @@ own 30s default, so a caller-supplied budget only ever tightens the ceiling. A
1545
1590
  stuck required field fails as that field rather than as an anonymous step
1546
1591
  timeout; a slow-but-working funnel waits no longer than it did before.
1547
1592
 
1593
+ **`checkout_step_submitted` — one rung per `checkout_step`.** On a path whose
1594
+ Checkout is preceded by `checkout_step` pages, each step's rung checks the page
1595
+ is on that step, clicks the submit control of its
1596
+ `form[data-next-checkout-step]` (the SDK validates the step's fields, keeps the
1597
+ answers and navigates), and waits for the page to land on the step's declared
1598
+ next page (`expected_next_url`). It then fills whatever fields the arrived page
1599
+ renders. A step whose form carries no `data-next-checkout-step`, or whose
1600
+ submit never reaches the declared next page (usually a required field the
1601
+ step validates), fails the rung by name; nothing is reserved or submitted, so
1602
+ the creation classifier reads it as `not_created`. Evidence: `step_page_id`,
1603
+ `step_number`, `expected_next_url`, `arrived_url` and the arrived page's
1604
+ `fields` trace. A path that starts on its first step, with no page routing into
1605
+ it, skips `entered_via_landing`: the step page fills the cart and the guard
1606
+ before submit checks it.
1607
+
1548
1608
  **`cart_created` — cart-API observation.** The step reports what the cart API
1549
1609
  actually returned: the most recent `POST /api/v1/carts/` response's `status`, an
1550
1610
  `ok` flag, `line_count` when the response body exposes lines, `response_count`,
@@ -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.55.0`.
11
+ Recipe kind `campaigns-os-node-v1`, revision `1.0.3`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.57.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -81,9 +81,10 @@ Not redundant with the install step. Because install runs with lifecycle scripts
81
81
 
82
82
  The complete input set, enumerated explicitly rather than globbed. The compiler's configured include globs are NOT the input set: two root modules enter the compilation transitively through imports from the entry module and are emitted, so a fingerprint derived from the globs would cover 36 of the 38 compiled sources and miss one of the larger emitted surfaces. Test fixtures and the package's own tests are not inputs; nothing under them is emitted. A checker resolves the compiler's actual file list and asserts it equals this enumeration, so this list cannot rot silently.
83
83
 
84
- Fingerprint algorithm `sha256`, over 42 enumerated files:
84
+ Fingerprint algorithm `sha256`, over 47 enumerated files:
85
85
 
86
86
  - `campaign-spec/analytics-vocabulary.ts`
87
+ - `campaign-spec/checkout-flow.ts`
87
88
  - `campaign-spec/index.ts`
88
89
  - `campaign-spec/normalize.ts`
89
90
  - `campaign-spec/package.json`
@@ -91,7 +92,9 @@ Fingerprint algorithm `sha256`, over 42 enumerated files:
91
92
  - `campaign-spec/rules/analytics-contract-shape.ts`
92
93
  - `campaign-spec/rules/assembly-hints-shape.ts`
93
94
  - `campaign-spec/rules/campaign-metadata.ts`
95
+ - `campaign-spec/rules/checkout-forward-target.ts`
94
96
  - `campaign-spec/rules/checkout-has-success-url.ts`
97
+ - `campaign-spec/rules/checkout-step-reaches-checkout.ts`
95
98
  - `campaign-spec/rules/cycle-detection.ts`
96
99
  - `campaign-spec/rules/design-source-shape.ts`
97
100
  - `campaign-spec/rules/downsell-without-upsell.ts`
@@ -102,9 +105,11 @@ Fingerprint algorithm `sha256`, over 42 enumerated files:
102
105
  - `campaign-spec/rules/funnel-weight-sum.ts`
103
106
  - `campaign-spec/rules/index.ts`
104
107
  - `campaign-spec/rules/offer-ref-integrity.ts`
108
+ - `campaign-spec/rules/one-checkout-per-path.ts`
105
109
  - `campaign-spec/rules/package-pricing-sanity.ts`
106
110
  - `campaign-spec/rules/page-count.ts`
107
111
  - `campaign-spec/rules/page-id-uniqueness.ts`
112
+ - `campaign-spec/rules/pre-payment-forward-target.ts`
108
113
  - `campaign-spec/rules/promo-code-input-validation.ts`
109
114
  - `campaign-spec/rules/promo-codes-shape.ts`
110
115
  - `campaign-spec/rules/route-field-ignored-for-page-type.ts`
@@ -132,7 +137,7 @@ The output directory is a build product. It is untracked and git-ignored, no com
132
137
 
133
138
  Directory `campaign-spec/dist`. Committed: `false`. Type entry `campaign-spec/dist/index.d.ts`.
134
139
 
135
- Expected inventory is derived, never listed twice. For every enumerated input under module_source_root whose path ends in .ts, the build is expected to emit campaign-spec/dist/<path relative to module_source_root, with .ts replaced> once per entry in emitted_extensions. The expected inventory is exactly that set: nothing missing, nothing extra. Emitted extensions: `.js`, `.d.ts`. At this revision that derivation yields 76 files.
140
+ Expected inventory is derived, never listed twice. For every enumerated input under module_source_root whose path ends in .ts, the build is expected to emit campaign-spec/dist/<path relative to module_source_root, with .ts replaced> once per entry in emitted_extensions. The expected inventory is exactly that set: nothing missing, nothing extra. Emitted extensions: `.js`, `.d.ts`. At this revision that derivation yields 86 files.
136
141
 
137
142
  ### Mandatory checks
138
143
 
@@ -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.55.0+skills.1"
19
+ "bundle_revision": "1.57.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.55.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.57.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.55.0+skills.1
40
+ Bundle revision: 1.57.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.55.0+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.57.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.55.0+skills.1",
93
+ "requested": "1.57.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.55.0+skills.1",
95
+ "on_disk": "1.57.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.55.0+skills.1)"
97
+ "message": "match (1.57.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.55.0+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.55.0+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.55.0+skills.1)
104
+ Skills revision: match (1.57.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.57.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.57.0+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an
@@ -13,7 +13,7 @@ implementation detail, however stable it looks.
13
13
 
14
14
  | Surface | Contract | Change discipline |
15
15
  |---|---|---|
16
- | `schemas/*.schema.json` (all of them) | The portable contract catalog: CampaignSpec, Design Source Package, Build Packet, Build Context, Assembly Report, Doctor Output, sidecar-bundle conformance, Run Record, Workflow Finding, Build Brief, Source-HTML Manifest, Tooling Orientation, Release Ledger, QA Verdict, the QA Verdict sidecar projection, Runtime Recipe, and the legacy-migration inventory/plan/receipt trio. | Hashed. Any content change requires updating the recorded hash **and** bumping `surface_version` in the same PR. A shape change that alters meaning gets a new schema-version const — one version identifier must never cover two shapes (the 2026-08 assembly-report drift is the incident this rule encodes). Additions to an open `v0` schema are expected and consumers must tolerate unknown fields; the security-sensitive legacy-migration schemas are closed, so additions there require a new lineage. 1.28.0 (RL entry `surface_version: 1.28.0`, breaking) removed the two required Build Packet booleans `qa.test_orders_allowed` and `qa.sandbox_test_card_confirmed` (nothing read them; test orders run from `--test-order <mode>` alone), added `local-serve` to the `deploy.target` enum, and added the optional `remit_result` / `remit_base_kind` fields to the Run Record. 1.30.0 (additive) added the optional `data_layer` record to the QA Verdict's `test_orders[]` entries — the order's `dl_purchase` reading (#325). 1.33.0 (additive) added the optional `qa_verdict_publish` block to the Run Record — which verdict was posted to the QA portal, by `qa run` or `qa publish`, and what the portal answered, in the `remit_result` vocabulary (#328). 1.55.0 (breaking) adds the optional publish decision `reason`, record `closed_by` closer, QA observation `verdict_run_id`, `build_fingerprint`, and `stale`, lifecycle `stages[].wait_ms` and `stages[].finding_codes`, and run-level `needs_input_count` and `failure_count` while removing the unused `agent_usage` field from the same v0 Run Record shape. |
16
+ | `schemas/*.schema.json` (all of them) | The portable contract catalog: CampaignSpec, Design Source Package, Build Packet, Build Context, Assembly Report, Doctor Output, sidecar-bundle conformance, Run Record, Workflow Finding, Build Brief, Source-HTML Manifest, Tooling Orientation, Release Ledger, QA Verdict, the QA Verdict sidecar projection, Runtime Recipe, and the legacy-migration inventory/plan/receipt trio. | Hashed. Any content change requires updating the recorded hash **and** bumping `surface_version` in the same PR. A shape change that alters meaning gets a new schema-version const — one version identifier must never cover two shapes (the 2026-08 assembly-report drift is the incident this rule encodes). Additions to an open `v0` schema are expected and consumers must tolerate unknown fields; the security-sensitive legacy-migration schemas are closed, so additions there require a new lineage. 1.28.0 (RL entry `surface_version: 1.28.0`, breaking) removed the two required Build Packet booleans `qa.test_orders_allowed` and `qa.sandbox_test_card_confirmed` (nothing read them; test orders run from `--test-order <mode>` alone), added `local-serve` to the `deploy.target` enum, and added the optional `remit_result` / `remit_base_kind` fields to the Run Record. 1.30.0 (additive) added the optional `data_layer` record to the QA Verdict's `test_orders[]` entries — the order's `dl_purchase` reading (#325). 1.33.0 (additive) added the optional `qa_verdict_publish` block to the Run Record — which verdict was posted to the QA portal, by `qa run` or `qa publish`, and what the portal answered, in the `remit_result` vocabulary (#328). 1.55.0 (breaking) adds the optional publish decision `reason`, record `closed_by` closer, QA observation `verdict_run_id`, `build_fingerprint`, and `stale`, lifecycle `stages[].wait_ms` and `stages[].finding_codes`, and run-level `needs_input_count` and `failure_count` while removing the unused `agent_usage` field from the same v0 Run Record shape. 1.56.0 (breaking) adds `schemas/campaign-spec.v5.schema.json` (CampaignSpec `5.0`: the `checkout_step` page type, and `checkout` meaning the page that takes payment; 4.2/4.3 specs still validate against the v4 schema and are upgraded on read) and the `success_url` and `step_number` keys to the closed `page_kit.frontmatter` object of the Build Packet and Build Context v0 schemas, so a validator holding the 1.55.0 schemas refuses a packet that carries them (campaigns-os#641). |
17
17
  | CLI commands: `start`, `prepare-build`, `build`, `polish`, `checkpoint`, `page-kit`, `spec`, `doctor`, `bundle`, `next`, `theme`, `tooling`, `install-skills`, `install-agent-context`, `validate-assembly-report`, `telemetry`, `standardize`, `qa`, `findings`, `run-record`, `run`, `sdk`, `demo`, `login`, `logout` | Scriptable entry points. The founding list (1.0.0) was the argv surface Campaigns Agent's remit fixture pins; 1.25.0 adds the partner entry path the public guides instruct — `install-skills` (skill install), `tooling status` (preflight), `next --packet` (the runtime cursor) — plus `theme`, `install-agent-context`, `validate-assembly-report`, and `telemetry`, since consent is part of the partner contract. `validate-build-packet`, formerly an undocumented and unsupported alias of `doctor`, was removed in 1.25.0+agent.7 (RL-0036) and now gets the unknown-command error; use `doctor`. `standardization-report`, a supported but redundant second spelling of `standardize` — same flags, same output, same exit codes — was removed in 1.27.0 and now gets the same unknown-command error; use `standardize`. Removing a supported command is a breaking change and is why that release moved the minor. `bundle check` validates the canonical JSON readback set and makes QA required only under `--require-qa`. `polish capture` owns page-load evidence production. `checkpoint waive` currently accepts five registered gates: `page_kit.store_profile`, `page_kit.sdk_version`, `polish.hidden_eager_media`, `built_output.upsell_selector_scope`, and `source_html.producer_provenance` (per page, with `--page <page_id>`). `page-kit sync` (added in 1.29.0) writes the CampaignSpec's Store Profile fields and SDK pin into the target's `_data/campaigns.json` entry — the repair the first two gates name — and is the only `page-kit` subcommand. `spec derive` (added in 1.31.0) is the reverse write for the repo-derived field class (#432): the target's SDK pin, page routes and analytics ids into the packet's local CampaignSpec; it is the only `spec` subcommand, and the `page_kit.sdk_version.repo_newer` advisory names it; its `--write-map` flag (1.33.0+agent.2) also records the derived pin in the saved Map's Build hints field through the proxy Worker, never moving a Map pin backwards. `qa publish` (added in 1.33.0) posts an already-stored verdict to the QA portal without a re-run or an order, refusing a stale `spec_hash` or a verdict its Run Record already records as published (#328). Within Polish, only the broader Source Freshness waiver remains on its existing report lane; theme and QA decisions also retain their existing lanes. | Any change to this list — adding, renaming, or removing a command — bumps `surface_version` in the same PR, and since 1.25.0 `check-supported-surface.mjs --base` enforces that (before, only hashed and named entries owed a bump, so `checkpoint` landed unbumped). Subcommands, registered gates, and flags may grow freely beneath a listed command. Removing a listed command from dispatch fails the gate outright. Do not infer support for an unregistered checkpoint from the top-level command. |
18
18
  | `sdk storage-check` and `docs/sdk-storage-compatibility.md` | Read-only AST compatibility report for explicitly scoped tracked campaign HTML/JS before an SDK bump; consumes the supplied SDK-owned manifest and records its SHA-256 and verified or unverified local Git provenance. Incompatible and unknown findings exit 2; a clean scan is static source evidence only. | Additive supported CLI and named documentation; no automatic merchant integration repair. |
19
19
  | `sdk repin` | Rewrites the version segment of semver-pinned Campaign Cart loader.js / campaign-cart.css URLs in a static repo (no `_data/campaigns.json`) to a target version, default the SDK support policy's `preferred_minimum`. It previews by default. `--apply` writes the files and the change record `.campaign-runtime/sdk-repin.json` (`campaigns-os-sdk-repin/v0`). `@latest`, `@main`, commit and prerelease refs, and pins at or above the target, are reported and never rewritten. A page-kit repo is refused with exit 2. Usage is in `docs/sdk-storage-compatibility.md`. | Additive supported subcommand under the listed `sdk` command. It never changes anything in a URL except the version segment. |
@@ -23,7 +23,10 @@ This repo uses independent compatibility versions:
23
23
  - QA Verdict sidecar projection: same `"1.0"` literal, projection guarantees in
24
24
  `schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json` (one contract, two
25
25
  schema files — the sidecar is an allowlist projection, never a second lineage)
26
- - CampaignSpec: `4.2`–`4.3` (JSON Schema: `schemas/campaign-spec.v4.schema.json`)
26
+ - CampaignSpec: `5.0` (JSON Schema: `schemas/campaign-spec.v5.schema.json`), with
27
+ `4.2`–`4.3` still read (`schemas/campaign-spec.v4.schema.json`) and upgraded on
28
+ read: each `checkout` page whose resolved forward target is another `checkout`
29
+ becomes `checkout_step`
27
30
  - starter-template agent contract: `1`
28
31
  - commerce surface catalog: `2`
29
32
  - Tooling Orientation: `campaigns-os-tooling-orientation/v1`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.55.0",
3
+ "version": "1.57.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,6 +44,7 @@
44
44
  "default": "./src/legacy-migration.mjs"
45
45
  },
46
46
  "./schemas/campaign-spec.v4.schema.json": "./schemas/campaign-spec.v4.schema.json",
47
+ "./schemas/campaign-spec.v5.schema.json": "./schemas/campaign-spec.v5.schema.json",
47
48
  "./schemas/campaigns-os-legacy-migration-inventory.v0.schema.json": "./schemas/campaigns-os-legacy-migration-inventory.v0.schema.json",
48
49
  "./schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json": "./schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json",
49
50
  "./schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json": "./schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json",
@@ -191,7 +191,9 @@
191
191
  },
192
192
  "permalink": { "type": "string", "minLength": 1 },
193
193
  "next_url": { "type": "string", "minLength": 1 },
194
- "decline_url": { "type": "string", "minLength": 1 }
194
+ "decline_url": { "type": "string", "minLength": 1 },
195
+ "success_url": { "type": "string", "minLength": 1, "description": "On a page that leads into a Checkout (select, checkout_step, or a landing page whose forward links reach one): that Checkout's post-payment destination, for the next-success-url meta tag the SDK reads for express orders. Never the next step (campaigns-os#641)." },
196
+ "step_number": { "type": "integer", "minimum": 1, "description": "On a checkout_step page: its position among its path's steps, from 1, for the step form's data-next-step-number." }
195
197
  }
196
198
  }
197
199
  },
@@ -193,7 +193,9 @@
193
193
  },
194
194
  "permalink": { "type": "string", "minLength": 1 },
195
195
  "next_url": { "type": "string", "minLength": 1 },
196
- "decline_url": { "type": "string", "minLength": 1 }
196
+ "decline_url": { "type": "string", "minLength": 1 },
197
+ "success_url": { "type": "string", "minLength": 1, "description": "On a page that leads into a Checkout (select, checkout_step, or a landing page whose forward links reach one): that Checkout's post-payment destination, for the next-success-url meta tag the SDK reads for express orders. Never the next step (campaigns-os#641)." },
198
+ "step_number": { "type": "integer", "minimum": 1, "description": "On a checkout_step page: its position among its path's steps, from 1, for the step form's data-next-step-number." }
197
199
  }
198
200
  }
199
201
  },