@nextcommerce/campaigns-os 1.43.1 → 1.46.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 (92) hide show
  1. package/AGENTS.md +9 -2
  2. package/CHANGELOG.md +1099 -5103
  3. package/README.md +34 -13
  4. package/agents/claude/CLAUDE.md +1 -1
  5. package/agents/codex/AGENTS.md +1 -1
  6. package/agents/copilot/copilot-instructions.md +1 -1
  7. package/agents/cursor/campaigns-os.mdc +1 -1
  8. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
  9. package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
  10. package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
  11. package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
  12. package/campaign-spec/dist/types.d.ts +2 -2
  13. package/contracts/agent-relevant-change-policy.v1.json +5 -0
  14. package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
  15. package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
  16. package/contracts/effects.v1.json +1184 -121
  17. package/contracts/orientation-reason-codes.v1.json +7 -0
  18. package/contracts/release-ledger.json +2190 -5260
  19. package/contracts/supported-surface.json +7 -4
  20. package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
  21. package/docs/brand-theme-bridge.md +81 -0
  22. package/docs/build-packet.md +222 -23
  23. package/docs/campaigns-os-build-flow.md +4 -3
  24. package/docs/design-source-package.md +162 -15
  25. package/docs/effects.md +66 -12
  26. package/docs/gateway-login.md +3 -0
  27. package/docs/local-setup.md +1 -1
  28. package/docs/orientation-contract-reference.md +42 -2
  29. package/docs/polish-evidence.md +74 -0
  30. package/docs/progress-snapshots.md +10 -6
  31. package/docs/qa-and-test-orders.md +230 -20
  32. package/docs/release-ledger-authoring-guide.md +70 -8
  33. package/docs/runtime-readiness.md +1 -1
  34. package/docs/sdk-storage-compatibility.md +1 -1
  35. package/docs/skills-revision.md +10 -10
  36. package/docs/supported-surface.md +2 -2
  37. package/docs/versioning.md +4 -1
  38. package/package.json +1 -1
  39. package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
  40. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
  41. package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
  42. package/skills/campaign-readback-classification/SKILL.md +3 -3
  43. package/skills/campaign-run-evidence/SKILL.md +7 -6
  44. package/skills/contribution-intake/SKILL.md +3 -3
  45. package/skills/next-campaigns-build/SKILL.md +7 -6
  46. package/skills/next-campaigns-os/SKILL.md +7 -7
  47. package/skills/next-campaigns-os/references/session-intake.md +9 -3
  48. package/skills/next-campaigns-os-setup/SKILL.md +5 -5
  49. package/skills/next-campaigns-polish/SKILL.md +28 -9
  50. package/skills/next-campaigns-qa/SKILL.md +7 -4
  51. package/skills.json +10 -10
  52. package/src/brand-theme.mjs +320 -20
  53. package/src/build-brief.mjs +6 -4
  54. package/src/built-script-syntax.mjs +480 -0
  55. package/src/built-site-scope.mjs +16 -4
  56. package/src/campaigns-api-key.mjs +99 -0
  57. package/src/cli-helpers.mjs +118 -0
  58. package/src/cli.mjs +1530 -7580
  59. package/src/commercial-parity.mjs +48 -2
  60. package/src/design-source-package.mjs +1 -1
  61. package/src/design-source-publication.mjs +898 -0
  62. package/src/deviation.mjs +13 -1
  63. package/src/diagnostic.mjs +6 -2
  64. package/src/directory-lock.mjs +270 -0
  65. package/src/doctor/checks.mjs +4654 -0
  66. package/src/doctor/inspect.mjs +678 -0
  67. package/src/doctor/next-step.mjs +731 -0
  68. package/src/doctor/source-provenance.mjs +184 -0
  69. package/src/install-invocation.mjs +29 -0
  70. package/src/invocation.mjs +183 -0
  71. package/src/live-campaign-refs.mjs +466 -0
  72. package/src/login.mjs +2 -2
  73. package/src/page-kit-store-profile.mjs +69 -12
  74. package/src/page-kit-sync.mjs +31 -12
  75. package/src/private-template-source.mjs +1 -1
  76. package/src/progress-node.mjs +9 -36
  77. package/src/proof-policy.mjs +1 -1
  78. package/src/qa-analytics-correctness.mjs +3 -0
  79. package/src/qa-binding-evidence.mjs +76 -11
  80. package/src/qa-browser.mjs +1316 -105
  81. package/src/qa-build-scope.mjs +47 -0
  82. package/src/qa-commercial-parity.mjs +48 -5
  83. package/src/qa-node.mjs +339 -19
  84. package/src/qa-test-order-topology.mjs +148 -0
  85. package/src/sdk-markup.mjs +72 -8
  86. package/src/source-html-intake.mjs +117 -1
  87. package/src/source-html-manifest.mjs +9 -2
  88. package/src/stage-ledger.mjs +28 -0
  89. package/src/stage-record.mjs +551 -0
  90. package/src/target-lock.mjs +54 -0
  91. package/src/template-brand-contract.mjs +17 -1
  92. package/src/upsell-selector-scope.mjs +112 -2
@@ -1,5 +1,13 @@
1
1
  # QA And Test Orders
2
2
 
3
+ For packet-based partial builds, QA honors the Assembly Report's recorded
4
+ `stages.prepare_build.declared_out_of_scope` declarations together with the
5
+ packet's skip mappings. Unbuilt declared pages emit `skipped` evidence with
6
+ reason `out_of_build_scope`; HTTP, browser and commercial checks do not request
7
+ those routes, and entry URLs come from the remaining pages. A materialized
8
+ stock page rejoins QA. Missing in-scope pages still fail normally. A raw skip
9
+ mapping without a recorded declaration does not suppress checks.
10
+
3
11
  The public v0 QA runner is Node/npm-based and does not require access to a private runtime repo.
