@nextcommerce/campaigns-os 1.53.0 → 1.55.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 (71) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/CONTEXT.md +8 -5
  3. package/README.md +26 -11
  4. package/compatibility.json +1 -1
  5. package/contracts/campaign-cart-checkout-field-contract.v0.json +10 -0
  6. package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
  7. package/contracts/commerce-surface-catalog.json +27 -27
  8. package/contracts/effects.v1.json +96 -37
  9. package/contracts/release-ledger.json +774 -0
  10. package/contracts/supported-surface.json +4 -4
  11. package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
  12. package/docs/brand-theme-bridge.md +10 -0
  13. package/docs/build-packet.md +45 -22
  14. package/docs/campaign-standardization-report.md +32 -0
  15. package/docs/campaigns-os-build-flow.md +1 -1
  16. package/docs/design-source-package.md +7 -3
  17. package/docs/effects.md +4 -6
  18. package/docs/local-setup.md +1 -1
  19. package/docs/orientation-contract-reference.md +1 -1
  20. package/docs/progress-snapshots.md +3 -1
  21. package/docs/qa-and-test-orders.md +83 -14
  22. package/docs/runtime-readiness.md +1 -1
  23. package/docs/sdk-storage-compatibility.md +15 -0
  24. package/docs/skills-revision.md +10 -10
  25. package/docs/supported-surface.md +2 -1
  26. package/docs/workflow-findings-sidecar.md +57 -2
  27. package/package.json +3 -1
  28. package/schemas/campaign-runtime-assembly-report.v0.schema.json +5 -1
  29. package/schemas/campaigns-os-run-record.v0.schema.json +20 -15
  30. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  31. package/skills/campaign-readback-classification/SKILL.md +3 -3
  32. package/skills/campaign-run-evidence/SKILL.md +3 -3
  33. package/skills/contribution-intake/SKILL.md +3 -3
  34. package/skills/next-campaigns-build/SKILL.md +3 -3
  35. package/skills/next-campaigns-os/SKILL.md +3 -3
  36. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  37. package/skills/next-campaigns-polish/SKILL.md +3 -3
  38. package/skills/next-campaigns-qa/SKILL.md +4 -4
  39. package/skills.json +10 -10
  40. package/src/adapter-decision-contract.mjs +26 -12
  41. package/src/brand-theme.mjs +44 -8
  42. package/src/campaign-ecosystem.mjs +153 -31
  43. package/src/campaign-identity.mjs +33 -4
  44. package/src/cli.mjs +58 -62
  45. package/src/consent.mjs +3 -2
  46. package/src/design-source-publication.mjs +1 -1
  47. package/src/deviation.mjs +2 -1
  48. package/src/directory-lock.mjs +79 -7
  49. package/src/doctor/checks.mjs +28 -8
  50. package/src/doctor/inspect.mjs +13 -0
  51. package/src/doctor/next-step.mjs +16 -6
  52. package/src/invocation.mjs +7 -4
  53. package/src/lifecycle.mjs +88 -6
  54. package/src/local-preview-policy.mjs +6 -0
  55. package/src/login.mjs +2 -1
  56. package/src/polish-browser.mjs +8 -2
  57. package/src/polish-node.mjs +6 -1
  58. package/src/polish-readability.mjs +2 -2
  59. package/src/progress-node.mjs +3 -2
  60. package/src/qa-browser.mjs +595 -95
  61. package/src/qa-cart-entry.mjs +29 -6
  62. package/src/qa-node.mjs +177 -43
  63. package/src/qa-verdict-publish.mjs +2 -1
  64. package/src/run-record.mjs +46 -34
  65. package/src/sdk-attribute-index.mjs +111 -23
  66. package/src/sdk-markup.mjs +89 -10
  67. package/src/sdk-repin.mjs +282 -0
  68. package/src/stage-ledger.mjs +15 -3
  69. package/src/stage-record.mjs +85 -17
  70. package/src/standardization-report.mjs +26 -1
  71. package/src/target-lock.mjs +9 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act — hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here — src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) — is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
3
- "surface_version": "1.53.0",
3
+ "surface_version": "1.55.0",
4
4
  "package_exports": [
5
5
  "./commercial-journey",
6
6
  "./commercial-parity",
@@ -60,7 +60,7 @@
60
60
  "sha256": "65bcd8b5f41f1e9ecfc36f3b8838cd4ed531bd74c5a318ee5147131f6c9b4a1c"
61
61
  },
62
62
  "schemas/campaign-runtime-assembly-report.v0.schema.json": {
63
- "sha256": "8a0d37be9a4bb34d0ed073c9d6bfa4406a7b499c801c34c5fbb26646e2665c0b"
63
+ "sha256": "625a6f2ed6bee726f9cddad750e64a54a7e03744c777f5a70afe4a39afdd9848"
64
64
  },
65
65
  "schemas/campaign-runtime-build-context.v0.schema.json": {
66
66
  "sha256": "f310c1b7844d9b1994864d15858ff0c6f17efc1e5285ba193d181bdb2dd4be2f"
@@ -96,7 +96,7 @@
96
96
  "sha256": "d7521fe0ab4deb62485f92286cb890ee548ea290793a6ce7444ecc42ba336d49"
97
97
  },
98
98
  "schemas/campaigns-os-run-record.v0.schema.json": {
99
- "sha256": "00dc9bcec79c12ea22a27f636188bcc33c6811df2d646ac7b49d4cc076e97355"
99
+ "sha256": "390cbc9debacfbfb17d999961baa041a2138fa20a116ec463fbbefbbb05f622d"
100
100
  },
101
101
  "schemas/campaigns-os-runtime-recipe.v1.schema.json": {
102
102
  "sha256": "f8a2ab5eadbb71fb6d5653b670ee71e4471c3df74dd04000d3dbd3d9ae9e71fc"
@@ -120,7 +120,7 @@
120
120
  "sha256": "dfc9abed38d456969e47a21f606d308a03bdf036f3466f7d6e47a602747dacf4"
121
121
  },
122
122
  "contracts/effects.v1.json": {
123
- "sha256": "8282f7ffe91aaf12c4b79e4431b5ddc35cce82e42f1746d4a0ea44f54bd7fb8e"
123
+ "sha256": "10a9020aaba5780e2ee2f3ddf6d81127047f6caec0de774c48540d9b664e0a6a"
124
124
  },
