@nextcommerce/campaigns-os 1.54.0 → 1.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/CONTEXT.md +19 -5
  3. package/README.md +26 -12
  4. package/campaign-spec/README.md +15 -1
  5. package/campaign-spec/dist/checkout-flow.d.ts +90 -0
  6. package/campaign-spec/dist/checkout-flow.js +229 -0
  7. package/campaign-spec/dist/index.d.ts +1 -0
  8. package/campaign-spec/dist/index.js +6 -1
  9. package/campaign-spec/dist/normalize.d.ts +3 -2
  10. package/campaign-spec/dist/normalize.js +7 -5
  11. package/campaign-spec/dist/routing.d.ts +6 -3
  12. package/campaign-spec/dist/routing.js +6 -3
  13. package/campaign-spec/dist/rules/checkout-forward-target.d.ts +17 -0
  14. package/campaign-spec/dist/rules/checkout-forward-target.js +48 -0
  15. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.d.ts +12 -0
  16. package/campaign-spec/dist/rules/checkout-step-reaches-checkout.js +37 -0
  17. package/campaign-spec/dist/rules/exit-intent-validation.js +2 -1
  18. package/campaign-spec/dist/rules/index.d.ts +4 -0
  19. package/campaign-spec/dist/rules/index.js +12 -0
  20. package/campaign-spec/dist/rules/one-checkout-per-path.d.ts +25 -0
  21. package/campaign-spec/dist/rules/one-checkout-per-path.js +84 -0
  22. package/campaign-spec/dist/rules/pre-payment-forward-target.d.ts +12 -0
  23. package/campaign-spec/dist/rules/pre-payment-forward-target.js +42 -0
  24. package/campaign-spec/dist/rules/promo-code-input-validation.js +2 -1
  25. package/campaign-spec/dist/rules/schema-version.d.ts +9 -4
  26. package/campaign-spec/dist/rules/schema-version.js +10 -5
  27. package/campaign-spec/dist/types.d.ts +20 -3
  28. package/campaign-spec/dist/types.js +3 -2
  29. package/compatibility.json +2 -2
  30. package/contracts/fixtures/runtime-recipe/accept/current.json +6 -1
  31. package/contracts/fixtures/runtime-recipe/accept/minimal.json +6 -1
  32. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  33. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +6 -1
  34. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +6 -1
  35. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +6 -1
  36. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +6 -1
  37. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +6 -1
  38. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +1 -1
  39. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +6 -1
  40. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +6 -1
  41. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +6 -1
  42. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +5 -0
  43. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +6 -1
  44. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +6 -1
  45. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +6 -1
  46. package/contracts/release-ledger.json +1146 -0
  47. package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
  48. package/contracts/supported-surface.json +9 -5
  49. package/docs/brand-theme-bridge.md +10 -0
  50. package/docs/build-packet.md +86 -26
  51. package/docs/campaigns-os-build-flow.md +1 -1
  52. package/docs/effects.md +2 -5
  53. package/docs/local-setup.md +1 -1
  54. package/docs/orientation-contract-reference.md +1 -1
  55. package/docs/qa-and-test-orders.md +133 -29
  56. package/docs/runtime-readiness.md +8 -3
  57. package/docs/skills-revision.md +10 -10
  58. package/docs/supported-surface.md +1 -1
  59. package/docs/versioning.md +4 -1
  60. package/docs/workflow-findings-sidecar.md +57 -2
  61. package/package.json +4 -1
  62. package/schemas/campaign-runtime-build-context.v0.schema.json +3 -1
  63. package/schemas/campaign-runtime-build-packet.v0.schema.json +3 -1
  64. package/schemas/campaign-spec.v5.schema.json +586 -0
  65. package/schemas/campaigns-os-run-record.v0.schema.json +20 -15
  66. package/skills/campaign-lifecycle-orientation/SKILL.md +10 -8
  67. package/skills/campaign-readback-classification/SKILL.md +3 -3
  68. package/skills/campaign-run-evidence/SKILL.md +3 -3
  69. package/skills/contribution-intake/SKILL.md +3 -3
  70. package/skills/next-campaigns-build/SKILL.md +6 -4
  71. package/skills/next-campaigns-os/SKILL.md +3 -3
  72. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  73. package/skills/next-campaigns-polish/SKILL.md +21 -6
  74. package/skills/next-campaigns-qa/SKILL.md +5 -4
  75. package/skills.json +10 -10
  76. package/src/adapter-decision-contract.mjs +27 -13
  77. package/src/brand-theme.mjs +44 -8
  78. package/src/build-brief.mjs +14 -5
  79. package/src/campaign-identity.mjs +33 -4
  80. package/src/cli-helpers.mjs +129 -0
  81. package/src/cli.mjs +156 -111
  82. package/src/commercial-journey.mjs +25 -2
  83. package/src/consent.mjs +3 -2
  84. package/src/design-source-package.mjs +1 -1
  85. package/src/design-source-publication.mjs +8 -8
  86. package/src/deviation.mjs +2 -1
  87. package/src/doctor/checks.mjs +212 -19
  88. package/src/doctor/inspect.mjs +4 -2
  89. package/src/doctor/next-step.mjs +46 -16
  90. package/src/doctor-sidecar.mjs +5 -3
  91. package/src/gate-actions.mjs +21 -1
  92. package/src/input-refresh.mjs +6 -6
  93. package/src/invocation.mjs +4 -3
  94. package/src/lifecycle.mjs +88 -6
  95. package/src/local-preview-policy.mjs +116 -12
  96. package/src/login.mjs +2 -1
  97. package/src/polish-browser.mjs +8 -2
  98. package/src/polish-node.mjs +9 -4
  99. package/src/polish-readability.mjs +2 -2
  100. package/src/qa-browser.mjs +741 -120
  101. package/src/qa-cart-entry.mjs +107 -20
  102. package/src/qa-node.mjs +179 -72
  103. package/src/qa-test-order-topology.mjs +118 -1
  104. package/src/qa-verdict-discovery.mjs +21 -4
  105. package/src/qa-verdict-publish.mjs +2 -1
  106. package/src/run-record.mjs +46 -34
  107. package/src/runtime-state-ignore.mjs +7 -5
  108. package/src/sdk-markup.mjs +41 -1
  109. package/src/source-html-intake.mjs +46 -4
  110. package/src/stage-ledger.mjs +18 -4
  111. package/src/stage-record.mjs +53 -16
@@ -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
@@ -341,6 +365,24 @@ npm run campaigns-os -- qa run \
341
365
 
342
366
  The runner fetches deployed pages, checks route availability, verifies CampaignSpec `sdk_hints.meta_tags` (a key the Campaign Cart SDK does not read, `next-currency` or `next-predictive-address` from `src/sdk-meta-tags.mjs`, is a `warn` row at severity `warn` carrying the shared note, never `manual_review` and never a blocker, whether or not the tag rendered; doctor reports the same key as `sdk_hints.meta_tags.ignored_by_sdk`), writes a local verdict JSON under `<target-repo>/qa-output/<qa-storage-key>/<run-id>.json` (the packet's `assembly.target_repo`, else the packet's directory; `--output-dir` overrides it, and a packet-less run uses `qa-output/` under the current directory), and returns exit code `4` when the verdict is blocked. The storage key is the Map ID for saved-Map QA and `local-spec-<local_spec_id>` for local-spec packet QA. The target's managed ignore block lists `qa-output/`, because full verdicts carry live storefront URLs; the committed form is the `.campaign-runtime/qa-verdict.json` projection.
343
367
 
368
+ `--currency <code>` proves one currency per run: the runner adds
369
+ `?currency=<CODE>` to each funnel's entry URL (the HTTP fetch, the browser load
370
+ and `entry_urls`) and to the first page load of each test-order path (the
371
+ checkout probe, or the entry page or checkout when that load comes first),
372
+ keeping any query or tracking params already there. Campaign Cart keeps the URL
373
+ currency for the rest of that tab's session, so the runner adds it to no other
374
+ load (upsell pages and the receipt reload in recovery go without it); a later
375
+ load of the entry URL still carries it, because that URL is tagged. The flag takes one code per run (the
376
+ shared parser keeps the last one if it is given twice). The code must be three
377
+ letters (upper-cased: `gbp` runs as `GBP`); anything else is refused before any
378
+ request. A campaign set up for three currencies takes three runs. `qa parity`
379
+ refuses `--currency`, and so does a run whose orders come from
380
+ `--legacy-api-test-order`, since a direct API order would be placed in the
381
+ default currency. The persisted verdict redacts every URL query, so the
382
+ currency is recorded as `evidence.currency` on the entry page's
383
+ `http:<page_id>` assertion; an entry URL that does not parse gets no currency,
384
+ and its row records `evidence.currency_not_applied` with the reason instead.
385
+
344
386
  ### Automatic commercial parity