4
12
 
5
13
  > **Commerce QA requires network; it cannot run in a no-outbound sandbox.** The SDK, product images, fonts, the Netlify preview, and the Playwright typed-card test order all need outbound network. A build environment without it can only validate markup/build/CSS — the commerce runtime and the typed-card test order (the Campaigns OS control) must be deferred to a deployed preview. Always run the QA runner against a `--base-url` preview/production origin (e.g. `npm run campaigns-os -- qa run --packet campaign-runtime.build.json --base-url https://deploy-preview-7--your-site.netlify.app/ --browser --test-order common`); never report commerce-runtime QA as passed from an offline build.
@@ -238,12 +246,18 @@ before QA with the relevant gate ID:
238
246
  ```bash
239
247
  campaigns-os checkpoint waive \
240
248
  --packet campaign-runtime.build.json \
241
- --gate <page_kit.store_profile|page_kit.sdk_version|polish.hidden_eager_media|built_output.upsell_selector_scope> \
249
+ --gate <page_kit.store_profile|page_kit.sdk_version|polish.hidden_eager_media|built_output.upsell_selector_scope|source_html.producer_provenance> \
250
+ [--page <page_id>] \
242
251
  --reason "<why>" \
243
252
  --waived-by "<named human>" \
244
253
  --review-condition "<specific re-evaluation trigger>"
245
254
  ```
246
255
 
256
+ `--page` is required for `source_html.producer_provenance`, which is waived one
257
+ Figma-typed page at a time (see the
258
+ [Design Source Package](./design-source-package.md) hand-written HTML route),
259
+ and refused for the other gates.
260
+
247
261
  Legacy source/theme/QA waiver commands and artifact lanes remain in place until
248
262
  those gates are registered. Store Profile evidence includes only the governed
249
263
  nine-field matrix plus status, normalized slug, relative target path,
@@ -424,7 +438,8 @@ work that lands with the receiver's connection contract. This section
424
438
  documents the semantics of the stamps the receiver already applies.
425
439
 
426
440
  Add `--browser --test-order common` for the normal proof pass: first-party
427
- Playwright browser checks plus the default typed-card order sample. If the
441
+ Playwright browser checks plus the default `common` typed-card orders
442
+ (described under "Test Orders" below). If the
428
443
  browser binary is missing, the CLI will prompt you to run
429
444
  `npm run qa:install-browser`:
430
445
 
@@ -444,6 +459,30 @@ machine-checkable `agentContract.qaStructure` selectors in the commerce surface
444
459
  catalog. If the family contract is silent, the assertion returns
445
460
  `manual_review`, not `pass`; if declared required structure is missing, it
446
461
  soft-fails with warning severity so the verdict becomes `ready_with_exceptions`.
462
+ The checkout wrapper and page composition are source-owned, so QA checks what
463
+ the checkout does rather than family class names. Family shell is a fixed list:
464
+ `.checkout-wrapper`, `.checkout-layout__left`, `.checkout-layout__right`,
465
+ `.checkout__layout`, `.checkout__column--left`, `.checkout__column--right` and
466
+ the family include's `[data-next-component="shipping-field-row"]` marker. When
467
+ family shell is all that is missing, the row is `warn` if the checkout also
468
+ passes three behaviour checks: a `<form data-next-checkout="form">` exists;
469
+ `email`, `fname`, `lname`, `country`, `address1`, `city`, `province` and
470
+ `postal` are each an input, select or textarea carrying that
471
+ `data-next-checkout-field` inside the form, not a `type="hidden"` input or a
472
+ disabled control (visibility is not required, so a field hidden until a country
473
+ is chosen still counts); and a cart-summary total
474
+ (`[data-next-display="cart.total"]` or
475
+ `[data-next-cart-summary] .order-totals__value--total`) is visible with text.
476
+ If any behaviour check fails, or no checkout form is found, the row stays
477
+ `fail`. Any other missing selector is always `fail`: the SDK selectors
478
+ (`[data-next-checkout="form"]`, `[os-checkout-payment]`,
479
+ `[data-next-cart-summary]`, `[data-next-bundle-slots-for]`) and any class the
480
+ list does not name, such as a hosted payment field class. `evidence.behaviour` records
481
+ the three checks and each `evidence.checks[]` entry carries `kind`
482
+ (`family_shell` or `sdk_wiring`).
483
+ The upsell and checkout bundle price checks count the SDK's
484
+ `[data-next-bundle-display*='price']` alongside the contract's price rows; a
485
+ hidden, zero-size or empty bundle-display node does not count.
447
486
  Promoted template families must also have
448
487
  `contracts/template-brand-contract.<family>.v0.json`; QA emits a blocker if the
449
488
  selected family is missing its brand/residue/pricing contract instead of
@@ -793,9 +832,12 @@ retire that guard once #36 ships and `cartLines` is populated.
793
832
  Analytics correctness has two deliberately separate evidence phases in one QA
794
833
  run:
795
834
 
