@nextcommerce/campaigns-os 1.62.0 → 1.66.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 (74) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/campaign-spec/dist/rules/campaign-currencies.d.ts +1 -0
  3. package/campaign-spec/dist/rules/campaign-currencies.js +1 -1
  4. package/campaign-spec/dist/rules/index.d.ts +1 -0
  5. package/campaign-spec/dist/rules/index.js +3 -0
  6. package/campaign-spec/dist/rules/package-currency-prices.d.ts +29 -0
  7. package/campaign-spec/dist/rules/package-currency-prices.js +137 -0
  8. package/campaign-spec/dist/types.d.ts +16 -0
  9. package/compatibility.json +1 -1
  10. package/contracts/effects.v1.json +129 -1
  11. package/contracts/fixtures/runtime-recipe/accept/current.json +1 -0
  12. package/contracts/fixtures/runtime-recipe/accept/minimal.json +1 -0
  13. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  14. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +1 -0
  15. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +1 -0
  16. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +1 -0
  17. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +1 -0
  18. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +1 -0
  19. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +1 -0
  20. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +1 -0
  21. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +1 -0
  22. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +1 -0
  23. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +1 -0
  24. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +1 -0
  25. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +1 -0
  26. package/contracts/release-ledger.json +810 -0
  27. package/contracts/runtime-recipe.campaigns-os-node-v1.json +1 -0
  28. package/contracts/supported-surface.json +6 -6
  29. package/docs/build-packet.md +31 -5
  30. package/docs/campaigns-os-build-flow.md +38 -0
  31. package/docs/campaignspec-authoring-examples.md +31 -0
  32. package/docs/effects.md +8 -0
  33. package/docs/local-setup.md +1 -1
  34. package/docs/orientation-contract-reference.md +1 -1
  35. package/docs/qa-and-test-orders.md +44 -10
  36. package/docs/readback.md +8 -1
  37. package/docs/runtime-readiness.md +4 -3
  38. package/docs/skills-revision.md +10 -10
  39. package/docs/supported-surface.md +1 -1
  40. package/package.json +1 -1
  41. package/schemas/campaign-spec.v4.schema.json +15 -0
  42. package/schemas/campaign-spec.v5.schema.json +15 -0
  43. package/schemas/campaigns-os-qa-verdict.v0.schema.json +17 -2
  44. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  45. package/skills/campaign-readback-classification/SKILL.md +3 -3
  46. package/skills/campaign-run-evidence/SKILL.md +3 -3
  47. package/skills/contribution-intake/SKILL.md +3 -3
  48. package/skills/next-campaigns-build/SKILL.md +3 -3
  49. package/skills/next-campaigns-os/SKILL.md +3 -3
  50. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  51. package/skills/next-campaigns-polish/SKILL.md +3 -3
  52. package/skills/next-campaigns-qa/SKILL.md +4 -4
  53. package/skills/next-campaigns-sdk-bump/SKILL.md +3 -3
  54. package/skills.json +11 -11
  55. package/src/change-scope.mjs +25 -6
  56. package/src/cli.mjs +120 -7
  57. package/src/commercial-journey.mjs +3 -0
  58. package/src/commercial-parity.mjs +157 -7
  59. package/src/doctor/checks.mjs +28 -3
  60. package/src/doctor/ci.mjs +36 -1
  61. package/src/evidence-debt.mjs +9 -1
  62. package/src/input-currency.mjs +4 -2
  63. package/src/input-refresh.mjs +1 -1
  64. package/src/invocation.mjs +3 -2
  65. package/src/orchestration-stage-contract.mjs +3 -1
  66. package/src/polish-gate.mjs +10 -0
  67. package/src/qa-browser.mjs +46 -0
  68. package/src/qa-commercial-parity.mjs +173 -18
  69. package/src/qa-node.mjs +10 -5
  70. package/src/qa-verdict.mjs +3 -1
  71. package/src/readback.mjs +1 -0
  72. package/src/runtime-state-ignore.mjs +44 -19
  73. package/src/stage-ledger.mjs +35 -9
  74. package/src/stage-record.mjs +93 -14
@@ -131,6 +131,7 @@
131
131
  "campaign-spec/rules/index.ts",
132
132
  "campaign-spec/rules/offer-ref-integrity.ts",
133
133
  "campaign-spec/rules/one-checkout-per-path.ts",
134
+ "campaign-spec/rules/package-currency-prices.ts",
134
135
  "campaign-spec/rules/package-pricing-sanity.ts",
135
136
  "campaign-spec/rules/page-count.ts",
136
137
  "campaign-spec/rules/page-id-uniqueness.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.62.0",