345
387
 
346
388
  When the CampaignSpec has enabled pages with package rows, the same `qa run`
@@ -804,7 +846,7 @@ prints the portal link.
804
846
  The outcome lands on the run's Run Record as the `qa_verdict_publish` block —
805
847
  `verdict_run_id`, `publisher` (`qa run` or `qa publish`), `state`
806
848
  (`skipped` / `ok` / `failed`), `result` in the remit vocabulary, `base_kind`,
807
- `published_at` — on the record whose `qa_verdict` artifact references the
849
+ `published_at`, and the nullable publish decision `reason` — on the record whose `qa_verdict` artifact references the
808
850
  verdict under the packet's campaign. `qa run` records its own publish (or
809
851
  its `--no-post-verdict` skip) the same way when the run session closes, which
810
852
  is what `already_published` reads. A stored `ok` is never downgraded: a
@@ -1183,8 +1225,8 @@ Fixture essentials:
1183
1225
 
1184
1226
  Test Orders use **global test cards** that work on any live store and integration.
1185
1227
  They **bypass the payment gateway and create no transactions** (and no fulfillment),
1186
- so they are safe to run any time and need **no permission flags, packet policy,
1187
- merchant sandbox routing, or test-order approval** — you just pick a mode. They leave a small,
1228
+ but each creates a real order record on the store. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing test orders; then proceed. No permission flag,
1229
+ packet policy, or merchant sandbox routing is required. They leave a small,
1188
1230
  easy-to-clean footprint: Test orders are deletable in bulk, and the resulting
1189
1231
  Customer record is reused (see the test email note below) rather than multiplied.
1190
1232
 
@@ -1428,7 +1470,21 @@ timeout still leaves the ladder up to the point of failure. Ladder entries carry
1428
1470
  object. Evidence is resolved even when the step fails or times out, because the
1429
1471
  failing path is the one worth reading.
1430
1472
 
1431
- 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.
1432
1488
 
1433
1489
  **`entered_via_landing` — cart entry.** The first rung of the ladder, before
1434
1490
  `opened_checkout`. A checkout renders its customer form whether or not the SDK
@@ -1472,7 +1528,13 @@ checkout — never a receipt or an offer page), navigates there, waits for the
1472
1528
  SDK, and clicks the cart-entry control: an SDK add-to-cart control
1473
1529
  (`[data-next-action="add-to-cart"]`, the only attribute the SDK activates the
1474
1530
  feature on), or a link into the checkout URL carrying `?forcePackageId=`,
1475
- 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
1476
1538
  spelled any other way is not a cart entry, so a page that offers nothing else
1477
1539
  fails the step by name (`cart_entry_control_missing`) rather than clicking a
1478
1540
  control the SDK never wired and waiting out the navigation budget. A visible
@@ -1490,8 +1552,9 @@ cart away. `opened_checkout` then records the arrival instead of re-opening.
1490
1552
 
1491
1553
  Evidence: `landing_url`, `landing_page_id`, `landing_page_type`,
1492
1554
  `landing_resolution` (`routes_into_checkout`, `entry_page_fallback`,
1493
- `first_page_fallback`), `control_text`, `control_kind` (`add_to_cart` or
1494
- `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
1495
1558
  `checkout_selection_surface` probe result, `selection_surface_probe`
1496
1559
  (`loaded`, `reused`, or `failed`), and `selection_surface_probe_error` when it
1497
1560
  failed. The failure codes are
@@ -1527,6 +1590,21 @@ own 30s default, so a caller-supplied budget only ever tightens the ceiling. A
1527
1590
  stuck required field fails as that field rather than as an anonymous step
1528
1591
  timeout; a slow-but-working funnel waits no longer than it did before.
1529
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
+
1530
1608
  **`cart_created` — cart-API observation.** The step reports what the cart API
1531
1609
  actually returned: the most recent `POST /api/v1/carts/` response's `status`, an
1532
1610
  `ok` flag, `line_count` when the response body exposes lines, `response_count`,
@@ -1566,17 +1644,43 @@ Two flags target funnels the default-tier drive cannot prove:
1566
1644
 
1567
1645
  - `--select-package <ref[:qty],...>` — **strict** package/bundle card selection.
1568
1646
  Each ref is matched against rendered selector/bundle cards
1569
- (`[data-next-package-id]`, `[data-next-bundle-card][data-next-bundle-id]`) and
1570
- clicked. An explicit quantity requires one unambiguous rendered card whose
1647
+ (`[data-next-package-id]`, `[data-next-bundle-card][data-next-bundle-id]`, and
1648
+ the package a bundle card's `data-next-bundle-items` declares, so
1649
+ `--select-package 1:2` finds the card declaring
1650
+ `[{"packageId":1,"quantity":2}]` whether or not an inner node repeats the
1651
+ package id) and clicked. An explicit quantity requires one unambiguous rendered card whose
1571
1652
  `data-next-bundle-items` composition contains exactly that package and
1572
- quantity; package identity alone is not enough. The card must then expose a
1653
+ quantity; package identity alone is not enough. A card declaring several
1654
+ items matches by any one of them (`4:1` finds a kit declaring
1655
+ `[{"packageId":1,"quantity":2},{"packageId":4,"quantity":1}]`); a card
1656
+ declaring the item alone is preferred over one declaring it among others. The card must then expose a
1573
1657
  selected-state marker (`data-next-selected="true"` / `.next-selected`) after
1574
1658
  the click. A missing card, ambiguous composition, wrong quantity, or
1575
1659
  unverifiable/refused selection **fails the `selected_bundle` step** instead
1576
1660
  of silently driving the pre-selected default tier. A bundle ref remains an
1577
1661
  authoritative identity when exactly one rendered card declares it. Use this
1578
1662
  flag to traverse non-default tiers of a multi-tier selector. `--cart` remains
1579
- the best-effort variant.
1663
+ the best-effort variant. For a package that bundle cards declare, `--cart
1664
+ <ref:qty>` clicks the card whose items carry that package at that quantity,
1665
+ and clicks nothing when the selected card already does; refs no bundle card
1666
+ declares (an order bump) are clicked as before. The cards are read again
1667
+ before each ref, so a selector that re-renders after a click is clicked as
1668
+ it now stands. Beside `--select-package`,
1669
+ `--cart` never clicks another card in the chosen card's selector group, and
1670
+ a card in no recognised selector container
1671
+ (`[data-next-bundle-selector]`, `[data-next-selector-id]`,
1672
+ `[data-next-cart-selector]`) counts as in that group: a
1673
+ ref the chosen card carries is left as it is, a ref an order bump or a card
1674
+ in another selector group carries is applied through that control, and any
1675
+ other ref is left unapplied and named in the `selected_bundle` detail
1676
+ (`--cart 3:1 not applied: ...`). The display-parity row reconciles the order
1677
+ against the packages and quantities every selected bundle card declares, not
1678
+ its `data-next-bundle-id`. Without `--select-package` it also fails when an
1679
+ explicit `--cart <ref:qty>` is not in the order at that quantity: a
1680
+ different quantity (`--cart 1:2` that ordered one unit) or no line for the
1681
+ package at all (`--cart 1:2,3:1` on a swap-mode selector that kept only the
1682
+ `3:1` card); beside `--select-package` it judges the order against the card
1683
+ that flag chose.
1580
1684
  - `--apply-coupon <code>` — types the code into the rendered coupon/promo input
1581
1685
  (the SDK's `[data-next-checkout-field="coupon"]` or
1582
1686
  `input[data-next-coupon="input"]`, then hand-rolled `coupon`/`voucher`/`promo`
@@ -1709,7 +1813,7 @@ browser launch, prints the planned count, lists the planned paths (up to 40 ids;
1709
1813
  past that the remainder is counted, never cut silently, and `--select-package
1710
1814
  <ref[:qty]>` lists one tier's paths), and names the exact `--max-test-orders <count>` raise. For example, a linear three-offer graph has
1711
1815
  eight terminal paths plus the checkout baseline, so it requires
1712
- `--max-test-orders 9`. No approval step is involved.
1816
+ `--max-test-orders 9`. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing these real store order records; then proceed. The command has no permission flag.
1713
1817
 
1714
1818
  `--max-test-orders` bounds **planned paths**, which is not the same as real
1715
1819
  purchases. `--max-order-creations` bounds **actual order creations**, defaults to
@@ -1849,7 +1953,7 @@ and hardcoded phone numbers that differ from CampaignSpec `campaign.store_phone`
1849
1953
  If a static claim is intentionally preserved, wrap it in an element with
1850
1954
  `data-skip-market-lint="true"` and record why in the assembly report.
1851
1955
 
1852
- Test orders themselves need no allowlist or approval. A separate concern is the
1956
+ Test orders need no allowlist and no permission flag; unless the operator has already said test orders are fine for this campaign, ask once, up front, before the first one (each is a real order record). A separate concern is the
1853
1957
  **SDK origin allowlist**: the Campaign Cart SDK must be allowed to load on the
1854
1958
  tested origin for the campaign API key, or runtime checks (and the live page
1855
1959
  itself) may not initialize. Localhost on any port is globally available as a
@@ -1864,8 +1968,8 @@ npm run campaigns-os -- qa policy set \
1864
1968
  --allowed-domains-confirmed true
1865
1969
  ```
1866
1970
 
1867
- There is no permission flag for test orders — they run from `--test-order
1868
- <mode>` alone. The former `--test-orders-allowed` /
1971
+ There is no permission flag for test orders; unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing these real store order records, then run `--test-order
1972
+ <mode>`. The former `--test-orders-allowed` /
1869
1973
  `--sandbox-test-card-confirmed` flags and the `qa.test_orders_allowed` /
1870
1974
  `qa.sandbox_test_card_confirmed` packet fields they set were removed in
1871
1975
  supported surface 1.28.0: nothing had read their values since the gate itself
@@ -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.54.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.54.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.54.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.54.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.54.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.54.0+skills.1",
93
+ "requested": "1.57.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.54.0+skills.1",
95
+ "on_disk": "1.57.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.54.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.54.0+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.54.0+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.54.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). |
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`
@@ -51,7 +51,8 @@ own schemas and evolve independently). Instead it carries:
51
51
 
52
52
  - **Stable envelope** — `schema_version` (`campaigns-os-run-record/v0`),
53
53
  `run_id`, package version, the command that ran, an `argv` *shape* (flag names
54
- present, not raw values), `created_at`, consent state, and remit status.
54
+ present, not raw values), `created_at`, consent state, remit status, and
55
+ optional `closed_by` (`run_end`, `qa_auto_end`, `stale_sweep`, or `manual`).
55
56
  - **Run identity** — `map_id`, `campaign_slug`, `template_family`,
56
57
  `entry_point_shape`. (Best-effort; missing identity never blocks capture.)
57
58
  - **Source artifact refs** — for the Build Packet, Build Context, Assembly
@@ -64,6 +65,17 @@ own schemas and evolve independently). Instead it carries:
64
65
  this run.
65
66
  - **Findings snapshot** — this run's Workflow Findings (see channel below).
66
67
 
68
+ When a QA verdict is present, `observations.qa.verdict_run_id` names the same
69
+ verdict that supplies `disposition`. `build_fingerprint` is the QA stage's
70
+ recorded build fingerprint only when the Assembly Report binds that stage to
71
+ the same verdict; otherwise it is `null`. `stale` compares that fingerprint
72
+ with the Assembly Report's recorded build fingerprint using the same comparison
73
+ as `next`: `true` when they differ, `false` when they match, and `null` when
74
+ either fingerprint is unknown. A rebuild without `record build` therefore reads
75
+ `stale: false` when the recorded fingerprints still match; doctor's
76
+ `built_output.fingerprint_stale` checks the output on disk. All three fields
77
+ are optional for older records; without a verdict, `observations.qa` is absent.
78
+
67
79
  ### Run identity
68
80
 
69
81
  A single canonical `campaigns_os_run_id` is minted at the run boundary and
@@ -78,6 +90,45 @@ Stage timings and repair-loop count are captured from the command lifecycle
78
90
  journal when a run session or explicit lifecycle journal is active. They remain
79
91
  best-effort signal: telemetry records the commands Campaigns OS can observe, not
80
92
  every thought, browser click, or external editor action in an agent session.
93
+ Each new journal entry records `wait_ms` as non-negative whole milliseconds
94
+ blocked on terminal input; an invocation without a prompt records 0. Legacy
95
+ entries may omit it or hold null, and Run Record stages preserve unknown wait
96
+ as absent or null rather than inventing 0. `duration_ms` still includes prompt
97
+ wait, so observed command work is `duration_ms - wait_ms` when wait is known. Tier-2 phases carry their
98
+ own measured wait when the prompt occurs while that phase is active; the journal
99
+ also retains the invocation total. Nested prompt hooks count their outer wait
100
+ interval once. Authentication prompts are outside lifecycle capture by policy.
101
+
102
+ On a non-zero doctor or QA exit, optional `finding_codes` is an array of at most
103
+ five distinct string codes in first-seen order; null is invalid. Codes belong
104
+ to the phase active when they were recorded. Codes recorded outside any phase,
105
+ and codes in a legacy entry, are invocation-level only; aggregation assigns them
106
+ to the entry's last phase. A successful stage has no finding codes. The optional run-level
107
+ `needs_input_count` and `failure_count` are non-negative integer counts; null is
108
+ invalid and absence means a legacy record did not report the count.
109
+ `needs_input_count` counts journal entries with exit 2 and `failure_count`
110
+ counts entries with any other non-zero exit. Current command
111
+ paths use 0 for success, 1 for operational errors, 2 for missing or blocked
112
+ inputs, and 4 for blocked QA; the other-non-zero rule also covers a future
113
+ status without reclassifying exit 2. Both counts are per command invocation,
114
+ including a command that emits multiple Tier-2 stages. `repair_loop_count`
115
+ continues to count repeated commands plus explicit repair-loop hooks.
116
+
117
+ | Terminal input site | Wait hook | Capture context |
118
+ |---|---|---|
119
+ | Consent question (`src/consent.mjs`) | Around the injectable `ask` call | Active command, including `run-record` |
120
+ | Store question (`src/login.mjs`) | Around readline `question` | Authentication runs outside lifecycle capture |
121
+ | Finding stage, kind, summary, details (`src/cli.mjs`) | Around each readline `question` | Active `findings add` command; omitted fields do not prompt |
122
+
123
+ | Non-zero finding source | Codes recorded |
124
+ |---|---|
125
+ | `doctor` | `result.errors[].code` before rendering and exit 2 |
126
+ | `start` and `build` | Their embedded doctor `errors[].code` before the prepare result sets exit 2 |
127
+ | `qa run` and `qa parity` | Blocked verdict assertions: `evidence.code`, or assertion `id` when no code is present, before returning from QA dispatch with exit 4 |
128
+
129
+ Spec resolution exceptions and prepare-build without a doctor result do not
130
+ expose doctor or QA codes. They keep their existing exit and timing evidence,
131
+ with no invented finding code.
81
132
 
82
133
  ### Validation
83
134
 
@@ -246,7 +297,11 @@ remit(path, payload, proxyBase) // mirrors qa-node.mjs postVerdict
246
297
  for a later post of the stored verdict), `attempted` / `ok` / `error` /
247
298
  `endpoint` (`/api/qa/verdicts`), a `state` (`skipped` when the run's
248
299
  publish was off, `ok`, `failed`), the `result` in the same vocabulary as
249
- `remit_result`, the `base_kind`, and `published_at`. `qa run` hands the
300
+ `remit_result`, the `base_kind`, `published_at`, and optional `reason`.
301
+ The reason records the actual QA publish decision: `loopback_base_url`,
302
+ `portal_managed_default`, `consent_off`, `flag_opt_out`, `flag_opt_in`,
303
+ `default`, or `local_spec`. A later `qa publish` has no QA run decision and
304
+ records a null reason. `qa run` hands the
250
305
  block to the session through its QA attempt, so `run end` and the auto-end
251
306
  stamp it; `qa publish` stamps the record whose `qa_verdict` artifact
252
307
  references the verdict, reads `state: "ok"` as already published, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.54.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",
@@ -66,6 +67,8 @@
66
67
  "check:spec": "node --test \"campaign-spec/test/**/*.test.ts\"",
67
68
  "check": "npm run check:provenance && npm run check:workflows && npm run check:changelog-structure && npm run build:spec && npm run check:tests && npm run check:spec && npm run check:fixtures && npm run check:legacy-migration && npm run check:spec-conformance && npm run check:private-strings && npm run check:template-doctrine && npm run check:slot-manifest && npm run check:skill-versions && npm run check:cart-readiness && npm run check:sidecar-bundle && npm run check:supported-surface && npm run check:effects && npm run check:release-ledger && npm run check:runtime-recipe && npm run check:orientation-docs && npm run check:runtime-docs && npm run check:pack -- --skip-prepare",
68
69
  "check:tests": "node ./scripts/check-tests.mjs",
70
+ "check:fast": "node ./scripts/check-fast.mjs",
71
+ "hooks:install": "node ./scripts/install-git-hooks.mjs",
69
72
  "check:legacy-migration": "node ./scripts/check-legacy-migration.mjs",
70
73
  "check:fixtures": "node ./scripts/check-fixtures.mjs",
71
74
  "check:spec-conformance": "node ./scripts/check-campaign-spec-conformance.mjs",
@@ -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
  },