796
- 1. The campaign-root visit inventories declared providers, containers, pixels,
797
- and other observable tags. It does not prove or disprove Purchase, even if a
798
- stray Purchase-shaped event appears there.
835
+ 1. The inventory visit inventories declared providers, containers, pixels,
836
+ and other observable tags on one page: the campaign root, or a built entry
837
+ when the root cannot be captured (see
838
+ [Which page the inventory captures](#which-page-the-inventory-captures)).
839
+ It does not prove or disprove Purchase, even if a stray Purchase-shaped
840
+ event appears there.
799
841
  2. The existing canonical typed-card order run supplies Purchase evidence. For
800
842
  each planned order, the topology classifier must recognize the final URL as
801
843
  that plan's receipt, then the runner waits the full `--analytics-settle`
@@ -839,6 +881,98 @@ analytics block to gate. The SDK's own data layer is a separate, always-on
839
881
  reading taken on the same order — see [Purchase data layer](#purchase-data-layer-dl_purchase)
840
882
  under Test Orders.
841
883
 
884
+ ### Which page the inventory captures
885
+
886
+ The inventory starts at the campaign root composed from the campaign identity
887
+ (`public_route_slug` plus `route_root`). A partial build (a topology with a
888
+ partial build scope, or pages excluded from the build) may have no page there:
889
+ the root is then whatever the host answers, such as a directory index or a
890
+ generic fallback. So the root is visited only when it is in scope. On a full
891
+ build it always is; on a partial build it is in scope only when a built,
892
+ in-scope topology page is served at the root. When the root is out of scope,
893
+ answers non-2xx, or fails to load (a timeout, a refused connection), the leg
894
+ tries each funnel's built entry in turn: the first in-scope page on a partial
895
+ build (the same entry the partial-scope planner selects), otherwise the first
896
+ entry-like page. It captures the first page that answers 2xx. A response with no
897
+ HTTP status counts as an answer. A closed page or a disconnected browser is not
898
+ a per-page failure: it is the `analytics-correctness:runner` blocker.
899
+
900
+ `analytics-correctness:capture` records the page it used:
901
+ `evidence.capture_page` holds `url` (the URL requested, query redacted),
902
+ `source` (`campaign_root` or `built_entry`), `page_id`, `funnel_id` and
903
+ `http_status`. `evidence.final_url` is the page URL after redirects and
904
+ settling. When a built entry was used, `evidence.root_fallback` says why the
905
+ root was not: `reason` is `out_of_built_scope`, `non_2xx` (with the root's
906
+ `http_status`), or `navigation_error` (with its `error_code`).
907
+
908
+ When no page is captured, the leg emits `analytics-correctness:capture` alone,
909
+ with no per-vendor assertion measured against an empty page. There are two
910
+ outcomes, and `evidence.attempts` lists each page tried:
911
+
912
+ | `evidence.reason` | When | Result |
913
+ |---|---|---|
914
+ | `no_in_scope_page_captured` | The root is out of the built scope and no built entry other than the root is left to try, so nothing was loaded | `skipped`: there is no page whose tags could be measured |
915
+ | `no_capture_page_answered` | At least one page was tried, and every one answered non-2xx or failed to load | `FAIL`/`BLOCKER`: the declared analytics went unmeasured, so a later passing order cannot report the run ready |
916
+
917
+ Step routing is path-based in every certified family. Page-kit builds each
918
+ page to its own `<route>/index.html`, and QA strips the query string from a
919
+ CampaignSpec route. So `/campaign`, `/campaign/` and `/campaign/index.html` are
920
+ one page, and a query string does not name a different page. A topology page
921
+ whose own URL declares a query (for example `/campaign/?step=checkout`) is
922
+ still never merged into the root on its path alone. Unless its query is
923
+ exactly the root's own (parameter order aside), it does not put the root in
924
+ scope, it is captured as its own entry, and its `capture_page` carries
925
+ `query_routed: true`, since the redacted URL alone would read as the root. An
926
+ entry on a different path is never marked `query_routed`, whatever query it
927
+ carries. A URL with no query of its own names the page at that path whatever
928
+ query the other URL carries.
929
+
930
+ ### Local-serve review (`manual_review`)
931
+
932
+ A local proof run renders the development environment on purpose (see
933
+ [Local proof mode](#local-proof-mode-deploytarget-local-serve)), and the
934
+ starter templates gate every vendor loader out of that render. A pixel that
935
+ did not fire there is the render's design, not a campaign defect. So on a
936
+ local-serve run, a failing fire-dependent check becomes `manual_review` at
937
+ `warn` instead of a blocker. The fire-dependent checks are
938
+ `analytics-correctness:tag:*`, `analytics-correctness:oob:*` and
939
+ `analytics-correctness:purchase-fires`.
940
+
941
+ The run qualifies only when all of these hold:
942
+
943
+ - the packet's `deploy.target` is `local-serve`;
944
+ - the analytics capture target (else the base URL) is a loopback URL;
945
+ - the Assembly Report records the development render:
946
+ `stages.assembly.evidence.build_environment` is `development`. A production
947
+ build served on localhost, or a build whose environment was never recorded,
948
+ keeps its blockers.
949
+
950
+ Each failing check is then downgraded only when the page it measured is on
951
+ record as loopback. For `tag:*` and `oob:*`, the check's own URL, the passing
952
+ capture's `capture_page.url` and its `final_url` must all be loopback, so a
953
+ built-entry fallback on a remote host, or a localhost root that redirected to
954
+ a production host, keeps the blocker. For `purchase-fires`, there must be at
955
+ least one judged receipt, and every one needs a loopback `receipt_url` and
956
+ `receipt_document_url` (the page URL read after the receipt analytics
957
+ settled). A missing or unparseable URL keeps the blocker.
958
+
959
+ What always stays a blocker:
960
+
961
+ - `analytics-correctness:data-layer-purchase:<path>`. It counts the SDK's own
962
+ `dl_purchase`, which the development render still pushes, so a miss on
963
+ localhost can be a real defect.
964
+ - A capture or runner failure: `analytics-correctness:runner`, a check whose
965
+ evidence carries an `error_code`, and a `purchase-fires` reading whose
966
+ `capture_error_plan_ids` is missing or not empty. The environment explains a
967
+ silent pixel, not an unmeasured one.
968
+
969
+ A downgraded check keeps its evidence and adds `reason:
970
+ local_serve_development_render`, `local_serve_status: fail`, the recorded
971
+ `build_environment`, the recorded `production_parity` (`status:
972
+ not_recorded` when none is on the report, with a note unless it passed), and a
973
+ `follow_up`: re-run `qa run` against the PR preview (a production render) with
974
+ `--base-url <preview-url>`. That run gates these checks.
975
+
842
976
  ## Analytics parity (dataLayer / GTM)
843
977
 
844
978
  The analytics-parity leg proves the live **dataLayer event stream + GTM/pixel
@@ -858,14 +992,22 @@ npm run campaigns-os -- qa run \
858
992
  ```
859
993
 
860
994
  This receipt-to-receipt parity example names `--analytics-candidate`
861
- explicitly. When that flag is omitted, the candidate is the campaign identity's
862
- composed root (`public_route_slug` plus `route_root`), not the raw
863
- `--base-url` value.
995
+ explicitly, and that URL is captured as given. When that flag is omitted, the
996
+ candidate is chosen the same way as the correctness inventory (see
997
+ [Which page the inventory captures](#which-page-the-inventory-captures)): the
998
+ campaign identity's composed root (`public_route_slug` plus `route_root`), not
999
+ the raw `--base-url` value, when it is in scope and answers 2xx, else the first
1000
+ built entry that does. `analytics-parity:capture` then records the same
1001
+ `capture_page` and `root_fallback` evidence. The candidate is captured before
1002
+ the baseline, and when no candidate page is captured the leg emits
1003
+ `analytics-parity:capture` alone, without loading the baseline:
1004
+ `no_in_scope_page_captured` (skipped) or `no_capture_page_answered`
1005
+ (`FAIL`/`BLOCKER`).
864
1006
 
865
1007
  | Flag | Meaning |
866
1008
  |---|---|
867
1009
  | `--analytics-baseline <url>` | Legacy funnel URL to capture as the parity baseline (enables the leg) |
868
- | `--analytics-candidate <url>` | Migrated URL to capture; defaults to the identity-composed campaign root |
1010
+ | `--analytics-candidate <url>` | Migrated URL to capture as given; defaults to the identity-composed campaign root, or the first built entry when that root is out of the built scope or does not answer |
869
1011
  | `--analytics-hosts a,b` | Extra host substrings to treat as analytics tag-fires (Everflow is built in) |
870
1012
  | `--analytics-settle <ms>` | Wait after analytics page loads and after a recognized typed-order receipt for async tags to fire (default 5000); receipt settling must fit inside the order deadline |
871
1013
 
@@ -996,13 +1138,64 @@ npm run campaigns-os -- qa run \
996
1138
  --test-order common
997
1139
  ```
998
1140
 
999
- The default mode is **`common`** (also what bare `--test-order` runs): at most
1000
- four shapes from the selected checkout's declared topology — the checkout
1001
- baseline, first-offer `accept` and `decline` when `expected_next_url` reaches an
1002
- upsell/downsell, and the shortest declared path that actually reaches a
1003
- receipt/thank-you page. The receipt path is deduplicated when it is already
1004
- `accept` or `decline`; Campaigns OS never invents a receipt path from offer
1005
- count alone. This is the everyday QA sample.
1141
+ The default mode is **`common`** (also what bare `--test-order` runs). It
1142
+ starts from the selected checkout's declared topology:
1143
+
1144
+ - When every actual terminal path (the `full` set, checkout baseline included)
1145
+ fits under the flood cap (`--max-test-orders`, default `6`), `common` runs
1146
+ them all. The run says so on stderr, and on a funnel with offer pages the
1147
+ coverage row below records
1148
+ `order_path_depth_effective: "full"` with `order_path_depth_reason:
1149
+ "under_cap"`.
1150
+ - Above the cap, or when the topology cannot be walked exhaustively, `common`
1151
+ runs the sample: the checkout baseline, first-offer `accept` and `decline`
1152
+ when `expected_next_url` reaches an upsell/downsell, and the shortest
1153
+ declared path that actually reaches a receipt/thank-you page (deduplicated
1154
+ when it is already `accept` or `decline`; Campaigns OS never invents a
1155
+ receipt path from offer count alone). It then adds, for each offer or
1156
+ downsell page whose decline no planned path clicks yet, the shortest actual
1157
+ terminal path that clicks it, until the plan reaches the cap. Pages still
1158
+ left out are named on stderr and in the coverage row. The sample itself is
1159
+ never trimmed, so a `--max-test-orders` below it is refused before launch as
1160
+ before.
1161
+
1162
+ A page counts as covered only when a path clicks its **decline** control.
1163
+ Reaching a page, or clicking only its accept, does not count: a broken decline
1164
+ link strands the shopper even when accept works.
1165
+
1166
+ Every browser test-order run whose funnels include offer pages records
1167
+ `browser-test-order:upsell-action-coverage`, read from the clicks the runner
1168
+ actually made in orders it placed, not from the plan. It lists the offer pages
1169
+ of every funnel in the run, including funnels the orders do not drive, and a
1170
+ click credits only the funnel whose order made it, never another funnel's page
1171
+ at the same URL. The row is conservative: it is `pass` or `warn` only when
1172
+ coverage is certain, and `manual_review` in every other case.
1173
+
1174
+ Coverage is certain when all of these hold: at least one order was placed;
1175
+ every funnel lists its pages; every page is either a known non-offer type
1176
+ (checkout, landing, thank-you and the like) or an upsell/downsell page with its
1177
+ own absolute http(s) URL that no other page shares; every planned order belongs
1178
+ to exactly one funnel (its checkout is that funnel's checkout and it was
1179
+ planned over that funnel's page list); and every click an order recorded lands
1180
+ on a declared offer page of that order's own funnel. Then the row is `warn`
1181
+ (severity `warn`) naming each page whose decline no order of its funnel
1182
+ clicked, and each funnel no order ran through, or `pass` when every page's
1183
+ decline was clicked.
1184
+
1185
+ Otherwise the row is `manual_review` (severity `warn`) naming the pages not
1186
+ proved clicked through, with `evidence.reason`: `test_orders_off` for a
1187
+ `--test-order off` run, `no_order_recorded` when no order was placed (an
1188
+ attempt that failed before an order reference counts as none), `no_topology`
1189
+ when no funnel topology reached the check, or `not_assessable` for the rest.
1190
+ `evidence.not_assessable[]` names each page that cannot be matched to a click,
1191
+ with a reason: `no_url` or `unresolvable_url`, `shared_url` (the URL belongs to
1192
+ another page too), `unknown_page_type` (neither an offer type nor a known
1193
+ non-offer type), or `no_pages` (a funnel with no page list).
1194
+ `evidence.uncertainty[]` lists the run-level reasons: `unattributed_plan` (a
1195
+ plan that does not match exactly one funnel's checkout and page list),
1196
+ `unattributed_click` (a click with no usable URL, or from such a plan's order)
1197
+ and `undeclared_click` (an order clicked a page its funnel does not declare).
1198
+ `evidence.pages[]` gives `accept_clicked` / `decline_clicked` per page.
1006
1199
 
1007
1200
  Other modes: `checkout` (base order redirect only), `accept`/`decline` (click the
1008
1201
  rendered control on the first upsell page), `both` (two fresh orders for those
@@ -1436,8 +1629,11 @@ npm run campaigns-os -- qa run \
1436
1629
  ```
1437
1630
 
1438
1631
  `--max-test-orders` (default `6`) is an **accidental-flood guard, not a permission
1439
- gate**. A single checkout's `common` sample always stays under it, though tier
1440
- expansion can exceed it. If `full` expands past the cap, the command stops before
1632
+ gate**. A single checkout's `common` plan always keeps its checkout,
1633
+ first-offer accept/decline and shortest-receipt sample. If that sample alone
1634
+ exceeds the cap (a `--max-test-orders` below it), the run is refused before
1635
+ browser launch, as before. Otherwise the added decline paths stop at the cap.
1636
+ Tier expansion can exceed it. If `full` expands past the cap, the command stops before
1441
1637
  browser launch, prints the planned count, lists the planned paths (up to 40 ids;
1442
1638
  past that the remainder is counted, never cut silently, and `--select-package
1443
1639
  <ref[:qty]>` lists one tier's paths), and names the exact `--max-test-orders <count>` raise. For example, a linear three-offer graph has
@@ -1618,8 +1814,9 @@ use the declared topology instead of a single happy path:
1618
1814
 
1619
1815
  1. Checkout-only with the base cart.
1620
1816
  2. Checkout-only with the base cart plus bump when the bump is in scope.
1621
- 3. Base cart through the checkout/first-action sample plus the shortest real
1622
- receipt path (`--test-order common` covers up to four deduplicated shapes).
1817
+ 3. Base cart through `--test-order common`: every actual terminal path when
1818
+ they fit under the cap, otherwise the checkout/first-action sample, the
1819
+ shortest real receipt path and one decline path per uncovered offer page.
1623
1820
  4. Base plus bump cart through the same sample matrix when bump behavior is
1624
1821
  launch-relevant.
1625
1822
  5. Use `full` when you want every actual terminal path, raising the flood cap to
@@ -1690,6 +1887,19 @@ review. Nested Google Maps/payment keys and inert HTML do not count as campaign
1690
1887
  credentials. This small static grammar deliberately leaves many real pages
1691
1888
  unknown; a literal inside arbitrary code is not proof of effective configuration.
1692
1889
 
1890
+ A page script that does not parse is not treated as dynamic. The browser throws
1891
+ a `SyntaxError` on it and nothing in it runs, so the binding reads its
1892
+ declarations as unavailable (`script_unavailable_or_limit`) and QA adds a
1893
+ separate `script-parse:<page_id>` blocker in the same `api-metadata` family.
1894
+ Its `actual` names each script by path (inline scripts as `inline script`)
1895
+ with the line and column, and its evidence lists a fixed diagnostic category
1896
+ per script, never text from the script. Classic scripts are parsed as scripts
1897
+ and `type="module"` scripts as modules, with the type stripped of surrounding
1898
+ ASCII whitespace and compared case-insensitively as the browser does; classic
1899
+ `nomodule` scripts are not fetched or parsed (a module script ignores
1900
+ `nomodule` and is parsed), and script srcs resolve against the page's first `<base href>`. Doctor runs the same parse over the built output before deploy; see
1901
+ `built_output.script_syntax` in [the Build Packet doc](build-packet.md).
1902
+
1693
1903
  External executable scripts other than the recognized jsDelivr Campaign Cart
1694
1904
  loader/index are inspected only on the page's origin. Each page admits at most
1695
1905
  6 such references; each run fetches at most 24 distinct URLs (deduplicated),
@@ -118,7 +118,8 @@ be recorded without the bytes actually moving somewhere.
118
118
  ### Fixes that touch only policy-ignored paths
119
119
 
120
120
  A fix living entirely in paths the policy ignores — `src/` other than
121
- `src/cli.mjs`, `scripts/`, tests and fixtures — carries a same-surface CHANGELOG
121
+ `src/cli.mjs`, `src/agent/` and `src/doctor/`, `scripts/`, tests and
122
+ fixtures — carries a same-surface CHANGELOG
122
123
  section (`X.Y.Z+agent.N`) and **no ledger entry**. There is nothing for an entry
123
124
  to claim: every change item must map to a classified changed path in the range,
124
125
  and an ignored path is never classified, so an entry written for such a PR is
@@ -127,9 +128,10 @@ path-less item fails the same way, because no classified change of its class
127
128
  exists in the range. The ignore list and its stated reasons are in
128
129
  [`contracts/agent-relevant-change-policy.v1.json`](../contracts/agent-relevant-change-policy.v1.json).
129
130
 
130
- The dividing line inside `src/` is `src/cli.mjs`: an explicit rule classifies it
131
- as `cli_surface`, so any change to it is agent-relevant and owes an entry, even
132
- when the behaviour change originates in a helper module beside it.
131
+ The classified paths inside `src/` are `src/cli.mjs`, `src/agent/` and
132
+ `src/doctor/`: an explicit rule classifies each as `cli_surface`, so any change
133
+ there is agent-relevant and owes an entry, even when the behaviour change
134
+ originates in a helper module beside it.
133
135
 
134
136
  ### Amendments
135
137
 
@@ -144,7 +146,7 @@ incomplete, append a correction:
144
146
  "amends": "RL-0007",
145
147
  "amendment_reason": "RL-0007 was recorded as compatible; it removed a documented guarantee.",
146
148
  "surface_version": null,
147
- "changelog_section": "1.15.0+agent.1",
149
+ "changelog_section": "1.16.0+agent.1",
148
150
  "compatibility": "breaking",
149
151
  "migration": "Stop relying on the removed guarantee; see docs/build-packet.md.",
150
152
  "agent_impact": "Treat the 1.15.0 packet doc change as breaking, not compatible.",
@@ -152,6 +154,10 @@ incomplete, append a correction:
152
154
  }
153
155
  ```
154
156
 
157
+ The amendment corrects a 1.15.0 entry, but its own section is a new one at the
158
+ very top of `CHANGELOG.md`, numbered on the release on top when it is written
159
+ (`1.16.0` here). It is never inserted under the older release it corrects.
160
+
155
161
  An amendment is the only entry kind whose change items may map to no changed
156
162
  path in its own range, because it corrects meaning rather than moving bytes.
157
163
 
@@ -169,13 +175,25 @@ entry currently holding the link, may re-link a section; any other second link
169
175
  still fails the one-to-one rule. Say in `amendment_reason` what changed in the
170
176
  section and why.
171
177
 
178
+ This works only for a section still in the live `CHANGELOG.md`. An archived
179
+ section cannot be corrected in place: archive files are never edited, and each
180
+ is pinned by its SHA-256 in `baseline_floor.archives`, so any change to one
181
+ fails the gate. Correct archived history with a new live amendment entry that
182
+ links its own new section at the top of `CHANGELOG.md`, and say in
183
+ `amendment_reason` what is wrong in the archived section.
184
+
172
185
  `scripts/check-changelog-structure.mjs` (part of `npm run check`) refuses the
173
186
  marker lines outright, in `CHANGELOG.md` and under `docs/`, and also holds the
174
187
  section layout: identifiers unique, `+agent.N` sections in one run directly
175
188
  above their release with N descending (newest first), and every ledger
176
- `changelog_section` naming a section that exists. Insert a new `+agent.N`
177
- section at the top of its release's run, not directly above the release
178
- heading.
189
+ `changelog_section` naming a section that exists.
190
+
191
+ A new section always goes at the very top of `CHANGELOG.md`: either a new
192
+ release, or a `+agent.N` section numbered on the current top release (one above
193
+ its highest N, or `+agent.1` if it has none). Never add a `+agent.N` section
194
+ under an older release. Given `--base`, both `check-changelog-structure.mjs` and
195
+ the release-ledger gate refuse any new section that sits below a section base
196
+ already had.
179
197
 
180
198
  ## Running the gate
181
199
 
@@ -272,3 +290,47 @@ a quiet raise.
272
290
 
273
291
  Raising a limit is a reviewed policy change: advance `limits_version`, and the
274
292
  change owes its own ledger entry like anything else.
293
+
294
+ ## Rotating the baseline
295
+
296
+ When the live ledger and changelog approach a bound, rotate them at a reviewed
297
+ cut instead of raising a limit. The first rotation (2026-09-30, `RL-0190`)
298
+ moved `RL-0001` through `RL-0124`; the ledger's `baseline_floor` records it.
299
+
300
+ 1. Pick the cut: the last entry to archive. Everything up to it moves, and so
301
+ does the changelog from the first section those entries link down to the end
302
+ of the file, including sections no entry links. If a kept entry amends an
303
+ archived one, or links a section in that tail, move the cut earlier until the
304
+ pair is on one side. Never later.
305
+ 2. Run the rotation through `rotateLedger` in `scripts/orientation-contract.mjs`
306
+ with a new dated pair under `contracts/archive/`, e.g.
307
+ `release-ledger.<date>.json` and `CHANGELOG.<date>.md`. It writes the entries
308
+ byte-for-byte (original `sequence` and hashes kept) and the changelog tail
309
+ verbatim, appends the pair and its SHA-256 to `baseline_floor.archives`, and
310
+ moves `last_archived_id`, `last_archived_sequence` and `first_kept_id`. It
311
+ refuses a cut that splits a pair.
312
+ 3. Never edit an existing archive file, and never reuse a date: each rotation
313
+ adds a new pair. Add both new files to `named` in
314
+ `contracts/supported-surface.json`.
315
+ 4. Record the rotation as its own entry, named in `baseline_floor.rotation_entry`,
316
+ with a change item for each new archive file and a CHANGELOG section of its
317
+ own. Its `agent_impact` tells consumers which baseline is now too old.
318
+
319
+ The gate accepts a base entry missing from the live ledger only when the head
320
+ floor covers it, the new archive holds it canonical-JSON-identical, and the floor
321
+ moved with a new rotation entry in the same range. Any other deletion, an
322
+ archive copy that differs from base, a floor that moves without a rotation
323
+ entry, a floor that moves back or is rewritten, an edited archive file, and a
324
+ cut that archives an entry while an entry that was live at base stays live and
325
+ amends it (the same refusal `rotateLedger` gives) all fail. A new entry added
326
+ after the rotation may still amend an archived entry, linking its own section
327
+ in the live changelog: that is how archived history is corrected. The live changelog and each archive changelog must each be well-formed on
328
+ their own, a section id may appear in only one of them, a section present
329
+ at base must still be in one of them, and the live changelog followed by the
330
+ archives, newest rotation first, must read as the base's sections in order
331
+ with new sections only at the top of the live file. So every live section is
332
+ newer than every archived one, and an archived section never moves back
333
+ (`check-changelog-structure.mjs --base` and the `--base` release-ledger gate).
334
+ The archive files are not mandatory
335
+ orientation reads and are not measured; a consumer whose reviewed baseline is older than the floor refuses
336
+ with `baseline_below_floor` and adopts a newer reviewed baseline.
@@ -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.43.1`.
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.46.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -8,7 +8,7 @@ npx --no-install campaigns-os sdk storage-check --target . --target-sdk 0.4.38 -
8
8
 
9
9
  Omit `--json` for the concise human report. Exit 0 means source-compatible; exit 2 means incompatible or unknown; invalid arguments or manifests exit 1. No merchant files or pins are rewritten. This is independent of doctor's built HTML markup check.
10
10
 
11
- The manifest is generated and owned by Campaign Cart, from its storage registry. The scanner has no independent SDK key list and fetches no remote code. Supply the generated `storage-migrations.v1.json` from the SDK contract change (campaign-cart #102); until that change is published, this command does **not** imply a released SDK tag contains that file. The report records the full file SHA-256, SDK version, supported target range, registry/extractor/input digest, and Git provenance. When the supplied bytes equal their repository HEAD blob, provenance is `verified-git-blob` with commit and repository path. Proposed, modified, or copied manifests are labeled `unverified-local-file`; the label never certifies a release. Review/pin SDK provenance separately before acting on findings. Targets outside the manifest's supported SDK range report unknown.
11
+ The manifest is generated and owned by Campaign Cart, from its storage registry. The scanner has no independent SDK key list and fetches no remote code. Supply `docs/compatibility/storage-migrations.v1.json` from an unmodified checkout of a Campaign Cart release tag, v0.4.40 or later. The report records the full file SHA-256, SDK version, supported target range, registry/extractor/input digest, and Git provenance. When the supplied bytes equal their repository HEAD blob, provenance is `verified-git-blob` with commit and repository path. Proposed, modified, or copied manifests are labeled `unverified-local-file`; the label never certifies a release. Review/pin SDK provenance separately before acting on findings. Targets outside the manifest's supported SDK range report unknown.
12
12
 
13
13
  `--target` must name the Git root. `--scope` is mandatory: comma-separated literal repository-relative directory/file paths; use `.` only when the complete repository is intended. Add shared JavaScript directories explicitly. `--exclude` uses the same syntax and records explicit exclusions. Archives receive no implicit exemption. Only Git-tracked `.html`, `.htm`, `.js`, `.mjs`, and `.cjs` files in the selected scope are scanned, using current working-tree bytes and per-file digests; untracked files and built dependencies are outside this evidence. A selected HTML page's local script outside the selected files reports unknown. Relative scripts affected by an HTML `<base href>` also report unknown; review their actual dependency paths. Remote SDK and third-party scripts are not fetched or analyzed.
14
14
 
@@ -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.43.1+skills.1"
19
+ "bundle_revision": "1.46.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.43.1+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.46.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.43.1+skills.1
40
+ Bundle revision: 1.46.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.43.1+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.46.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.43.1+skills.1",
93
+ "requested": "1.46.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.43.1+skills.1",
95
+ "on_disk": "1.46.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.43.1+skills.1)"
97
+ "message": "match (1.46.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.43.1+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.43.1+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.43.1+skills.1)
104
+ Skills revision: match (1.46.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.46.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.46.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). |
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 four registered gates: `page_kit.store_profile`, `page_kit.sdk_version`, `polish.hidden_eager_media`, and `built_output.upsell_selector_scope`. `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; 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
  | `bin/campaigns-os.mjs` (`campaigns-os`) | The CLI entry itself. | Declared in `package.json` `bin`; the gate fails if it disappears. |
20
20
  | Package export `./campaign-spec` | The versioned campaign-spec rule registry, consumed as `@nextcommerce/campaigns-os` (pinned by consumers' lockfiles; lockstep policy — ADR-003 in the ops repo). | Behavior-guarded from the consumer side by their contract tests; the export path itself is gated here. |
@@ -24,7 +24,7 @@ implementation detail, however stable it looks.
24
24
  | Contract docs: `CONTEXT.md`, `docs/campaigns-os-build-flow.md`, `docs/build-packet.md`, `docs/migration-sidecar-bundle.md`, `docs/design-source-package.md`, `docs/campaign-build-brief.md`, `docs/campaign-standardization-report.md`, `docs/brand-theme-bridge.md`, `docs/qa-and-test-orders.md`, `docs/legacy-migration.md`, `docs/versioning.md`, `docs/workflow-findings-sidecar.md`, this file | Named entry points consumers pin for context. | Content evolves freely; the path must keep existing. |
25
25
  | `skills.json` + `skills/` + `skills.sh` | Versioned skill packages and their installer. | Governed by `check-skill-versions.mjs` (parity + bump gate + reserved external names). `skills.json` ships in the npm pack as of surface 1.0.0. As of 1.40.0 the manifest also carries `bundle_revision` (`<package version>+skills.<n>`), one identity for the bundle that every `SKILL.md` states on its first body line and that `campaigns-os tooling status --skills-revision <value>` checks against the bundle on disk; it must advance whenever any bundled skill changes (`--base` mode), and `docs/skills-revision.md` is its prose. |
26
26
  | `compatibility.json` | The published compatibility statement. | Named; must keep existing. |
27
- | Orientation contract: `AGENTS.md`, `CHANGELOG.md`, `contracts/release-ledger.json`, `contracts/agent-relevant-change-policy.v1.json`, `contracts/orientation-limits.v1.json`, `contracts/orientation-reason-codes.v1.json`, `docs/orientation-contract-reference.md`, `docs/release-ledger-authoring-guide.md`, `contracts/fixtures/orientation/canonicalization/v1.json` | The declarative data a consumer reads from Git objects to decide whether a commit is safe to work against, without executing anything from this repository. Schema `campaigns-os-tooling-orientation/v1`; ledger schema `campaigns-os-release-ledger/v1`. The canonicalization fixture gives independent implementations exact ledger and changelog bytes plus their expected SHA-256 digests. Entry point: [`AGENTS.md`](../AGENTS.md). | Named. The ledger is append-only: a correction ships as a new amendment entry, never an edit. `scripts/check-release-ledger.mjs` enforces the two-way gate; `docs/orientation-contract-reference.md` and the canonicalization fixture are generated together, and CI fails if either is stale. |
27
+ | Orientation contract: `AGENTS.md`, `CHANGELOG.md`, `contracts/release-ledger.json`, `contracts/archive/release-ledger.2026-09-30.json`, `contracts/archive/CHANGELOG.2026-09-30.md`, `contracts/agent-relevant-change-policy.v1.json`, `contracts/orientation-limits.v1.json`, `contracts/orientation-reason-codes.v1.json`, `docs/orientation-contract-reference.md`, `docs/release-ledger-authoring-guide.md`, `contracts/fixtures/orientation/canonicalization/v1.json` | The declarative data a consumer reads from Git objects to decide whether a commit is safe to work against, without executing anything from this repository. Schema `campaigns-os-tooling-orientation/v1`; ledger schema `campaigns-os-release-ledger/v1`. The canonicalization fixture gives independent implementations exact ledger and changelog bytes plus their expected SHA-256 digests. Entry point: [`AGENTS.md`](../AGENTS.md). | Named. The ledger is append-only: a correction ships as a new amendment entry, never an edit. A baseline rotation moves older entries and changelog sections unchanged into dated files under `contracts/archive/`, which never change once merged and are not mandatory orientation reads. `scripts/check-release-ledger.mjs` enforces the two-way gate; `docs/orientation-contract-reference.md` and the canonicalization fixture are generated together, and CI fails if either is stale. |
28
28
  | Orientation fixtures: `contracts/fixtures/orientation/envelope/*.json`, `contracts/fixtures/orientation/hostile-target/**` (as named) | The bytes a consumer's parser validates against: one envelope per terminal outcome, plus a hostile target carrying Git hooks, an executable file, and npm lifecycle scripts for proving a reader executes nothing. The hostile target carries a second invariant for the runtime recipe: preparing it must run the recipe's own two steps and no lifecycle script reachable from them. | Named. Regenerate the envelopes with `npm run generate:orientation-docs`. Fixtures under `contracts/fixtures/` that are **not** named here — including the legacy-migration conformance corpus — are this repo's own test data and are not supported. |
29
29
  | Runtime recipe: `contracts/runtime-recipe.campaigns-os-node-v1.json` | The declarative description of how a checkout at one commit becomes a usable installed runtime: exact commands, accepted tool ranges, per-step network policy, the enumerated input set, the mandatory output checks, and the enforced bounds. This repository publishes it; the consumer bootstrap executes it. Guide: [`docs/runtime-readiness.md`](runtime-readiness.md). | **Hashed**, deliberately unlike the orientation policy contracts beside it. Any change to commands, network policy, tool versions, inputs, or output verification is an agent-relevant release event, and only a hashed entry makes such a change require `surface_version` to advance in the same PR. A recipe whose commands can change without a version bump is not a contract. |
30
30
  | Runtime-recipe fixtures: `contracts/fixtures/runtime-recipe/**` (as named), `docs/runtime-readiness.md` | Accept and single-mutation reject documents a consumer's parser validates against, the prepared-runtime states its output checks must distinguish, and the generated guide. | Named. All of it is generated — regenerate with `npm run generate:runtime-docs`; CI fails on a stale copy. |
@@ -42,7 +42,10 @@ consumer needs to know about an agent-relevant change even when
42
42
  `CHANGELOG.md` narrates them, and the two are checked against each other in both
43
43
  directions. Reason codes and semantic classes are append-only vocabularies —
44
44
  renaming or removing one is a breaking change. Raising a limit advances
45
- `limits_version` and owes its own ledger entry.
45
+ `limits_version` and owes its own ledger entry. Rotating the baseline (moving
46
+ old entries and their sections into a dated `contracts/archive/` pair under the
47
+ ledger's `baseline_floor`) changes no limit and no schema id; it is recorded as
48
+ its own ledger entry.
46
49
 
47
50
  The runtime recipe carries two version identifiers because they gate different
48
51
  things. The **kind** names what an installed consumer must already understand in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.43.1",
3
+ "version": "1.46.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {