@nextcommerce/campaigns-os 1.52.0 → 1.54.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 (76) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +1 -0
  3. package/agents/claude/CLAUDE.md +2 -0
  4. package/agents/codex/AGENTS.md +2 -0
  5. package/agents/copilot/copilot-instructions.md +2 -0
  6. package/agents/cursor/campaigns-os.mdc +2 -0
  7. package/compatibility.json +1 -1
  8. package/contracts/campaign-cart-checkout-field-contract.v0.json +10 -0
  9. package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
  10. package/contracts/commerce-surface-catalog.json +27 -27
  11. package/contracts/effects.v1.json +295 -47
  12. package/contracts/release-ledger.json +660 -0
  13. package/contracts/supported-surface.json +3 -3
  14. package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
  15. package/docs/brand-theme-bridge.md +2 -0
  16. package/docs/build-packet.md +67 -19
  17. package/docs/campaign-build-brief.md +48 -17
  18. package/docs/campaign-standardization-report.md +32 -0
  19. package/docs/design-source-package.md +7 -3
  20. package/docs/effects.md +10 -1
  21. package/docs/local-setup.md +1 -1
  22. package/docs/orientation-contract-reference.md +1 -1
  23. package/docs/polish-evidence.md +2 -0
  24. package/docs/progress-snapshots.md +5 -5
  25. package/docs/qa-and-test-orders.md +30 -3
  26. package/docs/readback.md +19 -2
  27. package/docs/runtime-readiness.md +1 -1
  28. package/docs/sdk-storage-compatibility.md +15 -0
  29. package/docs/skills-revision.md +10 -10
  30. package/docs/supported-surface.md +1 -0
  31. package/package.json +1 -1
  32. package/schemas/campaign-runtime-assembly-report.v0.schema.json +5 -1
  33. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  34. package/skills/campaign-readback-classification/SKILL.md +3 -3
  35. package/skills/campaign-run-evidence/SKILL.md +3 -3
  36. package/skills/contribution-intake/SKILL.md +3 -3
  37. package/skills/next-campaigns-build/SKILL.md +5 -3
  38. package/skills/next-campaigns-os/SKILL.md +10 -19
  39. package/skills/next-campaigns-os/references/session-intake.md +10 -6
  40. package/skills/next-campaigns-os-setup/SKILL.md +4 -3
  41. package/skills/next-campaigns-polish/SKILL.md +7 -5
  42. package/skills/next-campaigns-qa/SKILL.md +5 -3
  43. package/skills.json +10 -10
  44. package/src/brand-theme.mjs +21 -20
  45. package/src/brief-summary.mjs +275 -0
  46. package/src/build-brief.mjs +167 -15
  47. package/src/campaign-ecosystem.mjs +153 -31
  48. package/src/cli.mjs +182 -115
  49. package/src/contrast.mjs +649 -0
  50. package/src/design-source-publication.mjs +110 -3
  51. package/src/directory-lock.mjs +79 -7
  52. package/src/doctor/checks.mjs +141 -16
  53. package/src/doctor/inspect.mjs +13 -0
  54. package/src/doctor/next-step.mjs +15 -6
  55. package/src/input-currency.mjs +394 -0
  56. package/src/input-refresh.mjs +708 -0
  57. package/src/invocation.mjs +6 -2
  58. package/src/polish-browser.mjs +503 -201
  59. package/src/polish-deadline.mjs +7 -0
  60. package/src/polish-node.mjs +326 -46
  61. package/src/polish-readability.mjs +828 -0
  62. package/src/progress-node.mjs +18 -5
  63. package/src/qa-browser.mjs +211 -93
  64. package/src/qa-node.mjs +47 -7
  65. package/src/qc-accept.mjs +44 -8
  66. package/src/qc-check-registry.mjs +15 -7
  67. package/src/qc-results.mjs +326 -34
  68. package/src/readback.mjs +58 -10
  69. package/src/sdk-attribute-index.mjs +111 -23
  70. package/src/sdk-markup.mjs +49 -10
  71. package/src/sdk-repin.mjs +282 -0
  72. package/src/stage-ledger.mjs +235 -44
  73. package/src/stage-record.mjs +154 -30
  74. package/src/standardization-report.mjs +26 -1
  75. package/src/target-lock.mjs +9 -1
  76. package/src/tooling-setup.mjs +1 -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.52.0",
3
+ "surface_version": "1.54.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"
@@ -120,7 +120,7 @@
120
120
  "sha256": "dfc9abed38d456969e47a21f606d308a03bdf036f3466f7d6e47a602747dacf4"
121
121
  },
122
122
  "contracts/effects.v1.json": {
123
- "sha256": "f49ac6dcbf441c90b12b51be9bc07805f6228f4143784045c5925e3c155a6181"
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
  },
@@ -94,6 +94,8 @@ used and reported with `theme.foreground.low_contrast`. With no declared CTA
94
94
  foreground, the output is unchanged. Declarations inside CSS comments are not
95
95
  read, so a commented-out token or rule never supplies a CTA or body text colour.
96
96
 
97
+ Contrast is computed with the same unrounded measurement Polish and QA use, so a declared colour at 4.495:1 now warns. The generator's colour choices are unchanged. Polish measures the rendered text at its real size and weight: a qualifying large label at 3:1 passes there, and smaller text produces a warning the operator can accept.
98
+
97
99
  ### Body text prefers the darkest declared text token
98
100
 
99
101
  A declared text token is a `:root` custom property in the selected source whose
@@ -48,8 +48,10 @@ report writes, polish capture, progress, run closeout and QA compare that local
48
48
  identity. Material spec hashes still bind the current revision; a changed ID or
49
49
  content cannot reuse earlier proof. After a material revision, doctor and
50
50
  `next` warn `spec.material_stale` (the spec no longer has the material hash
51
- prepare-build bound on the Assembly Report, which QA refuses); follow `next` to
52
- refresh preparation and affected evidence. Keep the spec, source, dependency
51
+ the Assembly Report binds, which QA refuses), and `next` routes to the build
52
+ and names the refresh: run `campaigns-os record spec --packet <packet>` to bind
53
+ the new content and keep stage history (see
54
+ [Refreshing after a CampaignSpec change](#refreshing-after-a-campaignspec-change)). Keep the spec, source, dependency
53
55
  pins and canonical sidecars in Git. Use `readback` and `next` after a fresh
54
56
  checkout; identity survives the move, but proof freshness is assessed again.
55
57
 
@@ -523,16 +525,13 @@ writing it would declare an analytics contract QA then blocks on. A derived
523
525
  field the entry does not carry at all is listed under `not_in_target[]` and
524
526
  left as it is.
525
527
 
526
- After a write, the sidecars that carry the spec's identity are re-bound. The
528
+ `spec derive` writes the spec alone and no longer rebinds identity: the
527
529
  Build Context's `spec.hash` / `spec.material_hash` and the Assembly Report's
528
- `identity.spec_hash` / `identity.spec_material_hash` move to the new spec
529
- when they were bound to the one derive replaced and name that spec file
530
- (`rebound` on the result; two packets sharing a target repo never re-bind
531
- each other's sidecars);
532
- QA's verdict and `bundle check` correlate against the material hash, so this
533
- is what keeps a derive-then-QA run conformant. A sidecar already carrying
534
- another identity is left alone with a `spec.derive.identity_not_rebound`
535
- warning naming `prepare-build`. A derived route change also warns
530
+ `identity.spec_hash` / `identity.spec_material_hash` keep the spec they were
531
+ bound to, so every build, Polish and QA record made against the earlier
532
+ content reads owed again. Run `record spec` after it (the result's `next`
533
+ names it) to bind the derived spec; QA's verdict and `bundle check` correlate
534
+ against the bound material hash. A derived route change also warns
536
535
  `spec.derive.projection_stale`: the packet's page-kit projection
537
536
  (`source_html.pages[].page_kit`) and the Build Context page map were prepared
538
537
  from the old routes, and `prepare-build` (or `start`) regenerates them. A
@@ -552,8 +551,8 @@ the spec resolves to, which must lie inside the spec's own directory or the
552
551
  target repo (`spec.derive.spec_escapes_boundary` otherwise, so a symlinked
553
552
  `spec.local_path` cannot redirect the write); the retained doctor sidecar is
554
553
  marked stale after a write, and a terminal build gets a
555
- `spec.derive.build_stale` warning naming the rebuild (doctor does not
556
- fingerprint the spec itself; the re-bound sidecar identity is what QA reads). `--dry-run` prints the
554
+ `spec.derive.build_stale` warning naming `record spec`, after which `next`
555
+ routes back to the build. `--dry-run` prints the
557
556
  same diff and writes nothing; unknown flags and a valued `--dry-run` are
558
557
  rejected. Exit 2 with `spec.derive.*` error codes and nothing written when the
559
558
  packet cannot be read, `spec.local_path` is absent or not a file, the spec is
@@ -828,12 +827,19 @@ Blockers (not waivable — the markup provably does not do what it says):
828
827
  - `CHECKOUT_NOT_FORM` — `data-next-checkout` on an element that is not `<form>`.
829
828
  - `WRONG_FIELD_NAME` — `data-next-checkout-field` with a value the SDK does not
830
829
  map. The set is vendored from the SDK at a named tag
831
- (`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`,
832
831
  `phone`, `address1`, `address2`, `city`, `province`, `postal`, `country`,
833
832
  `payment-method`, `accepts_marketing`, `cc-number`, `cc-month`, `cc-year`,
834
833
  `exp-month`, `exp-year`, `cvv`, the legacy `card-*` spellings, and any
835
834
  `billing-` prefixed name). The message names the SDK spelling for the usual
836
- 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.
837
843
  - `MISSING_SELECTOR_ID_MATCH` — an `add-to-cart` button whose
838
844
  `data-next-selector-id` names no selector on the page (an element that is a
839
845
  bundle, package, cart or upsell selector; another element echoing the id
@@ -1058,8 +1064,13 @@ full.
1058
1064
  and auto-discover `campaign-build-brief.yaml`, `.yml`, or `.json` from the
1059
1065
  source root or target repo. When none is present, Campaigns OS creates a guided
1060
1066
  draft at `.campaign-runtime/input/campaign-build-brief.normalized.json`.
1061
- See [Campaign Build Brief](./campaign-build-brief.md) for the schema and
1062
- prepared/guided behavior.
1067
+ A brief file is guided unless it sets `"brief_mode": "prepared"` (a campaign
1068
+ whose Assembly Report already records a prepared brief stays prepared). Save
1069
+ answers later with `record brief`, which keeps stage evidence unless the brief's
1070
+ material content changes; a presentation change makes build, Polish and QA
1071
+ owed again, and a `qa_policy` change makes QA alone owed again. See
1072
+ [Campaign Build Brief](./campaign-build-brief.md) for the schema, the mode
1073
+ rule and the stage map.
1063
1074
 
1064
1075
  `start` / `prepare-build` also prepares the normalized Design Source Package at
1065
1076
  `.campaign-runtime/input/design-source-package.json`. When that path is absent,
@@ -1681,6 +1692,43 @@ records it (`campaigns-os record setup|build|polish`; deploy and QA record their
1681
1692
  own stages) → calls `next` again → repeat until stage="done"
1682
1693
  ```
1683
1694
 
1695
+ ### Refreshing after a CampaignSpec change
1696
+
1697
+ When the CampaignSpec's material content changes, every build, Polish and QA record made against the earlier content stops counting as current, in local-spec, saved-Map and gateway packets alike. For a saved Map or gateway packet, Campaigns OS compares the copy intake fetched; it does not check whether that copy is the latest remote revision, and reports the remote as unconfirmed. Doctor warns `spec.material_stale`, and `next`, progress, readback, the Assembly Report's own status and the QA gate show the stages that are owed again; `next` routes to the first one. Run `campaigns-os record spec --packet <packet>` to bind the new content: it updates the build context and report identity, re-reads the brief file last saved (by intake or `record brief`), keeps waivers, warning accepts, the applied theme and deploy settings, and moves each superseded stage record into that stage's `history`, which is never used as current proof. `spec derive` no longer rebinds identity; run `record spec` after it. A change to the set, order or routes of active pages still needs intake. Re-recording an unchanged build after a spec change keeps the build owed, because the change has not reached the pages, unless the operator records a reason with `--deviation-reason`; this holds even when `record spec` was not run, and that `record build` also marks Polish and QA owed again. Polish stays owed until a new `polish capture`, and QA must produce a new verdict against the current content. Polish measurements of byte-identical output keep their values, but the Polish stage is owed again. These checks are tamper evidence, not proof: a hand-written record that copies the current values, or a packet pointed at an older copy of the spec, is not detected.
1698
+
1699
+ The CampaignSpec's material content is the whole spec except `spec_identity`,
1700
+ `slug`, `map_id` and `saved_at`; a page `label` edit is material. A spec
1701
+ change affects the stages as follows:
1702
+
1703
+ | Stage | After a spec material change |
1704
+ |---|---|
1705
+ | `prepare_build` | unchanged (`record spec` refuses a page-scope change, which needs intake) |
1706
+ | `doctor` | recomputed on every read |
1707
+ | `setup` | kept |
1708
+ | `assembly` | owed: `required`, `required_by: "spec"`, `required_for: ["polish", "qa"]` |
1709
+ | `polish` | owed: `required`, `required_by: "spec"`, `required_for: ["qa"]` |
1710
+ | `deploy` | kept |
1711
+ | `qa` | owed: `required`, `required_by: "spec"`, `required_for: []` |
1712
+
1713
+ `record spec` refuses, writing nothing, in this order: `spec_unreadable` (the
1714
+ spec does not parse), `spec_identity_changed` (its map id or local spec id
1715
+ names another campaign than the packet or the report), `page_scope_changed`,
1716
+ then, for the brief file last saved (by intake or `record brief`), `brief_too_large`,
1717
+ `brief_source_is_package_artifact` and `brief_file_missing` (the recorded
1718
+ path is missing or is not a readable regular file: a directory, a pipe or
1719
+ socket, a symlink to nothing, or a file without read permission). When it must
1720
+ re-derive the brief, it also refuses `brief_inputs_unavailable` (the Build
1721
+ Context lacks an intake product re-deriving the brief needs). It reads
1722
+ `unchanged` and writes nothing when the bound material equals the spec and no
1723
+ stage's spec or brief stamp differs from the current content; a stage that
1724
+ does not record which content it was made against is named in a
1725
+ `binding_unknown` notice instead. Otherwise it refreshes, re-deriving the
1726
+ normalized brief from the brief file last saved, so an edit made to the
1727
+ normalized brief itself is replaced by what its source gives. A stage whose spec
1728
+ stamp already equals the new content (a build recorded after the edit) is not
1729
+ demoted. `record spec` does not write the Build Packet. `--dry-run` runs every
1730
+ check and writes nothing.
1731
+
1684
1732
  ### Recording stage completion
1685
1733
 
1686
1734
  Setup, build and polish completion is recorded with one command each, never by
@@ -1689,10 +1737,10 @@ hand-editing `.campaign-runtime/` JSON:
1689
1737
  | Command | Writes | Refused (nothing written) when |
1690
1738
  |---|---|---|
1691
1739
  | `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 |
1692
- | `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 |
1740
+ | `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; a completed `stages.deploy` whose `source_build_fingerprint` names other output is reset 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, or `stages.setup` is not terminal |
1693
1741
  | `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 |
1694
1742
  | `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` |
1695
- | `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 |
1743
+ | `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 |
1696
1744
 
1697
1745
  Each command also refuses a stage `next` has not reached: while doctor's
1698
1746
  prepare-build gate is set (`next` answers prepare-build) or while an earlier
@@ -35,7 +35,7 @@ The Build Packet, Build Context, and Assembly Report all reference that normaliz
35
35
 
36
36
  ## Modes
37
37
 
38
- Prepared mode is for veteran users. A complete brief should let the build proceed without business questions. If a supplied brief is incomplete or contradictory, doctor blocks with `build_brief.*` errors.
38
+ Prepared mode is for veteran users, and a brief file selects it with `"brief_mode": "prepared"` (see [Brief mode](#brief-mode)). A complete brief should let the build proceed without business questions. If a prepared brief is incomplete or contradictory, doctor blocks with `build_brief.*` errors.
39
39
 
40
40
  Guided mode is for new or partial inputs. Campaigns OS drafts a brief from CampaignSpec, page mappings, template family, source assets, and available runtime hints. It records only high-impact unresolved questions as warnings so existing builds still run while the business uncertainty is visible.
41
41
 
@@ -56,27 +56,58 @@ The CLI avoids SDK/page-kit jargon in questions. The implementation can resolve
56
56
 
57
57
  Question 5 is asked only when the CampaignSpec maps a surface that fills the template's promo placeholders: a `funnels[].promo_codes` roster, or a checkout page's enabled `exit_intent` or `promo_code_input`. It never asks for approval of the source design's own promo, proof or urgency copy, which is built as designed (see below). Without such a surface the guided draft sets `header_claim_source` and `timer_label` to `"none"`: the template's promo placeholders are removed.
58
58
 
59
- ## Answering The Questions
59
+ ## Saving Answers
60
60
 
61
- An answer counts only once it is in a brief file that `start` or `prepare-build` reads. The guided draft is regenerated on every run, so an answer given in conversation, or typed into the normalized draft, does not stick.
61
+ Save brief answers with `campaigns-os record brief --packet <packet>`. Copy the normalized draft to `campaign-build-brief.json` in the target repository, set the fields named in the open questions, and run `record brief`. You do not need to re-run `start` or `prepare-build`, and no stage evidence is cleared unless the saved answers change the brief's material content. `record brief` refuses a path that points at one of the files Campaigns OS writes itself, such as the normalized brief.
62
62
 
63
- 1. Copy `.campaign-runtime/input/campaign-build-brief.normalized.json` to `campaign-build-brief.json` in the target repo. A brief file replaces the guided draft whole, so starting from the copy keeps the fields the draft already filled; any question the file leaves open blocks as a prepared brief.
64
- 2. Set the fields that close each open question:
63
+ A brief is prepared, where every open question blocks, only when the file sets `"brief_mode": "prepared"`. A brief file without `brief_mode` is guided: questions you leave open remain warnings. One exception keeps existing campaigns as they were built: when the campaign's Assembly Report already records a prepared brief, a file without `brief_mode` stays prepared. Set `"brief_mode": "guided"` to make it guided.
65
64
 
66
- | Question | Fields |
67
- |---|---|
68
- | `page_design_authority` | `design_authority.<page_id>.source` for each page named in the question |
69
- | `brand_palette_cta` | `brand.commerce_palette_source`, `brand.cta_style` |
70
- | `variant_media_rules` | `media.sold_variants`, `media.allow_other_variant_colors` |
71
- | `bundle_pricing_presentation` | `offer_presentation.bundle_cards.primary_price` |
72
- | `promo_urgency_copy` | `promo_urgency.header_claim_source` (`campaign_offers` or `none`), `promo_urgency.timer_label` (the template timer's label, or `none`) |
73
- | `payment_methods_trust` | `commerce_surfaces.payment_methods_allowed` |
74
- | `canonical_display_names` | `canonical_display.product_name_source` |
75
- | `regulated_claims` | one of `campaign_intent.compliance.approved_benefit_language`, `.forbidden_claims`, `.approved_claims`, `.copy_rules` |
65
+ Saving a brief whose content is unchanged keeps all stage evidence and statuses. Reformatting, reordering keys, writing a default value explicitly, or changing only `_meta` also keeps all stage evidence and statuses; the save may rewrite the brief files. When the presentation sections change (`campaign_intent`, `design_authority`, `brand`, `media`, `offer_presentation`, `promo_urgency`, `commerce_surfaces`, `canonical_display`, `template_residue_policy`, or any section you add), build, Polish and QA are owed again. When only `qa_policy` changes, QA alone is owed again. Setup and deploy records are kept. Each affected stage records when its inputs changed. Each replaced record is kept under the stage's `history`; the last five superseded records per stage are kept, and older ones are dropped. History is display only and is never used as current proof.
76
66
 
77
- 3. Re-run `start` or `prepare-build` with the same arguments. The file is found in the target repo automatically, or pass `--brief <file>`. Doctor's `build_brief.guided_questions` warning names the open questions and their fields.
67
+ Recording build again over output that has not changed since the brief or CampaignSpec changed does not make the build current: `next` keeps asking for a build, because the change has not reached the pages. If the operator decides the change needs no change to the built pages, record build with `--deviation-reason "<the operator's reason>"`; the build is then recorded with a warning, and the reason is shown in `next` and the QC handoff. `--deviation-reason` is a general flag any caller can pass: Campaigns OS records the reason, but cannot tell whether the operator chose it. A CampaignSpec change that was never saved with `record spec` is recorded by the next `record build`, `record polish` or QA run, with the same effect; `record build` then also marks Polish and QA owed again. An edit to the brief file is not read until it is saved with `record brief`: until then the build reads the saved brief, and doctor warns `build_brief.input_unsaved`. The same applies after `prepare-build --force`, which now keeps stage history, archives the build, Polish and QA records it clears, and keeps the record that the inputs changed. Recording Polish again does not make it current until a new `polish capture` runs after the change. Recording Polish as skipped, with a skip reason, while it is owed is the operator's decision to skip it: the input change no longer makes Polish owed. The Polish gate still passes only a completed Polish, so `next` stays at Polish until one is recorded (see `docs/polish-evidence.md`).
78
68
 
79
- Re-running regenerates the Assembly Report. Before any stage has recorded evidence, that costs nothing. Once a stage has recorded evidence (setup, build, polish or later), the re-run is refused unless you pass `--force`, which resets those stages and clears their evidence. Answer the questions right after `start`.
69
+ A build, Polish or QA record that does not say which brief and CampaignSpec content it was made against counts as unconfirmed, not current. Records made by earlier releases are unconfirmed, so after upgrading, `next` routes back to build once; record build and Polish again and run QA again. These checks are tamper evidence, not proof: a record written by hand that copies the current values or removes the record of an input change, or a packet pointed at an older copy of the spec or brief, is not detected.
70
+
71
+ The fields that close each open question (doctor's `build_brief.guided_questions` warning names the open questions and their fields):
72
+
73
+ | Question | Fields |
74
+ |---|---|
75
+ | `page_design_authority` | `design_authority.<page_id>.source` for each page named in the question |
76
+ | `brand_palette_cta` | `brand.commerce_palette_source`, `brand.cta_style` |
77
+ | `variant_media_rules` | `media.sold_variants`, `media.allow_other_variant_colors` |
78
+ | `bundle_pricing_presentation` | `offer_presentation.bundle_cards.primary_price` |
79
+ | `promo_urgency_copy` | `promo_urgency.header_claim_source` (`campaign_offers` or `none`), `promo_urgency.timer_label` (the template timer's label, or `none`) |
80
+ | `payment_methods_trust` | `commerce_surfaces.payment_methods_allowed` |
81
+ | `canonical_display_names` | `canonical_display.product_name_source` |
82
+ | `regulated_claims` | one of `campaign_intent.compliance.approved_benefit_language`, `.forbidden_claims`, `.approved_claims`, `.copy_rules` |
83
+
84
+ ### Brief mode
85
+
86
+ The mode is decided in this order:
87
+
88
+ 1. `"brief_mode": "prepared"` in the file: prepared.
89
+ 2. `"brief_mode": "guided"` in the file: guided.
90
+ 3. No `brief_mode`, and the campaign's Assembly Report on disk records a prepared brief: prepared.
91
+ 4. No `brief_mode` otherwise, including the generated draft and every new hand-written file: guided.
92
+ 5. Any other `brief_mode` value is the error `build_brief.brief_mode`, and the brief reads `invalid`.
93
+
94
+ The file's own `_meta` (including a copied draft's `_meta.mode`) is never read for the mode; the normalized brief records how the mode was decided in `_meta.mode_source` (`field`, `legacy_report` or `generated`). Input errors block in both modes. In guided mode, open questions and evaluation gates stay warnings.
95
+
96
+ ### What a brief change makes owed
97
+
98
+ The brief's material content is the normalized brief without `_meta`, `status`, `questions`, `gates`, `confidence`, `schema_version`, `brief_mode` and `qa_policy.enforcement`, in two parts: `qa_policy`, and every other section (presentation).
99
+
100
+ | Stage | Presentation changed | `qa_policy` changed |
101
+ |---|---|---|
102
+ | `prepare_build` | brief blockers re-derived; other blockers kept | re-derived |
103
+ | `doctor` | recomputed on every read | recomputed on every read |
104
+ | `setup` | kept | kept |
105
+ | `assembly` | owed (`required`, `required_by: "brief"`) | kept |
106
+ | `polish` | owed (`required`, `required_by: "brief"`) | kept |
107
+ | `deploy` | kept | kept |
108
+ | `qa` | owed (`required`, `required_by: "brief"`) | owed |
109
+
110
+ Only completed records are made owed. A skipped stage stays skipped.
80
111
 
81
112
  ## Risky Defaults
82
113
 
@@ -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
@@ -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,
@@ -127,6 +128,14 @@ itself carries QA's own read).
127
128
 
128
129
  **Accepting a warning.** `campaigns-os checkpoint accept` records an operator's decision about a warning that a Campaigns OS check measured. It does not change readiness, does not remove the warning, does not change the status `next` reports, and cannot be used on review, unexercised, or excluded results. The warning must already be on record (in the doctor snapshot `next` writes, the Polish capture, or the full QA verdict) before it can be accepted. The accept is bound to the exact measured state and lapses when that state changes, or when its evidence is stale or cannot be reproduced. An optional expiry or review condition adds a second bound. Run it only with the operator's explicit decision, made after the warning was shown to them. A plausible name is not authorization. The command refuses placeholder and automation identities. Results are re-derived from the package's raw captures on every read, and a result whose capture is missing or does not match is listed as unexercised. These checks are tamper evidence, not proof of authorship: a hand-written record that copies every value and recomputes the checksum, or a hand-written result written together with a matching capture, is not detected.
129
130
 
131
+ ### `record brief` and `record spec`
132
+
133
+ **Saving brief answers.** `campaigns-os record brief --packet <packet>` saves Campaign Build Brief answers without re-running `start` or `prepare-build`. It writes the normalized brief under `{target}/.campaign-runtime/input/`, the Build Packet, the build context, the Assembly Report and the doctor-sidecar stale stamp, and sends nothing. It refuses a path that points at one of the files Campaigns OS writes itself, such as the normalized brief. Saving a brief whose content is unchanged keeps all stage evidence and statuses. Reformatting, reordering keys, writing a default value explicitly, or changing only `_meta` also keeps all stage evidence and statuses; the save may rewrite the brief files. When the presentation sections change, build, Polish and QA are owed again. When only `qa_policy` changes, QA alone is owed again. Setup and deploy records are kept. Each replaced record is kept under the stage's `history`; the last five superseded records per stage are kept, and history is never used as current proof.
134
+
135
+ **Binding a CampaignSpec change.** `campaigns-os record spec --packet <packet>` binds the new CampaignSpec content: it updates the build context and report identity, re-reads the brief file last saved (by intake or `record brief`), keeps waivers, warning accepts, the applied theme and deploy settings, and moves each superseded stage record into that stage's `history`. It writes the build context, the Assembly Report, the normalized brief and the doctor-sidecar stale stamp; it does not write the Build Packet, and it sends nothing. `spec derive` no longer rebinds identity; run `record spec` after it. A change to the set, order or routes of active pages still needs intake.
136
+
137
+ Both rows are tier C, because a change makes recorded stages owed again, and each `--dry-run` form is tier none: it writes nothing and appends no lifecycle entry. A CampaignSpec change that was never saved with `record spec` is recorded by the next `record build`, `record polish` or QA run, with the same effect; `record build` then also marks Polish and QA owed again. An edit to the brief file is not read until it is saved with `record brief`: until then the build reads the saved brief, and doctor warns `build_brief.input_unsaved`. Intake now also refuses, without `--force`, to discard recorded waivers, warning accepts, stage history, an applied theme, or a recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation; with `--force`, it keeps stage history, archives the build, Polish and QA records it clears, and keeps the record that the inputs changed.
138
+
130
139
  ## How to read a row
131
140
 
132
141
  ```jsonc
@@ -152,7 +161,7 @@ itself carries QA's own read).
152
161
 
153
162
  There is **one row per command and per effect-changing flag combination**. The
154
163
  flags that change what the invocation does to the world are listed once, in
155
- `vocabulary.effect_changing_flags`: `--browser`, `--built`, `--dry-run`,
164
+ `vocabulary.effect_changing_flags`: `--apply`, `--browser`, `--built`, `--dry-run`,
156
165
  `--emit-packet`, `--example`, `--force`, `--from-store`, `--list`,
157
166
  `--no-live-refs`, `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
158
167
  `--no-write`, `--republish`, `--test-order`, `--write`, `--write-map`. Flags
@@ -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.52.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.54.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.52.0`
29
+ Supported surface at generation time: `1.54.0`
30
30
 
31
31
  ## Forward compatibility
32
32
 
@@ -119,6 +119,8 @@ It warns when a video, or an image over 500,000 transfer bytes, comes from the p
119
119
 
120
120
  The size check covers `<img>` elements in the page, including those inside open and closed shadow roots; images inside an iframe, `<input type=image>`, `<object>`, SVG `<image>` and CSS background images are not checked for size, and only their transfer bytes are weighed.
121
121
 
122
+ **Readability.** `polish capture` also measures text contrast on every page under `_site/<slug>/` (up to 128 pages per run), at desktop (1440×1200) and mobile (390×844) widths, device pixel ratio 1, including campaigns whose pages are all template stock: the Polish route plan now covers the mapped pages plus every built page, while the page-load capture keeps its own page set. On a campaign with no mapped pages, `polish capture` measures readability, prints the page-load message as a warning, and exits successfully. The text it reads: purchase, accept and decline controls; submit buttons and submit-type inputs; SDK action controls; bundle cards (names, prices, badges and the card selected at load); order bumps; prices; checkout labels and placeholder hints; and other body text. Ratios follow WCAG 2.x and are compared unrounded: 4.5:1 for normal text, 3:1 for large text (at least 24 CSS px, or at least 18.66 CSS px at weight 700 or more; semibold does not qualify). Colours in any CSS Color 4 form the browser reports are converted to sRGB; values outside sRGB are clipped and marked. Backgrounds are composited through transparent layers. SVG text is measured with its fill colour, and placeholder hints with their own colour, size and weight. Gradients, background images, pseudo-element backgrounds, a `::first-line` or `::first-letter` style that changes the text's colour, background or opacity, overlapping positioned layers or images (and the shapes of an SVG drawing under its text), text clipped to a background, colours that cannot be read (including a text fill colour, and an SVG fill that is not a plain colour or is combined with a stroke), and controls marked disabled only by the SDK's `next-disabled` class produce a review result with a reason and a crop; so do opacity, filters, blend modes and masks on the text or on any element around it. `clip-path` is ignored. Disabled controls (`disabled` or `aria-disabled="true"`) are listed as excluded (inactive control) and are never accepted; `readonly` is not disabled. Polish measures the states present when the page loads — default, selected, active, and open panels — without clicking, scrolling or typing. Text that has not rendered, controls still loading, controls hidden at load, text supplied by CSS `content`, selection states and collapsed panels not present at load, text inside closed shadow roots or cross-origin frames, pages measured before the SDK, stylesheets or fonts were ready, and pages not captured are listed as unexercised. Overlapping layers are detected by box intersection: a layer the page paints over text in another way is not detected. A ratio under the requirement is a warning. Warnings never block Polish or change readiness. The probe reads the page without changing it, and adds at most about five minutes of work per run; pages it could not reach in that time are listed as unexercised. The stored measurements are re-derived on every read, and the page set, widths and campaign are checked against the current build; but the colours the browser reported are tamper-evident only, like other Polish captures. Readability and media weight results read as stale when the built output differs from the recorded build, even before a new `record build`.
123
+
122
124
  ### Collector response records (the producer's wire form)
123
125
 
124
126
  Inside the producer, the browser adapter's CDP collector emits one
@@ -18,8 +18,7 @@ blocked QA keeps it open. A progress delivery cannot close a run.
18
18
 
19
19
  The strict v0 object carries its schema, package version, observation timestamp,
20
20
  producer (`next` or `qa`), opaque progress stream and sequence, prior snapshot ID,
21
- content digest, Map ID and separate revision/spec/build hashes. Six independent
22
- stage records retain observed status and explicit build binding. Continuation
21
+ content digest, Map ID and separate revision/spec/build hashes. Six independent stage records carry each stage's effective status and explicit build binding: the status the Assembly Report records, except that a stage made owed again by a brief or CampaignSpec change reads `required`, and a stage whose inputs cannot be confirmed reads `unknown`. Continuation
23
22
  retains the canonical stage, blocked/divergent flags, fixed action IDs and gate
24
23
  states. Preview carries only presence and a hash of the URL. An optional QA block
25
24
  retains the exact verdict ID, disposition (including `ready_with_exceptions`),
@@ -28,8 +27,7 @@ retained verdict; recovery uses `qa publish`, without repeating an order.
28
27
 
29
28
  No commands, prompts, free text, content, local paths, URLs, query strings,
30
29
  credentials, order values or customer payloads enter the wire. Unknown vocabulary
31
- becomes a fixed `unknown` marker and cannot imply a passing continuation. A
32
- stage's observed `completed` status is a ledger claim, not independent verification.
30
+ becomes a fixed `unknown` marker and cannot imply a passing continuation. A stage's `completed` status is a ledger claim, checked only against the brief and CampaignSpec content it records; it is not independent verification.
33
31
  Only matching fingerprints carry `matching`; absent or unverifiable binding is
34
32
  `unconfirmed`. A current local output hash never proves a deployment is current.
35
33
 
@@ -67,7 +65,9 @@ rename; a live process is never evicted. A lock directory with no owner record
67
65
  a second. If one is found, or if recovery itself is interrupted, capture fails
68
66
  closed and the warning names the affected `.allocation-lock` directory: stop
69
67
  all Campaigns OS writers for that target, then remove that directory before
70
- 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
71
71
  older Campaigns OS release against the same target at the same time.
72
72
 
73
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
@@ -726,6 +728,16 @@ spec resolved from the portal for the run — keep publish-by-default regardless
726
728
  of consent: those verdicts are the QA tab's product surface, not telemetry.
727
729
  Explicit flags always win in both directions for saved-Map QA.
728
730
 
731
+ A run whose base URL is a local address (`localhost` or any `*.localhost`
732
+ name, any `127.x.x.x` address, `0.0.0.0`, `[::1]` or an IPv4-mapped loopback,
733
+ from `--base-url` or the packet's `deploy.preview_url`) keeps its verdict local
734
+ by default, whatever the spec source. Those runs are checks on your own
735
+ machine, and people watching the Map's QA tab should not see them. The output
736
+ names the destination and how to publish: `qa publish` for the stored verdict,
737
+ or `--post-verdict` on the next run. `--post-verdict` publishes a local-address
738
+ run, and `publish_decision.reason` reads `loopback_base_url` when the default
739
+ kept it local.
740
+
729
741
  A packet with `local_spec_id` always keeps its verdict and progress local.
730
742
  `qa run` suppresses portal publication even with `--post-verdict`; the flag
731
743
  does not turn a local ID into a Map destination. `qa publish` refuses such a
@@ -1810,7 +1822,12 @@ The browser driver intentionally behaves like a user:
1810
1822
  proven against the persisted-order voucher read-back
1811
1823
  - checkout is advanced through the visible cart/checkout button
1812
1824
  - address autocomplete is settled or closed before submit
1813
- - Spreedly card and CVV iframes are filled with sequential keystrokes
1825
+ - the card and CVV iframes are filled with sequential keystrokes: Spreedly's
1826
+ iFrame fields on SDK 0.4.40 and earlier, and on 0.4.41 and later the
1827
+ Spreedly-hosted fields that NEXT's `payments.29next.com` script mounts.
1828
+ Typing starts once the checkout form drops `next-loading-spreedly` (the
1829
+ fields are ready), and both fields are read back and retyped, up to three
1830
+ attempts in all, before `card_fields_filled` passes
1814
1831
  - the real submit button is clicked without fabricating SDK state
1815
1832
 
1816
1833
  The intended QA order matrix is:
@@ -1893,6 +1910,8 @@ A redirect to the right page is fine. Missing links, links whose query differs,
1893
1910
 
1894
1911
  **Accepting a warning.** `campaigns-os checkpoint accept` records an operator's decision about a warning that a Campaigns OS check measured. It does not change readiness, does not remove the warning, does not change the status `next` reports, and cannot be used on review, unexercised, or excluded results. The warning must already be on record (in the doctor snapshot `next` writes, the Polish capture, or the full QA verdict) before it can be accepted. The accept is bound to the exact measured state and lapses when that state changes, or when its evidence is stale or cannot be reproduced. An optional expiry or review condition adds a second bound. Run it only with the operator's explicit decision, made after the warning was shown to them. A plausible name is not authorization. The command refuses placeholder and automation identities. Results are re-derived from the package's raw captures on every read, and a result whose capture is missing or does not match is listed as unexercised. These checks are tamper evidence, not proof of authorship: a hand-written record that copies every value and recomputes the checksum, or a hand-written result written together with a matching capture, is not detected.
1895
1912
 
1913
+ A readability warning is keyed by its colour pair and text size class on each page and width. The QC handoff lists each pair once with every page and width that uses it. Accepting it, with one `checkpoint accept` naming every listed ref, records the operator's decision for those pages. The accept states whether it covers normal text, large text, or both, by which size-class refs are named. When either colour changes, the pair becomes a new warning, and the old accept no longer matches any result (it is listed as orphaned). An accept lapses when the exact colours, ratio, font size or font weight of any text using the pair on that page change, when its capture is stale — including built output that changed since the build was recorded — or when it cannot be reproduced, including a missing crop. A page that starts using the same pair later is a new warning. An accept never hides review, unexercised or excluded results.
1914
+
1896
1915
  ## Launch Readiness Note
1897
1916
 
1898
1917
  Campaigns OS can prove the campaign build, SDK wiring, browser behavior, and
@@ -1960,6 +1979,14 @@ placeholder-text blocker and the other residue gates still run. The emitted
1960
1979
  minimal packet is marked `_synthesized` — it points doctor/QA at the built
1961
1980
  output and family, and is not a substitute for a real Build Packet.
1962
1981
 
1982
+ `doctor --built` reads the local built files only. `--base-url` is not
1983
+ fetched: it fills `deploy.preview_url` in the minimal packet, and the output
1984
+ says so (`derived.base_url.fetched: false` in JSON). A clean `doctor --built`
1985
+ run with a preview URL says nothing about the preview. To check the served
1986
+ pages, run `qa run --site <repo> --base-url <url> --family <family> --browser`.
1987
+ The emitted minimal packet does not drive packet QA: `qa resolve` and
1988
+ `qa run --packet` block on it, because it carries no local CampaignSpec.
1989
+
1963
1990
  **Trade-off — non-packet QA is narrower than packet-driven QA.** It runs the
1964
1991
  built-output gates (residue, placeholder text, demo-asset, pricing-CSS, brand
1965
1992
  contract) but **skips the CampaignSpec/source-HTML-driven checks** a packet
package/docs/readback.md CHANGED
@@ -6,7 +6,9 @@ Packet, the doctor output sidecar, the build context, the assembly report, a QA
6
6
  verdict, and a findings export when present. It reads each of those files at
7
7
  most once, plus two fixed Git metadata files (the nearest `.git` entry at the
8
8
  target or an ancestor, and that Git directory's `logs/HEAD` reflog) for the
9
- freshness comparison.
9
+ freshness comparison, and the CampaignSpec and normalized Campaign Build Brief
10
+ the Build Packet names, whose current content decides each stage's effective
11
+ status.
10
12
 
11
13
  The safety contract is the point of the command. The readback **writes nothing**
12
14
  under the target — not even a lifecycle journal entry, which every other command
@@ -107,7 +109,7 @@ RUN IDENTITY
107
109
  template_family = olympus [build packet]
108
110
  qa run_id = MSRBUNDLEFIXTURE000000000000 [QA verdict]
109
111
 
110
- STAGES [assembly report; report status: prepared]
112
+ STAGES [assembly report; report status: prepared; effective: prepared]
111
113
  prepare_build completed
112
114
  doctor pending
113
115
  setup pending
@@ -129,6 +131,21 @@ QA VERDICT [QA verdict; disposition: ready — Campaigns OS is the verdict auth
129
131
  pass http:checkout (family funnel-flow)
130
132
  ```
131
133
 
134
+ ### STAGES
135
+
136
+ The STAGES header prints the status the Assembly Report records and the status recomputed from each stage's effective status. A stage's effective status is the recorded one, except that a stage made owed again by a brief or CampaignSpec change reads `required`, and a stage whose inputs cannot be confirmed reads `unknown`. When the two differ, the stage line adds the recorded status and the reason. Superseded records appear only under "history (not current proof)".
137
+
138
+ ```
139
+ STAGES [assembly report; report status: completed; effective: prepared]
140
+ prepare_build completed
141
+ doctor pending
142
+ setup completed
143
+ assembly required (recorded: completed; spec_material_changed)
144
+ polish required (recorded: completed; spec_material_changed)
145
+ deploy completed
146
+ qa required (recorded: completed; spec_material_changed)
147
+ ```
148
+
132
149
  ## The JSON contract
133
150
 
134
151
  ```json
@@ -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.52.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.54.0`.
12
12
 
13
13
  ## What this is
14
14