3
+ "surface_version": "1.66.0",
4
4
  "package_exports": [
5
5
  "./commercial-journey",
6
6
  "./commercial-parity",
@@ -52,7 +52,7 @@
52
52
  "sha256": "53269453dfdaac4b286124d445b60b0c46205dfbaf8037ac61b30254a32df53a"
53
53
  },
54
54
  "contracts/runtime-recipe.campaigns-os-node-v1.json": {
55
- "sha256": "3a960f347a8b7de7d9f223fb2bc1ae35091c943d480bca931bf8afa5e70fa83f"
55
+ "sha256": "4755be6c700463e99b609ceaebeb92cb1df5c54805aeaea3e6fd68e87ebcd6ef"
56
56
  },
57
57
  "schemas/campaign-build-brief.v1.schema.json": {
58
58
  "sha256": "d524003184131398dc15fdb84385af1be0de0c69d6c5ede7278689a5b6101b81"
@@ -70,10 +70,10 @@
70
70
  "sha256": "1e309c5dad8c5eeef26ef6fd3b6b90b72440c5104319a161002b17f1359fbd76"
71
71
  },
72
72
  "schemas/campaign-spec.v4.schema.json": {
73
- "sha256": "587df2fdd5707b515a9b8685d30070bcf4bdac9e15de7a94df82224ef4fd91b6"
73
+ "sha256": "4e9a092c1426429b727aaa6ef5622ae2c6c6dad59270f08636987847b63f5001"
74
74
  },
75
75
  "schemas/campaign-spec.v5.schema.json": {
76
- "sha256": "c606453dc57d210a5a5ed8a099bf868546ae8f5996cf9ba0f8e71a50b2b03fa4"
76
+ "sha256": "24d04c8d3ede920de161fadf914dc497a3f61e126f377cb01dd0aeff12b53c4f"
77
77
  },
78
78
  "schemas/campaigns-os-legacy-migration-inventory.v0.schema.json": {
79
79
  "sha256": "d6edc4572900d15fdaf6b8c59916b0d885303c1d398bdadeb7c58285522c9049"
@@ -88,7 +88,7 @@
88
88
  "sha256": "3c40fd79f218d1b95f0424f9a2b67db140a306329e2ca02793e2740d3afded59"
89
89
  },
90
90
  "schemas/campaigns-os-qa-verdict.v0.schema.json": {
91
- "sha256": "8c689d29850b2b894c17438ee4411363033bae6f805820af53b7a04a5693d0e0"
91
+ "sha256": "237361d4cda53f3cd98d306bc9ab2c2a0ce1cdd4ded0b15001a3c47b5cf05988"
92
92
  },
93
93
  "schemas/campaigns-os-doctor-output.v0.schema.json": {
94
94
  "sha256": "87866e619d2a18f01a208a1682cb14a2079f61df51c642f4e1902b6e6bce0ce5"
@@ -124,7 +124,7 @@
124
124
  "sha256": "b397b417b0456cafc03346a2e882a34c19fa0500ea92594afca2f9fa1d7f2c3b"
125
125
  },
126
126
  "contracts/effects.v1.json": {
127
- "sha256": "7aca3aa2208ccb700a74bc9554725a23b1f6c413e7ea7dc9ec442e884a8d72c3"
127
+ "sha256": "4979649217470c1066f7efab722e56d4089f9df374bfbb5af03d2d7e5aba3c14"
128
128
  },
129
129
  "schemas/campaigns-os-effects.v1.schema.json": {
130
130
  "sha256": "3eadd22169ab98bc7c2f682af2751267605170581182158d96be035b0cfe44dc"
@@ -1339,7 +1339,7 @@ than hand-adding the field. Note that a committed artifact set always reads
1339
1339
  stale to the readback once HEAD moves past it — freshness proof is a
1340
1340
  regeneration at HEAD, not a property a commit can preserve.
1341
1341
 
1342
- 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). The block also lists `.campaign-runtime/funnels/*/run-records/` for the Run Records of a project's second and later funnels. An existing block gains `qa-output/`, `.campaign-runtime/progress/` and `.campaign-runtime/funnels/*/run-records/` 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, at the top level or under `funnels/<route>/`. 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.
1342
+ 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/`, `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). The block also lists `.campaign-runtime/funnels/*/run-records/` for the Run Records of a project's second and later funnels. An existing block gains `qa-output/`, `.campaign-runtime/progress/` and `.campaign-runtime/funnels/*/run-records/` when they are missing, since those hold live URLs, order references and remit records. An existing block also loses `.campaign-runtime/fetched-specs/`, which earlier versions listed. The readback bundle (`build-context.json`, `assembly-report.json`, `doctor-output.json`, `qa-verdict.json`), `fetched-specs/`, `input/`, `theme/`, `agent-context/`, and `setup-handoff.json` are deliberately not ignored, at the top level or under `funnels/<route>/`. 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.
1343
1343
 
1344
1344
  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.
1345
1345
 
@@ -1527,7 +1527,9 @@ root and is not hashed. Build does not type the value: after page-kit build it r
1527
1527
  `campaigns-os record build --packet <packet>`, which stamps doctor's
1528
1528
  `derived.build_output_fingerprint.value` onto `stages.assembly.build_fingerprint`
1529
1529
  (see "Recording stage completion" below). Doctor recomputes the value on every run
1530
- (`built_output.fingerprint`): a match is a ready line, a missing record is the
1530
+ (`built_output.fingerprint`): a match is a ready line, no built output after a
1531
+ completed build is the warning `built_output.absent` (nothing was compared with
1532
+ this checkout, so build it before reading the records as current), a missing record is the
1531
1533
  warning `built_output.fingerprint_missing` carrying the value to record, and a
1532
1534
  recorded value the output no longer matches is `built_output.fingerprint_stale`
1533
1535
  (blocking once assembly is complete). The polish gate, QA, and `polish capture`
@@ -1580,8 +1582,11 @@ change from a commerce-affecting one
1580
1582
 
1581
1583
  Readback's `evidence_debt.<stage>.change` and `doctor --ci` read these, so the
1582
1584
  changed-page list survives `record build`. A record made before these fields
1583
- existed has no signature, and its next change reads as unknown (full Polish and
1584
- QA) until `record build` records one.
1585
+ existed has no signature, and its next change reads as commerce-affecting, with
1586
+ the reason `commerce_signature_unavailable`, whatever the change was. `record
1587
+ build` keeps a signature from then on, but the QA it would extend is still
1588
+ anchored before that rebuild. So page-only changes start after one full QA on
1589
+ 1.62.0 or later, or after [a baseline](campaigns-os-build-flow.md#accepting-a-funnel-from-before-1620).
1585
1590
 
1586
1591
  A stale or missing Assembly Source Package Fingerprint is waivable only as an
1587
1592
  exceptional Source Freshness Waiver. The waiver must be structured in
@@ -1779,7 +1784,8 @@ When `--map-id <id>` is set, the CLI fetches `GET <proxy>/api/spec/<id>` (defaul
1779
1784
 
1780
1785
  Saved-Map retrieval behavior (`--map-id`):
1781
1786
 
1782
- - **Re-fetch by default.** Every `start` / `prepare-build` invocation re-fetches from KV. KV is the source of truth; the cache file is a debug/inspection artifact, not a performance optimization.
1787
+ - **Re-fetch by default.** Every `start` / `prepare-build` invocation re-fetches from KV and replaces the copy, including any `spec derive` edits in it.
1788
+ - **Commit the copy.** The packet, the Build Context and the Assembly Report are bound to it, and `spec derive` writes into it, so it is the campaign's spec until the next intake. A checkout without it fails doctor with `spec.local_path`. [Restore it with `spec fetch`](#restoring-a-saved-map-spec-spec-fetch).
1783
1789
  - **One writer at a time.** The fetch happens first, but the fetched spec is written to the cache file only once the run holds the per-target prepare-build lock. A second run against the same target, fetching a newer Map revision, waits for the lock before replacing the cache, so the run holding it records the hash of the revision it actually parsed.
1784
1790
  - **`--cached-spec`** reuses the cache without a network call. Use for offline iteration or when the proxy is temporarily unreachable. The cached copy is read as it is and never rewritten.
1785
1791
  - **`--proxy-base <url>`** overrides the default origin. Use for staging environments or local Worker dev (`wrangler dev`). Spec retrieval carries no credential, so any reachable origin works here — but the same flag also aims the credential-bearing rails (Run Telemetry remit, QA verdict publish, `telemetry list`), and those require `https:` unless the host is loopback (`localhost`, `127.0.0.1`, `[::1]`), which is allowed over plain http with a stderr warning. A plain-http remote proxy is refused before the request. See docs/workflow-findings-sidecar.md (Remit Channel).
@@ -2114,6 +2120,26 @@ records it (`campaigns-os record setup|build|polish`; deploy and QA record their
2114
2120
  own stages) → calls `next` again → repeat until stage="done"
2115
2121
  ```
2116
2122
 
2123
+ ### Restoring a saved-Map spec (`spec fetch`)
2124
+
2125
+ Before 1.64.0 the fetched copy was ignored by Git, so a fresh clone of those repositories doesn't have it. On that checkout, run:
2126
+
2127
+ ```bash
2128
+ campaigns-os spec fetch --packet campaign-runtime.build.json [--proxy-base <url>] [--json]
2129
+ ```
2130
+
2131
+ It fetches the Map named by the packet, writes the copy to `spec.local_path`, and takes `fetched-specs/` out of an older managed `.gitignore` block. Commit the file so the next checkout has it. A copy already on disk is never replaced and nothing is sent. The packet and the records are not written.
2132
+
2133
+ The result's `bound` compares the copy with the spec the records were made against:
2134
+
2135
+ | `bound` | Meaning |
2136
+ |---|---|
2137
+ | `matches` | The records still apply. |
2138
+ | `differs` | The Map changed since they were made, or the earlier copy held `spec derive` edits the Map doesn't have. Doctor shows build, Polish and QA owed until `record spec` binds it (warning `spec.fetch.differs_from_records`). |
2139
+ | `unknown` | The report records no spec material to compare with. |
2140
+
2141
+ The command exits 2 for a packet that names no saved Map, for one whose `spec.local_path` is not the Map's copy, when the Map returns a spec for another campaign (`spec.fetch.identity_mismatch`), and when the fetch fails.
2142
+
2117
2143
  ### Refreshing after a CampaignSpec change
2118
2144
 
2119
2145
  When the CampaignSpec's material content changes, every build, Polish and QA record made against the earlier content stops counting as current, in local-spec, saved-Map and gateway packets alike. For a saved Map or gateway packet, Campaigns OS compares the copy intake fetched; it does not check whether that copy is the latest remote revision, and reports the remote as unconfirmed. Doctor warns `spec.material_stale`, and `next`, progress, readback, the Assembly Report's own status and the QA gate show the stages that are owed again; `next` routes to the first one. Run `campaigns-os record spec --packet <packet>` to bind the new content: it updates the build context and report identity, re-reads the brief file last saved (by intake or `record brief`), keeps waivers, warning accepts, the applied theme and deploy settings, and moves each superseded stage record into that stage's `history`, which is never used as current proof. `spec derive` no longer rebinds identity; run `record spec` after it. A move of page routes binds with `record spec`, and so does a page added or removed after handoff (see [Binding a page change](#binding-a-page-change)); a change to the order of active pages, or a page added or removed before handoff, still needs intake. Re-recording an unchanged build after a spec change keeps the build owed, because the change has not reached the pages, unless the operator records a reason with `--deviation-reason`; this holds even when `record spec` was not run, and that `record build` also marks Polish and QA owed again. Polish stays owed until a new `polish capture`, with one exception: when `record build --deviation-reason` records the very output a completed Polish reviewed (same build fingerprint and source package) and only the CampaignSpec changed, not the brief's presentation, that `record build` restores the Polish record the change moved to history, stamped with the current inputs. It is the same review of the same pages. QA must produce a new verdict against the current content. Polish measurements of byte-identical output keep their values, but the Polish stage is owed again. These checks are tamper evidence, not proof: a hand-written record that copies the current values, or a packet pointed at an older copy of the spec, is not detected.
@@ -89,6 +89,44 @@ or when it was a page-scoped run that did not complete, in which case the
89
89
  pages are counted from the full QA that run extended, so its unpaid pages stay
90
90
  owed. A blocked or pending full QA owes a full run.
91
91
 
92
+ A change is read against the build each stage last checked, counting every
93
+ rebuild since, not just the latest. So a copy edit that `record build` calls
94
+ `page_only` still owes the full stages when an earlier rebuild since then was
95
+ commerce-affecting. The report names that build and that rebuild, and gives a
96
+ date for a QA that completed.
97
+
98
+ Page-only changes start after one full QA on 1.62.0 or later, or after a
99
+ baseline. A build recorded before 1.62.0 has no markup hashes to compare with,
100
+ so the first change on it reads as commerce-affecting.
101
+
102
+ ### Accepting a funnel from before 1.62.0
103
+
104
+ A funnel handed off before 1.62.0 owes the full Polish and QA on its first
105
+ change, whatever the change. To start from where it is instead, a named person
106
+ can accept it:
107
+
108
+ ```bash
109
+ campaigns-os record build --packet <packet>
110
+ campaigns-os record baseline --packet <packet> --accepted-by "<named person>" --reason "<why>"
111
+ ```
112
+
113
+ This tests nothing and places no orders. Polish and QA read `baseline_accepted`,
114
+ which is never `completed`. What they held moves to history, so its purchase
115
+ proof stops counting. After that:
116
+
117
+ - A page-only change owes QA on that page only. A passing `qa run --page` leaves
118
+ QA at `baseline_accepted`, because it proved those pages and not the funnel.
119
+ - A commerce-affecting change owes the full Polish and QA.
120
+ - A CampaignSpec or brief change owes the stages again.
121
+ - Doctor warns `qa.baseline_accepted`, and `doctor --ci`, readback and `next` all
122
+ say QA rests on a baseline and no purchase has been tested since.
123
+ - A completed full `qa run` ends it. A blocked one leaves the baseline in place.
124
+
125
+ A funnel gets one baseline. The command refuses a funnel whose last QA was
126
+ blocked, one where no QA ever completed, one whose records have markup hashes,
127
+ and one where no full QA is owed. For the first two, run a full `qa run`. Once it
128
+ passes, page-only changes work without a baseline.
129
+
92
130
  The PR comment opens with the kind it found and what that owes, for example
93
131
  "Change found: page-only. It owes QA on `upsell1`; Polish is advisory." A
94
132
  commerce-affecting change names its reasons.
@@ -116,6 +116,37 @@ Generator checklist:
116
116
  These checks are the `CampaignCurrencies` validator rule, which
117
117
  `campaigns-os doctor` runs with the rest of the spec rules.
118
118
 
119
+ A page package row can also carry its price in each currency, as `prices[]`:
120
+
121
+ ```json
122
+ {
123
+ "ref_id": 101, "qty": 1, "price": "49.99", "price_retail": "79.99",
124
+ "prices": [
125
+ { "currency": "USD", "price": "49.99", "price_retail": "79.99" },
126
+ { "currency": "EUR", "price": "46.99", "price_retail": "74.99" }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ - `price` and `price_retail` stay the default currency's. Each `prices[]`
132
+ entry has a `currency` code and a `price`, plus `price_retail` and, for a
133
+ subscription package, `price_recurring` when the Campaigns App has them.
134
+ - The values come from the Campaigns API read in each currency
135
+ (`GET /campaigns/?currency=EUR`). That read returns only the packages the
136
+ Campaigns App has a price for in that currency, so a package with no EUR
137
+ price is missing from the campaign for a shopper paying in EUR. Map Builder
138
+ fills `prices[]` when it refreshes from the API; set prices in the
139
+ Campaigns App, not in the spec. `campaigns-os spec derive` refreshes `price`
140
+ and `price_retail` from one default-currency read and leaves `prices[]` as
141
+ it was, so after changing any price, refresh the Map in Map Builder: a
142
+ default-currency change otherwise leaves `price` and the default currency's
143
+ `prices[]` entry disagreeing.
144
+ - `prices[]` is optional. A row without it says nothing about other
145
+ currencies and is not checked. A row with it should list every currency in
146
+ `available_currencies` and `campaign.currency`; the `PackageCurrencyPrices`
147
+ rule warns on a missing currency, a malformed entry and a currency listed
148
+ twice.
149
+
119
150
  ## Order Bump Notes
120
151
 
121
152
  An order bump is still a checkout package/offer surface. It should be encoded as
package/docs/effects.md CHANGED
@@ -98,6 +98,14 @@ are on every row for those three commands. The effect test runs each row with
98
98
  the one condition whose `--proxy-base` is the loopback receiver, so the fetch
99
99
  and the write are observed rather than taken on trust.
100
100
 
101
+ `spec fetch` sends the same request and writes the same file, only when the
102
+ packet's copy is missing, and it never replaces one. It also writes the target's
103
+ `.gitignore` when the managed block is missing or still lists `fetched-specs/`.
104
+
105
+ `record baseline` writes only the Assembly Report and sends nothing. It is tier C
106
+ because it replaces the live Polish and QA records, so any purchase proof they
107
+ held stops counting. History keeps them.
108
+
101
109
  ### The live campaign read
102
110
 
103
111
  `doctor --packet` (with or without `--write` / `--no-write`) and every `qa run`
@@ -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.3.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.62.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
6
+ npm init -y && npm install --save-exact next-campaign-page-kit@0.3.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.66.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.62.0`
29
+ Supported surface at generation time: `1.66.0`
30
30
 
31
31
  ## Forward compatibility
32
32
 
@@ -421,10 +421,11 @@ can load with `?currency=GBP` and still take a USD order:
421
421
  price and recurring-price claims are not compared with the run-currency
422
422
  totals: they count as unresolved, and the commercial report records
423
423
  `unresolved_money_claims_reason: "static_source_not_in_run_currency"`. A
424
- correctly built multi-currency page gets no `price-claim-mismatch` warning,
425
- and the commercial status reads `incomplete` instead of `pass`. With
426
- `--currency` set to the Map's own currency, the claims are compared as
427
- before.
424
+ correctly built multi-currency page gets no `price-claim-mismatch` warning.
425
+ The commercial status reads `incomplete` instead of `pass`, and a
426
+ `commercial-parity:incomplete:static_source_not_in_run_currency` warn row says
427
+ what was not compared. With `--currency` set to the Map's own currency, the
428
+ claims are compared as before.
428
429
 
429
430
  Either blocker makes the verdict `blocked` and the command exit `4`. Without
430
431
  `--currency` neither row is written and expected totals use
@@ -480,16 +481,49 @@ claims per document, 256 claims across the run, 1 MiB per price-preview
480
481
  response, 256 calculate scenarios, four concurrent proxy requests, and 20
481
482
  seconds per request. A limit, missing key, malformed response,
482
483
  or unavailable page records incomplete commercial evidence; it never invents
483
- a mismatch. `commercial.status = "incomplete"` preserves the disposition
484
- derived from the ordinary QA assertions; it does not create an exception by
485
- itself. QA dispositions remain `ready`, `ready_with_exceptions`, or `blocked` —
486
- `ready_with_waivers` is the doctor/`next` checkpoint-readiness term.
484
+ a mismatch. QA dispositions remain `ready`, `ready_with_exceptions`, or
485
+ `blocked` — `ready_with_waivers` is the doctor/`next` checkpoint-readiness term.
486
+
487
+ `commercial.status = "incomplete"` makes the verdict at least
488
+ `ready_with_exceptions`. It stays warn-only: it never makes a run `blocked` and
489
+ never changes the exit code. Each cause writes one warn row in family `pricing`,
490
+ `commercial-parity:incomplete:<code>`, for example
491
+ `commercial-parity:incomplete:missing_campaigns_api_key`. The row's `actual` says
492
+ what was not compared and how to fix it, so an agent reading only `exceptions[]`
493
+ sees the gap. The committed sidecar keeps the id, which names the cause. The report
494
+ repeats the causes as `commercial.incomplete_reasons`, and `qa run` prints them under
495
+ its commercial line. A cause an earlier one explains writes no second row, so a
496
+ missing key does not also report every page as unpriced.
497
+
498
+ Read the row for what to do, not the code: the set of codes is not a contract and
499
+ can change. Not every cause has a repair. A store coupon, two bound prices on one
500
+ page, a run in another currency and a funnel over the check's limits only say what
501
+ this run did not compare. A `commercial-currency:<RUN>:<ANSWERED>` blocker and a
502
+ `commercial_parity.recurring_claim_absent` warning are their own rows and add no
503
+ `commercial-parity:incomplete:` row.
504
+
505
+ Bound prices are read two ways, because the two bindings mean different things in
506
+ the source. `bundle.<id>.price` is compared as authored in the source HTML, against
507
+ the page's calculated total. `package.price` and `package.<ref>.price` are compared
508
+ as rendered, because what the source holds there is the template's placeholder (a
509
+ starter upsell ships `$9.99`). Under `qa run --browser`, QA reads each visible node
510
+ once the SDK has written it, which it knows by the `display-visible` class the SDK
511
+ adds, and compares it with calculate's `unit_price` for that package's row on the
512
+ page: one unit, after offers. So a page printing `package.price`, which is the list
513
+ price, beside a live offer gets a `price-claim-mismatch` warning. The package is the
514
+ id in the path, or else the nearest `data-next-package-id`, `data-next-package` or
515
+ `data-package-id`, which is how the SDK scopes it. The source is only read to learn
516
+ that these claims exist, so a run without `--browser` reports them as
517
+ `package_price_not_rendered`. `verdict.commercial.package_price_claims` counts them.
518
+ Under a `--currency` that is not the Map's they stay unresolved, like the source
519
+ prices above, because the browser pass does not load pages in the run currency.
487
520
 
488
521
  Only contract-governed claims are compared, and only against `Exact` normalized
489
522
  truth. Proven differences emit warn-severity `pricing` assertions named
490
523
  `price-claim-mismatch`, `cadence-disclosure-mismatch`, or
491
- `voucher-not-applied`. Decorative, ambiguous, stale, unresolved, or malformed
492
- claims remain silent. A page showing a voucher the normalized plan cannot price
524
+ `voucher-not-applied`. Decorative claims are ignored, and ambiguous, stale,
525
+ unresolved, or malformed claims never read as a mismatch: they leave the proof
526
+ incomplete, as above. A page showing a voucher the normalized plan cannot price
493
527
  (no calculated pair for its code, for example a live voucher on an upsell)
494
528
  leaves that page's price claims unresolved: the plan's only Exact truth for
495
529
  the page is then the list price, which a voucher-priced page is expected to
package/docs/readback.md CHANGED
@@ -525,13 +525,20 @@ the build a stage reviewed are on that stage instead. An owed stage carries:
525
525
  `stages.assembly.build_changes` plus the drift on disk. `kind` is `unknown`
526
526
  when that history does not reach the reviewed build (reason
527
527
  `change_history_unavailable`) or the drift cannot be read. `null` on a stage
528
- that is not owed.
528
+ that is not owed. It also carries `commerce_rebuild` when a recorded rebuild
529
+ since that build was not page-only: `{ "recorded_at", "build_fingerprint",
530
+ "kind", "latest_change_kind" }`.
529
531
  - Polish only, `advisory`: `true` when the change is `page_only` and no brief or
530
532
  spec change requires Polish. Advisory Polish is reported; `next`, `doctor
531
533
  --ci` and the QA gate do not require it.
532
534
  - QA only, `scope` and `owed_pages`: `pages` with the changed pages when the
533
535
  change is `page_only` (`qa run --page` pays it), `full` otherwise, `null`
534
536
  when QA is not owed.
537
+ - QA only, `reviewed_at`: when the completed QA the change is read from was
538
+ checked. Absent when QA did not complete.
539
+ - QA only, `baseline`: `{ "accepted_by", "accepted_at", "reason",
540
+ "build_fingerprint" }` while QA rests on a baseline (`record baseline`), which
541
+ is not a test. Present on a QA that is current as well as one that is owed.
535
542
 
536
543
  If recording removed a
537
544
  stage binding entirely, that stage reads unknown unless its recorded status is
@@ -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.3`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.62.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.66.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -81,7 +81,7 @@ 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 50 enumerated files:
84
+ Fingerprint algorithm `sha256`, over 51 enumerated files:
85
85
 
86
86
  - `campaign-spec/analytics-vendors.ts`
87
87
  - `campaign-spec/analytics-vocabulary.ts`
@@ -109,6 +109,7 @@ Fingerprint algorithm `sha256`, over 50 enumerated files:
109
109
  - `campaign-spec/rules/index.ts`
110
110
  - `campaign-spec/rules/offer-ref-integrity.ts`
111
111
  - `campaign-spec/rules/one-checkout-per-path.ts`
112
+ - `campaign-spec/rules/package-currency-prices.ts`
112
113
  - `campaign-spec/rules/package-pricing-sanity.ts`
113
114
  - `campaign-spec/rules/page-count.ts`
114
115
  - `campaign-spec/rules/page-id-uniqueness.ts`
@@ -140,7 +141,7 @@ The output directory is a build product. It is untracked and git-ignored, no com
140
141
 
141
142
  Directory `campaign-spec/dist`. Committed: `false`. Type entry `campaign-spec/dist/index.d.ts`.
142
143
 
143
- 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 92 files.
144
+ 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 94 files.
144
145
 
145
146
  ### Mandatory checks
146
147
 
@@ -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.62.0+skills.1"
19
+ "bundle_revision": "1.66.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.62.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.66.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.62.0+skills.1
40
+ Bundle revision: 1.66.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.62.0+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.66.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.62.0+skills.1",
93
+ "requested": "1.66.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.62.0+skills.1",
95
+ "on_disk": "1.66.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.62.0+skills.1)"
97
+ "message": "match (1.66.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.62.0+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.62.0+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.62.0+skills.1)
104
+ Skills revision: match (1.66.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.66.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.66.0+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an
@@ -14,7 +14,7 @@ implementation detail, however stable it looks.
14
14
  | Surface | Contract | Change discipline |
15
15
  |---|---|---|
16
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
- | 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. |
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; 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. `record baseline` (added in 1.66.0) accepts a handed-off funnel from before markup hashes as the starting point for page-only changes, one time per funnel, without testing it. `spec fetch` (added in 1.64.0) puts a saved-Map packet's CampaignSpec back on a checkout that lacks it, never replacing a copy on disk. `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. |
20
20
  | `bin/campaigns-os.mjs` (`campaigns-os`) | The CLI entry itself. | Declared in `package.json` `bin`; the gate fails if it disappears. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.62.0",
3
+ "version": "1.66.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -254,6 +254,21 @@
254
254
  "name": { "type": ["string", "null"] },
255
255
  "price": { "type": ["string", "number", "null"] },
256
256
  "price_retail": { "type": ["string", "number", "null"] },
257
+ "prices": {
258
+ "type": ["array", "null"],
259
+ "description": "The package's price in each currency the Campaigns App prices it in, one entry per currency, read from the Campaigns API (GET /campaigns/?currency=<code>), which returns only the packages priced in the requested currency. price and price_retail stay the default currency's. Optional: a row without prices (absent or null) says nothing about other currencies. A row with prices should list every currency in campaign.available_currencies and campaign.currency; the PackageCurrencyPrices rule warns when one is missing, since the campaign leaves the package out in that currency.",
260
+ "items": {
261
+ "type": "object",
262
+ "additionalProperties": true,
263
+ "required": ["currency", "price"],
264
+ "properties": {
265
+ "currency": { "type": "string", "description": "Three-letter upper-case ISO 4217 code." },
266
+ "price": { "type": ["string", "number"] },
267
+ "price_retail": { "type": ["string", "number", "null"] },
268
+ "price_recurring": { "type": ["string", "number", "null"] }
269
+ }
270
+ }
271
+ },
257
272
  "product_name": { "type": ["string", "null"] },
258
273
  "product_sku": { "type": ["string", "null"] },
259
274
  "product_variant_name": { "type": ["string", "null"] },
@@ -254,6 +254,21 @@
254
254
  "name": { "type": ["string", "null"] },
255
255
  "price": { "type": ["string", "number", "null"] },
256
256
  "price_retail": { "type": ["string", "number", "null"] },
257
+ "prices": {
258
+ "type": ["array", "null"],
259
+ "description": "The package's price in each currency the Campaigns App prices it in, one entry per currency, read from the Campaigns API (GET /campaigns/?currency=<code>), which returns only the packages priced in the requested currency. price and price_retail stay the default currency's. Optional: a row without prices (absent or null) says nothing about other currencies. A row with prices should list every currency in campaign.available_currencies and campaign.currency; the PackageCurrencyPrices rule warns when one is missing, since the campaign leaves the package out in that currency.",
260
+ "items": {
261
+ "type": "object",
262
+ "additionalProperties": true,
263
+ "required": ["currency", "price"],
264
+ "properties": {
265
+ "currency": { "type": "string", "description": "Three-letter upper-case ISO 4217 code." },
266
+ "price": { "type": ["string", "number"] },
267
+ "price_retail": { "type": ["string", "number", "null"] },
268
+ "price_recurring": { "type": ["string", "number", "null"] }
269
+ }
270
+ }
271
+ },
257
272
  "product_name": { "type": ["string", "null"] },
258
273
  "product_sku": { "type": ["string", "null"] },
259
274
  "product_variant_name": { "type": ["string", "null"] },
@@ -94,7 +94,7 @@
94
94
  "disposition": {
95
95
  "type": "string",
96
96
  "enum": ["ready", "ready_with_exceptions", "blocked"],
97
- "description": "Derived from the assertions (and the commercial section's status): any unwaived failing blocker => blocked; any warn/manual_review/waived assertion or a proven commercial mismatch => ready_with_exceptions; otherwise ready. Waived is never clean. ready_with_waivers is the doctor/next checkpoint term, never a verdict disposition."
97
+ "description": "Derived from the assertions (and the commercial section's status): any unwaived failing blocker => blocked; any warn/manual_review/waived assertion, a proven commercial mismatch or incomplete commercial coverage => ready_with_exceptions; otherwise ready. Waived is never clean. ready_with_waivers is the doctor/next checkpoint term, never a verdict disposition."
98
98
  },
99
99
  "assertions": {
100
100
  "type": "array",
@@ -417,7 +417,7 @@
417
417
  "status": {
418
418
  "type": "string",
419
419
  "enum": ["pass", "mismatch", "incomplete", "not_applicable", "not_run"],
420
- "description": "mismatch keeps the verdict at ready_with_exceptions; incomplete preserves the disposition derived from ordinary assertions and never invents an exception."
420
+ "description": "mismatch and incomplete each keep the verdict at ready_with_exceptions at least, and neither blocks it. incomplete also writes a warn commercial-parity:incomplete:<code> assertion for each named cause, and never invents a mismatch."
421
421
  },
422
422
  "coverage_complete": { "type": "boolean" },
423
423
  "findings": {
@@ -436,6 +436,21 @@
436
436
  }
437
437
  },
438
438
  "description": "Sanitized capture/proxy/limit issue codes with counts. An issue records incomplete evidence; it never invents a mismatch."
439
+ },
440
+ "incomplete_reasons": {
441
+ "type": "array",
442
+ "items": {
443
+ "type": "object",
444
+ "additionalProperties": true,
445
+ "required": ["code", "summary", "fix"],
446
+ "properties": {
447
+ "code": { "type": "string", "minLength": 1 },
448
+ "count": { "type": "integer" },
449
+ "summary": { "type": "string", "description": "What this run did not compare, and why." },
450
+ "fix": { "type": "string", "description": "What to do about it. Some causes have nothing to repair and say so." }
451
+ }
452
+ },
453
+ "description": "Present when status is incomplete: one entry per named cause, the same causes the commercial-parity:incomplete:<code> assertions carry. Empty when every cause already has a row of its own. Absent on verdicts emitted before the field existed."
439
454
  }
440
455
  },
441
456
  "description": "The compact commercial-parity report: counters over checked pages, scenarios, and claims, plus findings and issues. Counter fields are additive and not individually contracted here."
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-lifecycle-orientation
3
- version: 1.0.48
3
+ version: 1.0.52
4
4
  description: Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing any stage or changing any state.
5
5
  ---
6
6
 
7
- Bundle revision: 1.62.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.62.0+skills.1`
7
+ Bundle revision: 1.66.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.66.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-readback-classification
3
- version: 1.0.48
3
+ version: 1.0.52
4
4
  description: Classify a selected campaign from the readback projection's v2 fields and write a read-only handoff without turning diagnosis into permission.
5
5
  ---
6
6
 
7
- Bundle revision: 1.62.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.62.0+skills.1`
7
+ Bundle revision: 1.66.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.66.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read