125
125
  "schemas/campaigns-os-effects.v1.schema.json": {
126
126
  "sha256": "3eadd22169ab98bc7c2f682af2751267605170581182158d96be035b0cfe44dc"
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "asset_pin": {
57
57
  "repo": "NextCommerceCo/campaign-cart-starter-templates",
58
- "sha": "2c618942391ae398fb4812f36fc93e6632054874",
58
+ "sha": "783329017c6994a94d11e46580c33bd052aabdb0",
59
59
  "path": "src/<family>/assets/<asset>",
60
60
  "note": "asset_sha256 is the sha256 of each shipped starter asset at this commit (the commerce-surface-catalog _synced_from_sha pin; the bytes are identical across every family). A served asset whose bytes hash to the shipped value is the untouched starter asset and counts as residue even though its markup names no method; different bytes mean it was edited in place. check-template-doctrine verifies these hashes against the pinned checkout."
61
61
  },
@@ -22,6 +22,16 @@ It compares source tokens against known `figma-sections-export` scaffold
22
22
  defaults, maps real brand values onto a local versioned next-core target-token
23
23
  contract, and writes evidence into a theme report.
24
24
 
25
+ A rule whose selector subject has a whole sale, discount, badge, rating, star,
26
+ strike, border or announcement name part does not supply primary or CTA unless
27
+ that subject also has a brand, primary, cta, button or btn part, or is a `button`
28
+ element or a submit input. In comma lists,
29
+ the other selectors still count. Primary keeps the header, navigation, brand
30
+ and hero matching used on main, including element selectors and ancestors;
31
+ button backgrounds keep the same CTA matching, including cart, buy, order and
32
+ submit names and IDs. If no CTA token or button background exists, the CTA uses
33
+ the accent fallback and the warning asks the operator to confirm it.
34
+
25
35
  `campaigns-os theme generate` writes:
26
36
 
27
37
  ```text
@@ -827,12 +827,19 @@ Blockers (not waivable — the markup provably does not do what it says):
827
827
  - `CHECKOUT_NOT_FORM` — `data-next-checkout` on an element that is not `<form>`.
828
828
  - `WRONG_FIELD_NAME` — `data-next-checkout-field` with a value the SDK does not
829
829
  map. The set is vendored from the SDK at a named tag
830
- (`src/sdk-attribute-index.mjs`, currently v0.4.38: `email`, `fname`, `lname`,
830
+ (`src/sdk-attribute-index.mjs`, currently v0.4.41: `email`, `fname`, `lname`,
831
831
  `phone`, `address1`, `address2`, `city`, `province`, `postal`, `country`,
832
832
  `payment-method`, `accepts_marketing`, `cc-number`, `cc-month`, `cc-year`,
833
833
  `exp-month`, `exp-year`, `cvv`, the legacy `card-*` spellings, and any
834
834
  `billing-` prefixed name). The message names the SDK spelling for the usual
835
- offenders (`firstName` → `fname`, `zip` → `postal`).
835
+ offenders (`firstName` → `fname`, `zip` → `postal`). Three names depend on
836
+ the page's SDK version: `first_name` and `last_name` are mapped from v0.4.39
837
+ and `phone_number` from v0.4.41. The version is the page's own exact loader
838
+ pin, else the campaign's `sdk_version` in `_data/campaigns.json`. Below the
839
+ name's version, or when neither gives an exact released version, the name
840
+ blocks, and the message names `fname`, `lname` or `phone`, which every
841
+ version maps. Each finding carries `sdk_version` and `sdk_version_source`
842
+ (`loader` or `campaigns_json`) in its detail.
836
843
  - `MISSING_SELECTOR_ID_MATCH` — an `add-to-cart` button whose
837
844
  `data-next-selector-id` names no selector on the page (an element that is a
838
845
  bundle, package, cart or upsell selector; another element echoing the id
@@ -1246,25 +1253,39 @@ coverage blocks source readiness.
1246
1253
 
1247
1254
  ## Adapter And Proof Fields
1248
1255
 
1249
- Fresh packets now include `source_html.adapter_contract`. Build Context and
1250
- Assembly Report carry the same values as `adapter_decisions`, and build agents
1251
- should update the report as they complete work.
1256
+ Fresh packets include `source_html.adapter_contract`. Build Context and
1257
+ Assembly Report carry the same values as `adapter_decisions`. After building,
1258
+ record the true scalar decisions with `record build --adapter-decision
1259
+ <key>=<value>[,<key>=<value>...]` (one flag, comma-separated `key=value`
1260
+ pairs). For example, set `raw_html_conversion_status=completed` after source
1261
+ HTML conversion, or `not_required` when there is no source HTML to convert.
1262
+ A repeated `--adapter-decision` flag keeps only the last list. The command
1263
+ writes the Assembly Report only. Doctor's adapter gates and leftover-wrapper
1264
+ check read the effective decisions in report → Build Context → packet order;
1265
+ doctor still validates each copy's shape separately. Source preparation reads
1266
+ the packet's `wrapper_policy`, which is selected at intake with
1267
+ `prepare-build --wrapper-policy` or the source-html manifest's `wrapper_policy`
1268
+ option. The record flag refuses that key, an unknown key, or a value outside
1269
+ the allowed values below. The object-valued
1270
+ `template_files_copied` has no flag: its `status`, `required_groups`, `groups`,
1271
+ and `paths` remain required proof in the report and keep their existing doctor
1272
+ checks.
1252
1273
 
1253
1274
  Required adapter decisions:
1254
1275
 
1255
- | Field | Purpose |
1256
- | --- | --- |
1257
- | `raw_html_conversion_status` | Whether prepared HTML has been converted into page-kit-ready source. |
1258
- | `source_asset_strategy` | How images/fonts/CSS/JS are moved and referenced. Prefer `pagekit_campaign_asset_root`. |
1259
- | `commerce_shell_adoption` | Whether checkout/upsell/downsell/receipt use a template-clone-first SDK surface. |
1260
- | `route_rewrite_policy` | How page links, CTAs, and SDK routing values were rewritten from CampaignSpec routes. |
1261
- | `template_files_copied` | Whether the selected template family was copied/verified as one atomic page-kit slice. |
1262
- | `config_script_strategy` | How campaign config scripts are loaded. |
1263
- | `wrapper_policy` | Whether document wrappers are stripped, preserved, or not required. |
1264
- | `frontmatter_policy` | How Page Kit YAML frontmatter is created or preserved. |
1265
- | `script_style_reference_policy` | How scripts/styles move into frontmatter, campaign assets, inline blocks, or passthrough. |
1266
- | `cta_rewrite_policy` | How CTA destinations are rewritten from CampaignSpec routes. |
1267
- | `layout_choice` | Which Page Kit layout strategy wraps the prepared source. |
1276
+ | Field | Purpose | `record build` allowed values |
1277
+ | --- | --- | --- |
1278
+ | `raw_html_conversion_status` | Whether prepared HTML has been converted into page-kit-ready source. | `pending`, `in_progress`, `completed`, `not_required`, `blocked` |
1279
+ | `source_asset_strategy` | How images/fonts/CSS/JS are moved and referenced. | `pagekit_campaign_asset_root`, `external_cdn`, `raw_passthrough`, `not_applicable`, `unknown` |
1280
+ | `commerce_shell_adoption` | Whether checkout/upsell/downsell/receipt use a template-clone-first SDK surface. | `not_required`, `template_clone_first_required`, `template_clone_first_verified`, `sdk_surfaces_preserved`, `custom_html_experimental` |
1281
+ | `route_rewrite_policy` | How page links, CTAs, and SDK routing values were rewritten from CampaignSpec routes. | `campaignspec_routes_via_campaign_link`, `pagekit_public_routes`, `raw_passthrough`, `not_applicable`, `unknown` |
1282
+ | `template_files_copied` | Whether the selected template family was copied/verified as one atomic page-kit slice. | Object-valued proof; no flag |
1283
+ | `config_script_strategy` | How campaign config scripts are loaded. | `campaign_asset`, `frontmatter_script`, `inline`, `not_required`, `unknown` |
1284
+ | `wrapper_policy` | Whether document wrappers are stripped, preserved, or not required. | Intake only: `strip_document_wrappers`, `preserve_document_wrappers`, `not_required`, `unknown` |
1285
+ | `frontmatter_policy` | How Page Kit YAML frontmatter is created or preserved. | `pagekit_yaml_frontmatter`, `raw_passthrough`, `not_required`, `unknown` |
1286
+ | `script_style_reference_policy` | How scripts/styles move into frontmatter, campaign assets, inline blocks, or passthrough. | `frontmatter_or_campaign_asset`, `frontmatter`, `campaign_asset`, `inline`, `raw_passthrough`, `not_required`, `unknown` |
1287
+ | `cta_rewrite_policy` | How CTA destinations are rewritten from CampaignSpec routes. | `campaignspec_routes_via_campaign_link`, `pagekit_public_routes`, `raw_passthrough`, `not_applicable`, `unknown` |
1288
+ | `layout_choice` | Which Page Kit layout strategy wraps the prepared source. | `campaign_layout`, `page_layout`, `raw_passthrough`, `not_applicable`, `unknown` |
1268
1289
 
1269
1290
  Fresh build context also includes `source.asset_crawl`
1270
1291
  (`source-asset-crawl/v0`). `prepare-build` scans the source HTML files and
@@ -1730,10 +1751,10 @@ hand-editing `.campaign-runtime/` JSON:
1730
1751
  | Command | Writes | Refused (nothing written) when |
1731
1752
  |---|---|---|
1732
1753
  | `campaigns-os record setup --packet <p>` | Build Context `scaffold.required=false` (`handoff_skill` next-campaigns-build) and `stages.setup` completed | the campaign output directory (`assembly.output_dir`) does not exist, or there is no Build Context or Assembly Report |
1733
- | `campaigns-os record build --packet <p> [--build-environment <development\|production>]` | `stages.assembly` completed with `build_fingerprint` = doctor's `derived.build_output_fingerprint.value`, `source_package_material_fingerprint` = the report's Design Source Package material fingerprint when present, `evidence.build_environment` = the `--build-environment` value when given (kept from the last record otherwise), and `stages.polish` reset to `required` (`required_by` build, `required_for` qa) unless its evidence is bound to this exact output | doctor cannot compute the fingerprint (no `_site/<public_route_slug>/`), setup is still required, or `stages.setup` is not terminal |
1754
+ | `campaigns-os record build --packet <p> [--build-environment <development\|production>] [--adapter-decision <key>=<value>[,<key>=<value>...]]` | `stages.assembly` completed with `build_fingerprint` = doctor's `derived.build_output_fingerprint.value`, `source_package_material_fingerprint` = the report's Design Source Package material fingerprint when present, `evidence.build_environment` = the `--build-environment` value when given (kept from the last record otherwise), and the specified scalar `report.adapter_decisions`; all recordable decisions go in one comma-separated flag (a repeated flag keeps only the last list); `wrapper_policy` stays an intake choice. `stages.polish` resets to `required` (`required_by` build, `required_for` qa) unless its evidence is bound to this exact output; a completed `stages.deploy` whose `source_build_fingerprint` names other output resets to `required` the same way, without the old probe's `outputs` and `evidence`, and its prior record is kept in `stages.deploy.history` (a deploy with no `source_build_fingerprint`, recorded before deploy stamped it, is kept) | doctor cannot compute the fingerprint (no `_site/<public_route_slug>/`), setup is still required, `stages.setup` is not terminal, or an adapter decision key/value is invalid |
1734
1755
  | `campaigns-os record polish --packet <p> --evidence <file>` | `stages.polish` from the file (`docs/polish-evidence.md` §7: completed, blocked or skipped), bound to doctor's current fingerprint; `report.theme.repair_loop_defect` when the file sets it | build is not recorded for the current output, the file has a shape error (named by field), or, for a completed status, the polish gate doctor evaluates would not pass on the result |
1735
1756
  | `campaigns-os record theme --packet <p>` | `report.theme`: status `applied`, `load_order` `after-next-core`, `css_path`, `commerce_pages` and per-page evidence read from each built commerce page's stylesheet links; any earlier theme waiver is cleared | build is not recorded for the current output, the campaign ships no commerce pages, a built commerce page that loads `next-core.css` does not load `brand-theme.css` (or `checkout-brand.css`) after it or links one missing from the built output, or no built commerce page loads `next-core.css` |
1736
- | `campaigns-os record deploy --packet <p> --base-url <url>` | the packet's `deploy.preview_url` and `stages.deploy` completed with the URL in `outputs` and one evidence line per built page that answered (each page is requested under the URL first) | the packet is not `local-serve`, the URL is not a loopback origin naming the campaign's route root, a built page does not answer 2xx, polish is not recorded, the built output changed since build was recorded, or the theme gate is blocked |
1757
+ | `campaigns-os record deploy --packet <p> --base-url <url>` | the packet's `deploy.preview_url` and `stages.deploy` completed with the URL in `outputs`, one evidence line per built page that answered (each page is requested under the URL first), and `source_build_fingerprint` = the recorded `stages.assembly.build_fingerprint` it probed, so a later `record build` of different output makes deploy owed again and `next` routes back to `record deploy` before QA; a byte-identical rebuild keeps it current | the packet is not `local-serve`, the URL is not a loopback origin naming the campaign's route root, a built page does not answer 2xx, polish is not recorded, the built output changed since build was recorded, or the theme gate is blocked |
1737
1758
 
1738
1759
  Each command also refuses a stage `next` has not reached: while doctor's
1739
1760
  prepare-build gate is set (`next` answers prepare-build) or while an earlier
@@ -1766,7 +1787,9 @@ check exits non-zero with the problems listed, one per line; a value outside a
1766
1787
  schema enum (for example an `adapter_decisions` policy) is listed with the
1767
1788
  values the schema allows and the value it got. Re-run `record
1768
1789
  build` after every page-kit build; a rebuild that changes the output needs
1769
- `polish capture` and `record polish` again.
1790
+ `polish capture` and `record polish` again. After deploy is recorded for the
1791
+ rebuilt output, `next` asks for QA again if the last QA record names the
1792
+ previous build fingerprint, even while its stage status still says completed.
1770
1793
 
1771
1794
  Stage order: `setup → build → polish → deploy → qa`. The picker walks this list and returns the first stage whose recorded status isn't terminal (`completed`, `completed_with_warnings`, `skipped`). During Polish, install the package-owned browser first, then run `campaigns-os polish capture` against the served current build before recording a terminal `stages.polish.status` or proceeding to deploy/QA; the producer attaches package-owned `visual_review.page_load` evidence and never marks the stage complete itself.
1772
1795
 
@@ -1801,7 +1824,7 @@ Result shape (with `--json`):
1801
1824
  Terminal states:
1802
1825
 
1803
1826
  - **`stage: "doctor-blocked"`** — doctor returned errors. Resolve the blockers and re-run `campaigns-os doctor` to confirm before calling `next` again.
1804
- - **`stage: "done"`** — every stage is in a terminal status. Pipeline is complete. To re-run a specific stage, set its status back to `"pending"` in the assembly report and call `next` again.
1827
+ - **`stage: "done"`** — every stage is in a terminal status. Pipeline is complete. To repeat build work, do the work and use `record build`; it makes downstream stages owed as needed. Use `record setup` or `record polish` after repeating those stages, and `qa run` for QA. `record deploy` records a local-serve target; for a hosted deploy, record the URL and stage outcome as the deploy prompt describes. Then call `next` again.
1805
1828
  - **`stage_blocked: true`** — the picker returned a stage whose recorded status is `blocked`. Don't run the prompt as-is; clear the blocker first.
1806
1829
 
1807
1830
  The legacy form `campaigns-os next <stage>` (e.g. `next build`) still works and is the way to force a specific stage when you want to override the picker.
@@ -170,12 +170,34 @@ version discovery), `checkout_fields` (every
170
170
  script evidence, and `proof_state`), `runtime_contract`, `findings`, and
171
171
  `remediation`.
172
172
 
173
+ Three names in the field contract depend on the SDK version
174
+ (`version_gated_aliases`): `first_name` and `last_name` from 0.4.39 and
175
+ `phone_number` from 0.4.41. Each is `supported` only when the binding's SDK
176
+ version is an exact release at or after that version; otherwise it is
177
+ classified as on an earlier SDK (`first_name` and `last_name` as stale aliases,
178
+ `phone_number` as unknown). Each binding records the `sdk_version` it was
179
+ judged against and its `sdk_version_source`: `loader` (the file's own exact
180
+ loader pin), `campaign` (for a file with no loader of its own, the lowest of
181
+ the root's exact loader pins and its bundled dependency's floor: an exact pin,
182
+ or `^`, `~` or `>=` on a released version; any other range leaves the version
183
+ unknown), or
184
+ `campaigns_json` (a Page Kit campaign's `sdk_version`; a shared file takes the
185
+ lowest across campaigns). An unpinned loader or a missing pin leaves
186
+ `sdk_version` null, judged as an earlier SDK, because `fname`, `lname` and
187
+ `phone` work on every version.
188
+
173
189
  `sdk_loader.references` records pinned and unpinned loader refs alike
174
190
  (`version` is null for `@latest`/branch/commit pins, which raise
175
191
  `version.sdk_loader_unpinned` instead of a policy evaluation). Only URLs that
176
192
  point at a loader/dist artifact count; incidental `campaign-cart@x.y.z`
177
193
  strings elsewhere in source are ignored.
178
194
 
195
+ In a static repo (no `_data/campaigns.json`), `campaigns-os sdk repin --target
196
+ <repo> [--target-sdk <x.y.z>] [--apply]` rewrites the semver-pinned references
197
+ in this list that are below the target version, changing only the version
198
+ segment of each URL. It previews by default and leaves the unpinned refs
199
+ alone. See `sdk-storage-compatibility.md`.
200
+
179
201
  `payment.proof_state` is one of `runtime_proof_required` (custom-control
180
202
  evidence found), `undetermined` (radios exist; static scanning cannot exclude
181
203
  externally-styled custom controls), or `not_applicable` (no `payment_method`
@@ -216,6 +238,16 @@ Page Kit root that inlines them gets the same `checkout_fields` block and the
216
238
  same `checkout.unsupported_field_binding` / `checkout.unknown_field_binding`
217
239
  findings as an application root.
218
240
 
241
+ A bundled dependency is judged by its floor, the lowest version its
242
+ `package.json` spec lets npm install, recorded as
243
+ `sdk_loader.bundled_dependency.floor_version`. Only an exact pin (`0.4.30`,
244
+ `=0.4.30`, `v0.4.30`) or one `^`, `~` or `>=` range on a released version has
245
+ a certain floor; `^0.4.25` is evaluated as `0.4.25`. Any other spec (`<0.4.39`,
246
+ an `||`, hyphen or x-range, a tag) has none, so it gets no evaluation and
247
+ raises the warning `version.sdk_dependency_floor_unknown` instead; a
248
+ prerelease pin raises `version.sdk_prerelease_pin`. `resolved_version` stays
249
+ the version text extracted from the spec, for reference.
250
+
219
251
  Both are also injectable from the CLI: pass
220
252
  `--sdk-support-policy <path-to-json>` and/or `--field-contract <path-to-json>`
221
253
  to `standardize`. Each file is read and JSON-parsed
@@ -12,7 +12,7 @@ The happy path is intentionally tight:
12
12
  8. Run polish, serve the current built output, and run `campaigns-os polish capture --packet <packet> --base-url <served-current-build-url>`. Do not mark Polish terminal or begin deploy/QA until this package-owned page-load evidence passes or has an exact finding waiver.
13
13
  9. Deploy a preview.
14
14
  10. Run `campaigns-os qa resolve --packet <packet>`, then `campaigns-os qa run --packet <packet> --browser --test-order common` with the tested URL. Saved-Map QA uses the existing consent and publication controls; pass `--no-post-verdict` to keep its verdict local. Local-spec QA always keeps verdicts and progress local, even with `--post-verdict`, and `qa publish` refuses local-spec packets. Run Telemetry retains its consent controls.
15
- 11. Treat test-order depth as the control: global test cards bypass the gateway and create no transactions, so no approval is needed. Localhost on any port is a Campaigns App Development domain (SDK allowed, analytics suppressed); non-localhost preview/production origins must still be allowlisted for the campaign API key so the SDK loads.
15
+ 11. Global test cards bypass the gateway and create no transactions, but each test order leaves a real store order record. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing test orders; then proceed. There is no permission flag; test-order depth controls coverage. Localhost on any port is a Campaigns App Development domain (SDK allowed, analytics suppressed); non-localhost preview/production origins must still be allowlisted for the campaign API key so the SDK loads.
16
16
  12. Promote, block, or iterate from the recorded build, polish, deploy, QA, and test-order evidence.
17
17
 
18
18
  Pause only for missing inputs, doctor blockers, blocked deploys, out-of-scope runtime pages that block checkout proof, or merchant-specific uncertainty. The default path should not branch into external browser skills or hand-built backend order creation.
@@ -396,9 +396,13 @@ a minute. The lock directory and its owner record appear together, so a lock
396
396
  left by a process that died is recovered automatically, and a lock directory
397
397
  without an owner record (only an older release leaves one) is never taken
398
398
  over: the command refuses it after about a second and names it. Confirm no
399
- campaigns-os process is working on the target, then remove it. Do not run an
400
- older Campaigns OS release against the same target at the same time. A
401
- waiver's `--dry-run` preview takes no lock.
399
+ campaigns-os process is working on the target, then remove it. Anything else
400
+ at the lock path (a file, a symbolic link, or a directory with no owner record
401
+ that holds other entries) is not a lock: once the wait ends the command names
402
+ what is there and never removes it. The holder of the lock removes staging and
403
+ released directories beside it whose recorded owner process no longer exists.
404
+ Do not run an older Campaigns OS release against the same target at the same
405
+ time. A waiver's `--dry-run` preview takes no lock.
402
406
 
403
407
  Before writing any output, `prepare-build` also requires distinct paths for the
404
408
  Build Packet, Build Context, Assembly Report, Doctor output, normalized Build
package/docs/effects.md CHANGED
@@ -42,6 +42,7 @@ CLI exempts from lifecycle capture (the converse does not hold: `demo` and the
42
42
  `doctor` inspection and `doctor --no-write` skip it and write nothing but may
43
43
  send the one live campaign read): `help`, `readback`,
44
44
  `run status`, `doctor --no-live-refs` inspection, `sdk storage-check`,
45
+ the `sdk repin` preview (without `--apply`),
45
46
  `tooling diagnose`, a refused invocation, `run-record --no-write`, and every
46
47
  `--dry-run` form on the commands that implement the flag. Everything else
47
48
  appends an entry when a journal is selected — an active run session,
@@ -160,7 +161,7 @@ Both rows are tier C, because a change makes recorded stages owed again, and eac
160
161
 
161
162
  There is **one row per command and per effect-changing flag combination**. The
162
163
  flags that change what the invocation does to the world are listed once, in
163
- `vocabulary.effect_changing_flags`: `--browser`, `--built`, `--dry-run`,
164
+ `vocabulary.effect_changing_flags`: `--apply`, `--browser`, `--built`, `--dry-run`,
164
165
  `--emit-packet`, `--example`, `--force`, `--from-store`, `--list`,
165
166
  `--no-live-refs`, `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
166
167
  `--no-write`, `--republish`, `--test-order`, `--write`, `--write-map`. Flags
@@ -208,9 +209,7 @@ template family is certified, and whether a named brief can be read, depend on
208
209
  file content: those checks still run in the handler and are journaled.
209
210
  The operator-facing `run-record` and `run end` commands refuse bare, empty, or
210
211
  whitespace-only values for every value-taking inherited run-record flag before
211
- packet work. The five agent
212
- token and elapsed-time flags retain their non-negative-integer diagnostics;
213
- `--surfaces` rejects unknown values, and `--dry-run` rejects a value. The
212
+ packet work. `--surfaces` rejects unknown values, and `--dry-run` rejects a value. The
214
213
  inherited boolean flags (`--no-remit`, `--no-write`, `--dry-run`, and `--json`)
215
214
  retain their bare-flag behavior. `run end` also rejects `--new-run` and
216
215
  `--run-id` because the saved session fixes its run ID. `run-record` also
@@ -224,8 +223,7 @@ literal relative path, so the default context file is not read. Bare or empty
224
223
  `--context` or `--report` still makes QA auto-end fail and leaves the session
225
224
  open; bare or empty `--qa-verdict` fails a Run Record closeout when inherited,
226
225
  though QA auto-end supplies its own verdict path. The underlying run-record
227
- handler still rejects invalid agent
228
- integers, unknown `--surfaces`, and any valued `--dry-run` that reaches it. QA
226
+ handler still rejects unknown `--surfaces` and any valued `--dry-run` that reaches it. QA
229
227
  auto-end drops `--dry-run` from inherited flags; if another inherited value
230
228
  fails in the handler, auto-end is skipped and the session stays open. QA's own
231
229
  journal entry is unaffected because auto-end runs after QA persistence. A named
@@ -3,7 +3,7 @@
3
3
  For a new campaign, create an empty working folder and run this from it:
4
4
 
5
5
  ```sh
6
- npm init -y && npm install --save-exact next-campaign-page-kit@0.2.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.53.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
6
+ npm init -y && npm install --save-exact next-campaign-page-kit@0.2.0 && npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.55.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
7
7
  ```
8
8
 
9
9
  `npm init -y` gives the folder its own `package.json`. Without one, npm
@@ -26,7 +26,7 @@ Ledger schema id: `campaigns-os-release-ledger/v1`
26
26
  Change policy version: `1.0.0`
27
27
  Reason-code vocabulary version: `1.0.0`
28
28
  Limits version: `1.0.0`
29
- Supported surface at generation time: `1.53.0`
29
+ Supported surface at generation time: `1.55.0`
30
30
 
31
31
  ## Forward compatibility
32
32
 
@@ -65,7 +65,9 @@ rename; a live process is never evicted. A lock directory with no owner record
65
65
  a second. If one is found, or if recovery itself is interrupted, capture fails
66
66
  closed and the warning names the affected `.allocation-lock` directory: stop
67
67
  all Campaigns OS writers for that target, then remove that directory before
68
- retrying `next`. Do not remove a lock while a writer is active. Do not run an
68
+ retrying `next`. A file or other non-lock entry at the lock path is named in
69
+ the warning and never removed: move it out of the way. Do not remove a lock
70
+ while a writer is active. Do not run an
69
71
  older Campaigns OS release against the same target at the same time.
70
72
 
71
73
  An unchanged projection reuses its ID, timestamp and sequence. Identity
@@ -59,8 +59,10 @@ the toolkit runs that loop in a fixed order:
59
59
  2. **Serve and prove.** Serve `_site/` on localhost (the `next deploy` handoff
60
60
  names the directory and any root-route rewrite) and run `polish capture`
61
61
  against it. Once polish is recorded, `record deploy --packet <p> --base-url
62
- <url>` records the URL on `deploy.preview_url` and the deploy stage; then run
63
- `qa run --browser` and the typed-card order paths against it.
62
+ <url>` records the URL on `deploy.preview_url` and the deploy stage, bound
63
+ to the build it probed (a `record build` of different output makes it owed
64
+ again and keeps the old record in the deploy stage's history); then run `qa run --browser` and the typed-card order paths against
65
+ it.
64
66
  3. **Prove the pin on the production output.** Before committing, run
65
67
 
66
68
  ```bash
@@ -339,6 +341,24 @@ npm run campaigns-os -- qa run \
339
341
 
340
342
  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.
341
343
 
344
+ `--currency <code>` proves one currency per run: the runner adds
345
+ `?currency=<CODE>` to each funnel's entry URL (the HTTP fetch, the browser load
346
+ and `entry_urls`) and to the first page load of each test-order path (the
347
+ checkout probe, or the entry page or checkout when that load comes first),
348
+ keeping any query or tracking params already there. Campaign Cart keeps the URL
349
+ currency for the rest of that tab's session, so the runner adds it to no other
350
+ load (upsell pages and the receipt reload in recovery go without it); a later
351
+ load of the entry URL still carries it, because that URL is tagged. The flag takes one code per run (the
352
+ shared parser keeps the last one if it is given twice). The code must be three
353
+ letters (upper-cased: `gbp` runs as `GBP`); anything else is refused before any
354
+ request. A campaign set up for three currencies takes three runs. `qa parity`
355
+ refuses `--currency`, and so does a run whose orders come from
356
+ `--legacy-api-test-order`, since a direct API order would be placed in the
357
+ default currency. The persisted verdict redacts every URL query, so the
358
+ currency is recorded as `evidence.currency` on the entry page's
359
+ `http:<page_id>` assertion; an entry URL that does not parse gets no currency,
360
+ and its row records `evidence.currency_not_applied` with the reason instead.
361
+
342
362
  ### Automatic commercial parity
