@nextcommerce/campaigns-os 1.63.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 (50) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/campaign-spec/dist/types.d.ts +1 -1
  3. package/compatibility.json +1 -1
  4. package/contracts/effects.v1.json +129 -1
  5. package/contracts/release-ledger.json +549 -0
  6. package/contracts/supported-surface.json +3 -3
  7. package/docs/build-packet.md +31 -5
  8. package/docs/campaigns-os-build-flow.md +38 -0
  9. package/docs/campaignspec-authoring-examples.md +3 -2
  10. package/docs/effects.md +8 -0
  11. package/docs/local-setup.md +1 -1
  12. package/docs/orientation-contract-reference.md +1 -1
  13. package/docs/qa-and-test-orders.md +44 -10
  14. package/docs/readback.md +8 -1
  15. package/docs/runtime-readiness.md +1 -1
  16. package/docs/skills-revision.md +10 -10
  17. package/docs/supported-surface.md +1 -1
  18. package/package.json +1 -1
  19. package/schemas/campaigns-os-qa-verdict.v0.schema.json +17 -2
  20. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  21. package/skills/campaign-readback-classification/SKILL.md +3 -3
  22. package/skills/campaign-run-evidence/SKILL.md +3 -3
  23. package/skills/contribution-intake/SKILL.md +3 -3
  24. package/skills/next-campaigns-build/SKILL.md +3 -3
  25. package/skills/next-campaigns-os/SKILL.md +3 -3
  26. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  27. package/skills/next-campaigns-polish/SKILL.md +3 -3
  28. package/skills/next-campaigns-qa/SKILL.md +4 -4
  29. package/skills/next-campaigns-sdk-bump/SKILL.md +3 -3
  30. package/skills.json +11 -11
  31. package/src/change-scope.mjs +25 -6
  32. package/src/cli.mjs +120 -7
  33. package/src/commercial-journey.mjs +3 -0
  34. package/src/commercial-parity.mjs +157 -7
  35. package/src/doctor/checks.mjs +28 -3
  36. package/src/doctor/ci.mjs +36 -1
  37. package/src/evidence-debt.mjs +9 -1
  38. package/src/input-currency.mjs +4 -2
  39. package/src/input-refresh.mjs +1 -1
  40. package/src/invocation.mjs +3 -2
  41. package/src/orchestration-stage-contract.mjs +3 -1
  42. package/src/polish-gate.mjs +10 -0
  43. package/src/qa-browser.mjs +46 -0
  44. package/src/qa-commercial-parity.mjs +173 -18
  45. package/src/qa-node.mjs +10 -5
  46. package/src/qa-verdict.mjs +3 -1
  47. package/src/readback.mjs +1 -0
  48. package/src/runtime-state-ignore.mjs +44 -19
  49. package/src/stage-ledger.mjs +35 -9
  50. package/src/stage-record.mjs +93 -14
package/CHANGELOG.md CHANGED
@@ -2,6 +2,68 @@
2
2
 
3
3
  Notable supported-surface changes are recorded here.
4
4
 
5
+ ## [1.66.0] - 2026-11-07
6
+
7
+ Funnels handed off before 1.62.0 can now reach small checks without a test order. To take it, pin `@nextcommerce/campaigns-os@1.66.0`. Nothing changes for a funnel unless a named person runs the new command.
8
+
9
+ ### Added
10
+
11
+ - **`record baseline` (#716).** `campaigns-os record baseline --packet <packet> --accepted-by "<named person>" --reason "<why>"` accepts a handed-off funnel from before markup hashes as the starting point, one time per funnel. It tests nothing and places no orders. After it, a page-only change owes QA on that page alone, and a commerce-affecting change still owes the full Polish and QA. It refuses a funnel whose last QA was blocked, one where no QA ever completed, one whose records have markup hashes, and one where no full QA is owed.
12
+ - **The stage status `baseline_accepted`.** Polish and QA take it from `record baseline`. It ends the ladder, so `next` moves on, but it is never `completed` and never hands a campaign off. What those stages held moves to history, so its purchase proof stops counting. A passing `qa run --page` leaves QA at this status, and only a completed full QA ends it. Progress snapshots read it as `unknown`.
13
+ - **Reports say what it is.** Doctor warns `qa.baseline_accepted`, and `doctor --ci`, readback and `next` say QA rests on a baseline and no purchase has been tested since. `evidence_debt.qa` gains `baseline`, and the Assembly Report gains a top-level `commerce_baseline`, which keeps the status, warnings and skip reason of what it replaced.
14
+
15
+ ## [1.65.0+agent.1] - 2026-11-07
16
+
17
+ Commercial parity now checks `package.price` as well as `bundle.*.price`. Starter upsells use `package.price`, so a funnel could pass every calculate scenario with no printed price compared. No pin change is needed to read verdicts; the behavior ships in the next release.
18
+
19
+ ### Changed
20
+
21
+ - **`package.price` and `package.<ref>.price` are compared as rendered.** Under `qa run --browser`, each visible node the SDK has filled is compared with calculate's `unit_price` for that package's row on the page. The source holds a template placeholder (`$9.99` on a starter upsell), so the source text is never compared. A difference is the existing warn-only `price-claim-mismatch`. That includes a page printing `package.price`, the list price, beside a live offer.
22
+ - **What cannot be compared is named.** A run without `--browser`, a rendered page that showed none of the claims, a visible node the SDK never wrote, or a claim with no package id, no single exact row or unreadable text each writes a `commercial-parity:incomplete:<code>` warn row and never a mismatch. A run without `--browser` on a page with these claims now reads `incomplete`, so it stays at `ready_with_exceptions`.
23
+ - `verdict.commercial.package_price_claims` counts these claims, and they count toward `extracted_price_claims`, `compared_price_claims` and `unresolved_price_claims`. Journey rows from the `./commercial-journey` export carry `unit_price` when calculate sends it, and captures from `./commercial-parity` carry `package_price_claims`.
24
+
25
+ ## [1.65.0] - 2026-11-07
26
+
27
+ A QA run whose commercial parity stopped short no longer reads as a clean result. Commercial status `incomplete` now keeps the verdict at `ready_with_exceptions` and names the cause and the fix in `exceptions[]`. It stays warn-only: nothing new blocks, and the exit code does not change. To take it, pin `@nextcommerce/campaigns-os@1.65.0` and refresh the installed skills to `1.65.0+skills.1`.
28
+
29
+ ### Changed
30
+
31
+ - **Incomplete commercial coverage is an exception.** A spec with no `campaign.campaigns_api_key`, for example, used to run 0 of its calculate scenarios and could still report `ready`, with the gap visible only under `verdict.commercial`. The verdict is now `ready_with_exceptions`, with a warn row `commercial-parity:incomplete:missing_campaigns_api_key` whose `actual` says what was not compared and how to fix it.
32
+ - A cause an earlier one already explains writes no second row, so a missing key does not also report every page as unpriced. Read the row for what to do: the set of codes is not a contract.
33
+ - Not every cause has a repair. A store coupon, two bound prices on one page, a run in a currency other than the Map's and a funnel over the check's limits say what this run did not compare, and these runs stay at `ready_with_exceptions`.
34
+ - The committed sidecar keeps the row's id, which names the cause. The verdict's `commercial` section gains `incomplete_reasons` with the same causes, and `qa run` prints them under its commercial line.
35
+ - A run that was `ready` only because the gap was invisible now records its QA stage as `completed_with_warnings`, and its Run Record lists `pricing` among its gap classes. Both dispositions still end a run session.
36
+ - **A crash in the commercial check is reported the same way.** It used to leave status `not_run` and the verdict untouched, with a line on stderr. It now reads `incomplete` with the cause `commercial_runner_error`. Runs a gate blocks keep `not_run`, since those verdicts are already `blocked`.
37
+ - The verdict schema's `disposition` and `commercial.status` descriptions say the above, and it documents `commercial.incomplete_reasons`.
38
+
39
+ ## [1.64.0] - 2026-11-07
40
+
41
+ Developers changing a live funnel from a fresh clone can now get going, and the reports say what they compared. To take it, pin `@nextcommerce/campaigns-os@1.64.0`, run `spec fetch` once on each repository set up from a saved Map, and commit the file it writes. These come from the first post-handoff trial on #716.
42
+
43
+ ### Added
44
+
45
+ - **`spec fetch` (#716).** `campaigns-os spec fetch --packet <packet>` restores a saved-Map packet's CampaignSpec on a checkout that lacks it, by the same fetch intake makes. It never replaces a copy on disk, because that copy can hold `spec derive` edits the Map doesn't have, and it doesn't write the packet or the records. Its `bound` result says whether the copy matches the spec the records were made against. A doctor `spec.local_path` error for a missing Map copy now names the command.
46
+ - **`built_output.absent` doctor warning (#716).** When the build stage is complete and `_site/<route>/` isn't there, doctor says the records were not compared with this checkout, and `next` says so at `done`. A fresh clone used to read as finished however much source had moved.
47
+
48
+ ### Changed
49
+
50
+ - **The fetched CampaignSpec is committed (#716).** `.campaign-runtime/fetched-specs/` leaves the managed `.gitignore` block, and an existing block loses the line on the next setup command or `spec fetch`. The committed records are bound to this file and `spec derive` writes into it, so another machine can't rebuild it from the Map once it has been derived.
51
+ - **`doctor --ci` names what it compared (#716).** Each owed stage's report line gives the build it last checked, with the date when QA completed. When the latest change alone is page-only but an earlier rebuild since that check wasn't, the report names that rebuild, so it no longer seems to contradict `record build`. `evidence_debt.<stage>.change` gains `commerce_rebuild`, and `evidence_debt.qa` gains `reviewed_at` when QA completed, each only when there is a value.
52
+ - **Clearer wording for a pre-1.62 build (#716).** `commerce_signature_unavailable` now reads as a build from before the markup hashes existed.
53
+
54
+ ### Fixed
55
+
56
+ - **The docs on pre-1.62 records (#716).** They said such a change reads as `unknown` until `record build` records a signature. It reads as `commerce_affecting`, and `record build` doesn't clear it. Page-only changes start after one full QA on 1.62.0 or later, and the docs now say so.
57
+
58
+ ## [1.63.0+agent.1] - 2026-11-02
59
+
60
+ The `PagePackage` type accepts `prices: null`, as the schemas and the `PackageCurrencyPrices` rule already did. No pin change is needed to read specs; TypeScript callers that pass `null` stop failing type checks on the next release.
61
+
62
+ ### Fixed
63
+
64
+ - `PagePackage.prices` is typed `PackageCurrencyPrice[] | null`, like the package's other nullable fields. Schemas v4 and v5 allow `null`, and the rule treats it as absent, so a spec that validated could fail to type-check. (#714)
65
+ - The authoring guide's `prices[]` section says to refresh the Map after any price change: `spec derive` refreshes `price` and `price_retail` but not `prices[]`, so a default-currency change otherwise leaves `price` and that currency's `prices[]` entry disagreeing.
66
+
5
67
  ## [1.63.0] - 2026-10-31
6
68
 
7
69
  CampaignSpec can carry a page package's price in each currency the campaign sells in, and validation warns when a package has no price in one of them. To take it, pin `@nextcommerce/campaigns-os@1.63.0` and refresh the installed skills to `1.63.0+skills.1`; the skill text changes only in its stated revision.
@@ -133,7 +133,7 @@ export interface PagePackage {
133
133
  * that carries it should list every currency the campaign sells in
134
134
  * (PackageCurrencyPrices).
135
135
  */
136
- prices?: PackageCurrencyPrice[];
136
+ prices?: PackageCurrencyPrice[] | null;
137
137
  product_name?: string | null;
138
138
  product_sku?: string | null;
139
139
  product_variant_name?: string | null;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@nextcommerce/campaigns-os",
3
- "version": "1.63.0",
3
+ "version": "1.66.0",
4
4
  "status": "developer-preview",
5
5
  "contracts": {
6
6
  "campaign_spec": "4.2-5.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": "campaigns-os-effects/v1",
3
3
  "_note": "The declared effect of every supported invocation: what it writes, what it sends, and the test case that proves it. One row per command, per subcommand and per effect-changing flag (vocabulary.effect_changing_flags). A row is not publishable without its effect test — scripts/check-effects.mjs refuses one, and refuses a flag from that list that a help usage line carries and no row declares. Read this with docs/effects.md.",
4
- "contract_version": "1.3.0",
4
+ "contract_version": "1.5.0",
5
5
  "vocabulary": {
6
6
  "annotations": {
7
7
  "readOnlyHint": "The invocation changes nothing: no file under the target, the working directory or the user's machine, and no request off the machine. The command-lifecycle journal counts — a journal append is a write like any other, so every readOnlyHint: true row is an invocation the CLI exempts from lifecycle capture (the converse does not hold: some journal-exempt invocations, such as demo and the --no-write forms, still write other declared files).",
@@ -356,6 +356,67 @@
356
356
  "test_scope": "full",
357
357
  "notes": "page-kit sync --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
358
358
  },
359
+ {
360
+ "command": "spec",
361
+ "subcommand": "fetch",
362
+ "flags": [],
363
+ "annotations": {
364
+ "readOnlyHint": false,
365
+ "destructiveHint": false,
366
+ "openWorldHint": true,
367
+ "idempotentHint": true
368
+ },
369
+ "tier": "A",
370
+ "writes": [
371
+ {
372
+ "path": "{target}/.campaign-runtime/fetched-specs/*.json",
373
+ "when": "the packet's saved-Map copy is not on this checkout; a copy already there is never replaced and nothing is sent",
374
+ "observed_in": [
375
+ "no_session",
376
+ "ambient_session",
377
+ "stale_session",
378
+ "lifecycle_log",
379
+ "persisted_consent"
380
+ ]
381
+ },
382
+ {
383
+ "path": "{target}/.gitignore",
384
+ "when": "the command succeeds and the managed block is missing, lacks an entry the toolkit requires, or still lists fetched-specs/",
385
+ "observed_in": [
386
+ "no_session",
387
+ "stale_session",
388
+ "lifecycle_log",
389
+ "persisted_consent"
390
+ ]
391
+ },
392
+ {
393
+ "path": "{lifecycle-journal}",
394
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG",
395
+ "observed_in": [
396
+ "ambient_session",
397
+ "lifecycle_log"
398
+ ]
399
+ }
400
+ ],
401
+ "sends": [
402
+ {
403
+ "destination": "{proxy-base}/api/spec/{map-id}",
404
+ "what": "the CampaignSpec fetch",
405
+ "when": "the packet's saved-Map copy is not on this checkout",
406
+ "requires_consent": false,
407
+ "observed_in": [
408
+ "no_session",
409
+ "ambient_session",
410
+ "stale_session",
411
+ "lifecycle_log",
412
+ "persisted_consent"
413
+ ]
414
+ }
415
+ ],
416
+ "effect_test": "effects: spec fetch",
417
+ "test_scope": "full",
418
+ "notes": "Fills a gap only: the copy on disk can hold spec derive edits the Map does not. The packet and the records are not written; a Map that has moved since they were made is reported as bound: differs."
419
+ },
359
420
  {
360
421
  "command": "spec",
361
422
  "subcommand": "derive",
@@ -6214,6 +6275,73 @@
6214
6275
  "test_scope": "full",
6215
6276
  "notes": "record brief --dry-run runs every check the real invocation runs and prints the outcome and the files it would write; it writes nothing (no packet, brief, Build Context, report or stale stamp), takes no target lock, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
6216
6277
  },
6278
+ {
6279
+ "command": "record",
6280
+ "subcommand": "baseline",
6281
+ "flags": [],
6282
+ "annotations": {
6283
+ "readOnlyHint": false,
6284
+ "destructiveHint": true,
6285
+ "openWorldHint": false,
6286
+ "idempotentHint": false
6287
+ },
6288
+ "tier": "C",
6289
+ "writes": [
6290
+ {
6291
+ "path": "{runtime}/assembly-report.json",
6292
+ "when": "the record passes every check — stages.polish and stages.qa become baseline_accepted with what they held moved to history, and commerce_baseline names who accepted which build",
6293
+ "observed_in": [
6294
+ "no_session",
6295
+ "ambient_session",
6296
+ "stale_session",
6297
+ "lifecycle_log",
6298
+ "persisted_consent"
6299
+ ]
6300
+ },
6301
+ {
6302
+ "path": "{runtime}/doctor-output.json",
6303
+ "when": "a retained doctor sidecar exists — the record postdates the doctor snapshot, so it is stamped stale",
6304
+ "observed_in": [
6305
+ "no_session",
6306
+ "ambient_session",
6307
+ "stale_session",
6308
+ "lifecycle_log",
6309
+ "persisted_consent"
6310
+ ]
6311
+ },
6312
+ {
6313
+ "path": "{lifecycle-journal}",
6314
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG",
6315
+ "observed_in": [
6316
+ "ambient_session",
6317
+ "lifecycle_log"
6318
+ ]
6319
+ }
6320
+ ],
6321
+ "sends": [],
6322
+ "effect_test": "effects: record baseline",
6323
+ "test_scope": "full",
6324
+ "notes": "One time per funnel, for a handed-off funnel whose records predate markup hashes. Tests nothing and sends nothing. It replaces the live Polish and QA records, so any purchase proof they held stops counting; history keeps them."
6325
+ },
6326
+ {
6327
+ "command": "record",
6328
+ "subcommand": "baseline",
6329
+ "flags": [
6330
+ "--dry-run"
6331
+ ],
6332
+ "annotations": {
6333
+ "readOnlyHint": true,
6334
+ "destructiveHint": false,
6335
+ "openWorldHint": false,
6336
+ "idempotentHint": true
6337
+ },
6338
+ "tier": "none",
6339
+ "writes": [],
6340
+ "sends": [],
6341
+ "effect_test": "effects: record baseline --dry-run",
6342
+ "test_scope": "full",
6343
+ "notes": "record baseline --dry-run runs every check the real invocation runs and prints the files it would write; it writes nothing, takes no target lock, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
6344
+ },
6217
6345
  {
6218
6346
  "command": "record",
6219
6347
  "subcommand": "spec",