343
363
 
344
364
  When the CampaignSpec has enabled pages with package rows, the same `qa run`
@@ -726,6 +746,16 @@ spec resolved from the portal for the run — keep publish-by-default regardless
726
746
  of consent: those verdicts are the QA tab's product surface, not telemetry.
727
747
  Explicit flags always win in both directions for saved-Map QA.
728
748
 
749
+ A run whose base URL is a local address (`localhost` or any `*.localhost`
750
+ name, any `127.x.x.x` address, `0.0.0.0`, `[::1]` or an IPv4-mapped loopback,
751
+ from `--base-url` or the packet's `deploy.preview_url`) keeps its verdict local
752
+ by default, whatever the spec source. Those runs are checks on your own
753
+ machine, and people watching the Map's QA tab should not see them. The output
754
+ names the destination and how to publish: `qa publish` for the stored verdict,
755
+ or `--post-verdict` on the next run. `--post-verdict` publishes a local-address
756
+ run, and `publish_decision.reason` reads `loopback_base_url` when the default
757
+ kept it local.
758
+
729
759
  A packet with `local_spec_id` always keeps its verdict and progress local.
730
760
  `qa run` suppresses portal publication even with `--post-verdict`; the flag
731
761
  does not turn a local ID into a Map destination. `qa publish` refuses such a
@@ -792,7 +822,7 @@ prints the portal link.
792
822
  The outcome lands on the run's Run Record as the `qa_verdict_publish` block —
793
823
  `verdict_run_id`, `publisher` (`qa run` or `qa publish`), `state`
794
824
  (`skipped` / `ok` / `failed`), `result` in the remit vocabulary, `base_kind`,
795
- `published_at` — on the record whose `qa_verdict` artifact references the
825
+ `published_at`, and the nullable publish decision `reason` — on the record whose `qa_verdict` artifact references the
796
826
  verdict under the packet's campaign. `qa run` records its own publish (or
797
827
  its `--no-post-verdict` skip) the same way when the run session closes, which
798
828
  is what `already_published` reads. A stored `ok` is never downgraded: a
@@ -1171,8 +1201,8 @@ Fixture essentials:
1171
1201
 
1172
1202
  Test Orders use **global test cards** that work on any live store and integration.
1173
1203
  They **bypass the payment gateway and create no transactions** (and no fulfillment),
1174
- so they are safe to run any time and need **no permission flags, packet policy,
1175
- merchant sandbox routing, or test-order approval** — you just pick a mode. They leave a small,
1204
+ 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,
1205
+ packet policy, or merchant sandbox routing is required. They leave a small,
1176
1206
  easy-to-clean footprint: Test orders are deletable in bulk, and the resulting
1177
1207
  Customer record is reused (see the test email note below) rather than multiplied.
1178
1208
 
@@ -1554,17 +1584,43 @@ Two flags target funnels the default-tier drive cannot prove:
1554
1584
 
1555
1585
  - `--select-package <ref[:qty],...>` — **strict** package/bundle card selection.
1556
1586
  Each ref is matched against rendered selector/bundle cards
1557
- (`[data-next-package-id]`, `[data-next-bundle-card][data-next-bundle-id]`) and
1558
- clicked. An explicit quantity requires one unambiguous rendered card whose
1587
+ (`[data-next-package-id]`, `[data-next-bundle-card][data-next-bundle-id]`, and
1588
+ the package a bundle card's `data-next-bundle-items` declares, so
1589
+ `--select-package 1:2` finds the card declaring
1590
+ `[{"packageId":1,"quantity":2}]` whether or not an inner node repeats the
1591
+ package id) and clicked. An explicit quantity requires one unambiguous rendered card whose
1559
1592
  `data-next-bundle-items` composition contains exactly that package and
1560
- quantity; package identity alone is not enough. The card must then expose a
1593
+ quantity; package identity alone is not enough. A card declaring several
1594
+ items matches by any one of them (`4:1` finds a kit declaring
1595
+ `[{"packageId":1,"quantity":2},{"packageId":4,"quantity":1}]`); a card
1596
+ declaring the item alone is preferred over one declaring it among others. The card must then expose a
1561
1597
  selected-state marker (`data-next-selected="true"` / `.next-selected`) after
1562
1598
  the click. A missing card, ambiguous composition, wrong quantity, or
1563
1599
  unverifiable/refused selection **fails the `selected_bundle` step** instead
1564
1600
  of silently driving the pre-selected default tier. A bundle ref remains an
1565
1601
  authoritative identity when exactly one rendered card declares it. Use this
1566
1602
  flag to traverse non-default tiers of a multi-tier selector. `--cart` remains
1567
- the best-effort variant.
1603
+ the best-effort variant. For a package that bundle cards declare, `--cart
1604
+ <ref:qty>` clicks the card whose items carry that package at that quantity,
1605
+ and clicks nothing when the selected card already does; refs no bundle card
1606
+ declares (an order bump) are clicked as before. The cards are read again
1607
+ before each ref, so a selector that re-renders after a click is clicked as
1608
+ it now stands. Beside `--select-package`,
1609
+ `--cart` never clicks another card in the chosen card's selector group, and
1610
+ a card in no recognised selector container
1611
+ (`[data-next-bundle-selector]`, `[data-next-selector-id]`,
1612
+ `[data-next-cart-selector]`) counts as in that group: a
1613
+ ref the chosen card carries is left as it is, a ref an order bump or a card
1614
+ in another selector group carries is applied through that control, and any
1615
+ other ref is left unapplied and named in the `selected_bundle` detail
1616
+ (`--cart 3:1 not applied: ...`). The display-parity row reconciles the order
1617
+ against the packages and quantities every selected bundle card declares, not
1618
+ its `data-next-bundle-id`. Without `--select-package` it also fails when an
1619
+ explicit `--cart <ref:qty>` is not in the order at that quantity: a
1620
+ different quantity (`--cart 1:2` that ordered one unit) or no line for the
1621
+ package at all (`--cart 1:2,3:1` on a swap-mode selector that kept only the
1622
+ `3:1` card); beside `--select-package` it judges the order against the card
1623
+ that flag chose.
1568
1624
  - `--apply-coupon <code>` — types the code into the rendered coupon/promo input
1569
1625
  (the SDK's `[data-next-checkout-field="coupon"]` or
1570
1626
  `input[data-next-coupon="input"]`, then hand-rolled `coupon`/`voucher`/`promo`
@@ -1697,7 +1753,7 @@ browser launch, prints the planned count, lists the planned paths (up to 40 ids;
1697
1753
  past that the remainder is counted, never cut silently, and `--select-package
1698
1754
  <ref[:qty]>` lists one tier's paths), and names the exact `--max-test-orders <count>` raise. For example, a linear three-offer graph has
1699
1755
  eight terminal paths plus the checkout baseline, so it requires
1700
- `--max-test-orders 9`. No approval step is involved.
1756
+ `--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.
1701
1757
 
1702
1758
  `--max-test-orders` bounds **planned paths**, which is not the same as real
1703
1759
  purchases. `--max-order-creations` bounds **actual order creations**, defaults to
@@ -1810,7 +1866,12 @@ The browser driver intentionally behaves like a user:
1810
1866
  proven against the persisted-order voucher read-back
1811
1867
  - checkout is advanced through the visible cart/checkout button
1812
1868
  - address autocomplete is settled or closed before submit
1813
- - Spreedly card and CVV iframes are filled with sequential keystrokes
1869
+ - the card and CVV iframes are filled with sequential keystrokes: Spreedly's
1870
+ iFrame fields on SDK 0.4.40 and earlier, and on 0.4.41 and later the
1871
+ Spreedly-hosted fields that NEXT's `payments.29next.com` script mounts.
1872
+ Typing starts once the checkout form drops `next-loading-spreedly` (the
1873
+ fields are ready), and both fields are read back and retyped, up to three
1874
+ attempts in all, before `card_fields_filled` passes
1814
1875
  - the real submit button is clicked without fabricating SDK state
1815
1876
 
1816
1877
  The intended QA order matrix is:
@@ -1832,7 +1893,7 @@ and hardcoded phone numbers that differ from CampaignSpec `campaign.store_phone`
1832
1893
  If a static claim is intentionally preserved, wrap it in an element with
1833
1894
  `data-skip-market-lint="true"` and record why in the assembly report.
1834
1895
 
1835
- Test orders themselves need no allowlist or approval. A separate concern is the
1896
+ 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
1836
1897
  **SDK origin allowlist**: the Campaign Cart SDK must be allowed to load on the
1837
1898
  tested origin for the campaign API key, or runtime checks (and the live page
1838
1899
  itself) may not initialize. Localhost on any port is globally available as a
@@ -1847,8 +1908,8 @@ npm run campaigns-os -- qa policy set \
1847
1908
  --allowed-domains-confirmed true
1848
1909
  ```
1849
1910
 
1850
- There is no permission flag for test orders — they run from `--test-order
1851
- <mode>` alone. The former `--test-orders-allowed` /
1911
+ 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
1912
+ <mode>`. The former `--test-orders-allowed` /
1852
1913
  `--sandbox-test-card-confirmed` flags and the `qa.test_orders_allowed` /
1853
1914
  `qa.sandbox_test_card_confirmed` packet fields they set were removed in
1854
1915
  supported surface 1.28.0: nothing had read their values since the gate itself
@@ -1962,6 +2023,14 @@ placeholder-text blocker and the other residue gates still run. The emitted
1962
2023
  minimal packet is marked `_synthesized` — it points doctor/QA at the built
1963
2024
  output and family, and is not a substitute for a real Build Packet.
1964
2025
 
2026
+ `doctor --built` reads the local built files only. `--base-url` is not
2027
+ fetched: it fills `deploy.preview_url` in the minimal packet, and the output
2028
+ says so (`derived.base_url.fetched: false` in JSON). A clean `doctor --built`
2029
+ run with a preview URL says nothing about the preview. To check the served
2030
+ pages, run `qa run --site <repo> --base-url <url> --family <family> --browser`.
2031
+ The emitted minimal packet does not drive packet QA: `qa resolve` and
2032
+ `qa run --packet` block on it, because it carries no local CampaignSpec.
2033
+
1965
2034
  **Trade-off — non-packet QA is narrower than packet-driven QA.** It runs the
1966
2035
  built-output gates (residue, placeholder text, demo-asset, pricing-CSS, brand
1967
2036
  contract) but **skips the CampaignSpec/source-HTML-driven checks** a packet
@@ -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.53.0`.
11
+ Recipe kind `campaigns-os-node-v1`, revision `1.0.2`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.55.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -17,3 +17,18 @@ Acorn parses JavaScript ASTs; parse5 identifies executable inline HTML scripts a
17
17
  Dynamic storage keys, unresolved storage methods, shadowed storage names, missing migration release evidence, unsupported target versions, and JavaScript parse failures report unknown. This bounded analysis does not execute merchant code or prove computed keys correct. It does not follow arbitrary data flow, dynamic imports, event-handler attributes, generated code or runtime-loaded script graphs. Inspect unresolved findings and include all campaign sources in scope; a clean result is only static tracked source compatibility evidence. Browser behavior, order creation, tracking and analytics still require their own proof, especially where multiple campaigns share an origin. Use the SDK public store API where the manifest recommends it; do not invent a prefix-first merchant repair.
18
18
 
19
19
  Storage `length` property reads are unaffected metadata (`storage-metadata-read`). Storage `key()` enumeration, extracted method references, and mutations of reserved members remain unknown; enumeration cannot prove an SDK key lookup safe. Git discovery failures return one bounded CLI error. Local script diagnostics retain repository-relative spellings from HTML source, including `../` paths; excluded/outside sources are not read. The Git root and operator target are compared by filesystem device/inode identity, so supported case aliases and symlinks can name the same root. Source containment uses realpaths below the Git root without case folding. Extra positional CLI arguments are rejected so an accidental scope or version value cannot silently disappear.
20
+
21
+ ## Rewriting hardcoded pins in a static repo
22
+
23
+ A static campaign repo (no `_data/campaigns.json`) pins the SDK in its HTML: `loader.js@v0.4.x` in script tags and `campaign-cart.css@v0.4.x` in link tags. Once the storage check is clean, `sdk repin` updates those pins:
24
+
25
+ ```sh
26
+ npx --no-install campaigns-os sdk repin --target . --target-sdk 0.4.38 # preview: path:line, old URL -> new URL; writes nothing
27
+ npx --no-install campaigns-os sdk repin --target . --target-sdk 0.4.38 --apply # writes the files and the change record
28
+ ```
29
+
30
+ It reads the same references `standardize` reports under `sdk_loader.references`: every Campaign Cart URL that points at a loader or dist artifact, in HTML and script files, with HTML comments, `_site/`, `node_modules/`, `dist/`, `build/` and `.campaign-runtime/` left out. Only a semver pin (`@vX.Y.Z` or `@X.Y.Z`) below the target is rewritten, and only the version segment of the URL changes; a leading `v` stays as written. These are reported and left alone: `@latest`, `@main`, commit refs, prerelease refs, and pins already at or above the target (repin never moves a pin down). Without `--target-sdk` the target is the SDK support policy's `preferred_minimum`, the version `standardize` evaluates pins against.
31
+
32
+ The default is a preview that writes nothing. `--apply` writes the files, then writes a change record to `.campaign-runtime/sdk-repin.json` (`campaigns-os-sdk-repin/v0`). The record holds the files touched, the reference count, the from and to versions, each rewrite with its path, line and artifact (`loader` or `css`), and the refs left alone. It uses repo-relative paths only, so you can commit it, and a Run Record can cite it by path and sha256 (`--json` prints the sha256). After writing, repin scans again and reports whether any pin below the target remains. Running it a second time with the same target finds nothing to change and writes nothing. `--json` prints the full result: `status` is `changes`, `nothing_to_change`, `no_references` or `refused_page_kit`.
33
+
34
+ A repo with `_data/campaigns.json`, at its root or in any folder the scan walks, is refused with exit 2 and nothing written: page-kit owns that pin, so use `campaigns-os page-kit sync --packet <campaign-runtime.build.json>` there. An invalid `--target-sdk` is refused with exit 1. Repin does not prove that the new SDK works. After rewriting, run the campaign's browser QA against the new pin.
@@ -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.53.0+skills.2"
19
+ "bundle_revision": "1.55.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.53.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.55.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.53.0+skills.2
40
+ Bundle revision: 1.55.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.53.0+skills.2
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.55.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.53.0+skills.2",
93
+ "requested": "1.55.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.53.0+skills.2",
95
+ "on_disk": "1.55.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.53.0+skills.2)"
97
+ "message": "match (1.55.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.53.0+skills.2)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.53.0+skills.2 — start a fresh session
106
- Skills revision: unchecked (on disk 1.53.0+skills.2)
104
+ Skills revision: match (1.55.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.55.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.55.0+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an