@nextcommerce/campaigns-os 1.60.0 → 1.62.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 (161) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +244 -0
  3. package/README.md +86 -438
  4. package/agents/claude/CLAUDE.md +20 -0
  5. package/agents/codex/AGENTS.md +19 -0
  6. package/agents/copilot/copilot-instructions.md +19 -0
  7. package/agents/cursor/campaigns-os.mdc +21 -0
  8. package/campaign-spec/README.md +7 -0
  9. package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
  10. package/campaign-spec/dist/analytics-vendors.js +44 -0
  11. package/campaign-spec/dist/checkout-flow.d.ts +18 -0
  12. package/campaign-spec/dist/checkout-flow.js +28 -0
  13. package/campaign-spec/dist/index.d.ts +4 -2
  14. package/campaign-spec/dist/index.js +5 -1
  15. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
  16. package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
  17. package/campaign-spec/dist/rules/campaign-currencies.d.ts +29 -0
  18. package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
  19. package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
  20. package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
  21. package/campaign-spec/dist/rules/index.d.ts +2 -0
  22. package/campaign-spec/dist/rules/index.js +6 -0
  23. package/campaign-spec/dist/types.d.ts +43 -9
  24. package/compatibility.json +1 -1
  25. package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
  26. package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
  27. package/contracts/commerce-surface-catalog.json +68 -61
  28. package/contracts/effects.v1.json +549 -165
  29. package/contracts/fixtures/runtime-recipe/accept/current.json +3 -0
  30. package/contracts/fixtures/runtime-recipe/accept/minimal.json +3 -0
  31. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  32. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +3 -0
  33. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +3 -0
  34. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +3 -0
  35. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +3 -0
  36. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +3 -0
  37. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +3 -0
  38. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +3 -0
  39. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +3 -0
  40. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +3 -0
  41. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +3 -0
  42. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +3 -0
  43. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +3 -0
  44. package/contracts/release-ledger.json +1767 -0
  45. package/contracts/runtime-recipe.campaigns-os-node-v1.json +3 -0
  46. package/contracts/supported-surface.json +10 -10
  47. package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
  48. package/docs/access-model.md +20 -0
  49. package/docs/brand-theme-bridge.md +62 -3
  50. package/docs/build-packet.md +456 -36
  51. package/docs/campaign-build-brief.md +16 -0
  52. package/docs/campaign-standardization-report.md +4 -4
  53. package/docs/campaigns-os-build-flow.md +112 -2
  54. package/docs/campaignspec-authoring-examples.md +141 -0
  55. package/docs/design-source-package.md +64 -8
  56. package/docs/effects.md +17 -9
  57. package/docs/entry-points.md +188 -0
  58. package/docs/local-setup.md +1 -1
  59. package/docs/orientation-contract-reference.md +1 -1
  60. package/docs/polish-evidence.md +42 -2
  61. package/docs/progress-snapshots.md +7 -4
  62. package/docs/qa-and-test-orders.md +197 -3
  63. package/docs/quickstart.md +438 -0
  64. package/docs/readback.md +154 -6
  65. package/docs/runtime-readiness.md +6 -3
  66. package/docs/sdk-storage-compatibility.md +4 -2
  67. package/docs/skills-revision.md +10 -10
  68. package/docs/source-adapters.md +213 -0
  69. package/package.json +10 -3
  70. package/schemas/campaign-design-source-package.v0.schema.json +8 -0
  71. package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
  72. package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
  73. package/schemas/campaign-spec.v4.schema.json +19 -3
  74. package/schemas/campaign-spec.v5.schema.json +19 -3
  75. package/schemas/campaigns-os-readback.v2.schema.json +238 -1
  76. package/schemas/source-html-manifest.v0.schema.json +3 -0
  77. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  78. package/skills/campaign-readback-classification/SKILL.md +3 -3
  79. package/skills/campaign-run-evidence/SKILL.md +3 -3
  80. package/skills/contribution-intake/SKILL.md +3 -3
  81. package/skills/next-campaigns-build/SKILL.md +5 -4
  82. package/skills/next-campaigns-os/SKILL.md +7 -4
  83. package/skills/next-campaigns-os-setup/SKILL.md +5 -3
  84. package/skills/next-campaigns-polish/SKILL.md +8 -10
  85. package/skills/next-campaigns-qa/SKILL.md +7 -4
  86. package/skills/next-campaigns-sdk-bump/SKILL.md +151 -25
  87. package/skills.json +11 -11
  88. package/src/brand-theme.mjs +415 -9
  89. package/src/brief-summary.mjs +6 -2
  90. package/src/browser-launch.mjs +2 -1
  91. package/src/build-brief.mjs +13 -3
  92. package/src/built-site-scope.mjs +2 -2
  93. package/src/campaign-workspace.mjs +288 -15
  94. package/src/change-scope.mjs +284 -0
  95. package/src/cli.mjs +358 -97
  96. package/src/commercial-journey.mjs +16 -3
  97. package/src/commercial-parity.mjs +15 -7
  98. package/src/content-residue.mjs +4 -2
  99. package/src/design-file-reference.mjs +93 -0
  100. package/src/design-source-package.mjs +91 -1
  101. package/src/design-source-publication.mjs +9 -6
  102. package/src/directory-lock.mjs +73 -19
  103. package/src/doctor/checks.mjs +87 -81
  104. package/src/doctor/ci.mjs +177 -0
  105. package/src/doctor/inspect.mjs +41 -6
  106. package/src/doctor/next-step.mjs +7 -0
  107. package/src/doctor/source-provenance.mjs +74 -0
  108. package/src/doctor-sidecar.mjs +3 -2
  109. package/src/evidence-debt.mjs +159 -0
  110. package/src/finding-cause.mjs +12 -11
  111. package/src/input-refresh.mjs +339 -32
  112. package/src/invocation.mjs +7 -1
  113. package/src/live-campaign-refs.mjs +7 -2
  114. package/src/local-preview-policy.mjs +35 -2
  115. package/src/page-kit-analytics-vendors.mjs +132 -0
  116. package/src/parity/compare.mjs +316 -0
  117. package/src/parity/design-access.mjs +54 -0
  118. package/src/parity/element-parity-record.mjs +569 -0
  119. package/src/parity/probe.mjs +205 -0
  120. package/src/parity/snapshot.mjs +110 -0
  121. package/src/png-header.mjs +87 -0
  122. package/src/polish-browser.mjs +189 -10
  123. package/src/polish-captures-record.mjs +52 -0
  124. package/src/polish-deadline.mjs +14 -0
  125. package/src/polish-gate.mjs +25 -6
  126. package/src/polish-node.mjs +81 -7
  127. package/src/polish-readability.mjs +221 -9
  128. package/src/polish-visual-fixtures.mjs +74 -0
  129. package/src/polish-visual.mjs +613 -0
  130. package/src/progress-node.mjs +3 -2
  131. package/src/qa-analytics-correctness.mjs +14 -2
  132. package/src/qa-analytics-parity.mjs +111 -6
  133. package/src/qa-browser.mjs +40 -4
  134. package/src/qa-commercial-parity.mjs +50 -3
  135. package/src/qa-node.mjs +235 -22
  136. package/src/qa-publish.mjs +9 -8
  137. package/src/qa-sidecar.mjs +16 -4
  138. package/src/qa-vendor-beacons.mjs +232 -0
  139. package/src/qa-vendor-endpoints.mjs +306 -0
  140. package/src/qc-accept.mjs +176 -6
  141. package/src/qc-check-registry.mjs +42 -2
  142. package/src/qc-results.mjs +477 -42
  143. package/src/readback.mjs +113 -21
  144. package/src/route-identity.mjs +10 -0
  145. package/src/run-record.mjs +18 -10
  146. package/src/runtime-state-ignore.mjs +6 -2
  147. package/src/sdk-attribute-index.mjs +11 -11
  148. package/src/sdk-markup.mjs +77 -2
  149. package/src/sdk-storage-compatibility.mjs +115 -6
  150. package/src/source-html-intake.mjs +15 -1
  151. package/src/source-html-manifest.mjs +7 -0
  152. package/src/source-html-server.mjs +129 -0
  153. package/src/spec-derive-campaign.mjs +216 -0
  154. package/src/spec-derive.mjs +473 -53
  155. package/src/spec-identity.mjs +14 -0
  156. package/src/stage-ledger.mjs +40 -1
  157. package/src/stage-record.mjs +100 -17
  158. package/src/target-lock.mjs +11 -1
  159. package/src/theme-gate.mjs +2 -2
  160. package/src/visual-diff.mjs +296 -0
  161. package/src/visual-reference.mjs +126 -0
@@ -4,6 +4,22 @@ The Campaign Build Brief is the merchandising and design-presentation truth for
4
4
 
5
5
  CampaignSpec remains the operational truth: packages, offers, shipping, routing, SDK hints, payment/runtime values, and API identity. The Build Brief answers business-owned presentation questions that agents should not infer silently: page authority, palette and CTA treatment, product media rules, pricing display, promo language, payment/trust surfaces, canonical names, residue policy, and QA expectations.
6
6
 
7
+ ## Design Authority
8
+
9
+ For a page built from an HTML export of a named design file, the drafted brief
10
+ records `design_authority.<page>.source: design_file`, with `reference` set to
11
+ the design file link, `frames` set to the declared frame links, and
12
+ `derived_artifact` naming the export (`provided_design_export` and its HTML
13
+ path). The summary reads "follow the design file, built from its HTML export".
14
+ A Figma spec `design_source` supplies this lineage only with
15
+ figma-sections-export provenance that passes doctor's
16
+ `source_html.producer_provenance` check without a waiver; a manifest
17
+ `design_file` is its own declaration.
18
+ A value you state in the brief file is kept as you wrote it. This records
19
+ authority; it does not read the design file or compare elements. Doctor's
20
+ `design.access` reports that access as unexercised, with guidance to continue or
21
+ compare the export and file by hand.
22
+
7
23
  ## Artifact Locations
8
24
 
9
25
  Campaigns OS accepts YAML or JSON:
@@ -81,10 +81,10 @@ field before attributing the file.
81
81
  `--template-family`), `--slug`, `--sdk-support-policy`, `--field-contract`,
82
82
  `--no-doctor`, `--json`, and the two flags every command accepts,
83
83
  `--run-id` and `--lifecycle-journal`. Any other flag is refused before the
84
- scan starts, with the known list in the message — including the
85
- `--flag=value` spelling, which the parser would otherwise store as an unknown
86
- key (`--no-doctor=maybe` used to run the doctor anyway). Values follow the
87
- flag as the next argument.
84
+ scan starts, with the known list in the message. Values follow the flag as
85
+ the next argument: like every Campaigns OS command, standardize refuses the
86
+ `--flag=value` spelling before the scan starts (`--no-doctor=maybe` used to
87
+ run the doctor anyway).
88
88
 
89
89
  ### Exit codes
90
90
 
@@ -17,6 +17,116 @@ The happy path is intentionally tight:
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.
19
19
 
20
+ ## Live-campaign changes
21
+
22
+ Keep a standing pull-request check after launch: install locked dependencies
23
+ with `npm ci`, build the campaign site with
24
+ `CPK_ENV=development npx --no-install campaign-build` (the check does not read the build summary),
25
+ then run `npx --no-install campaigns-os doctor --packet <packet> --ci --no-live-refs`.
26
+ Run these from the Page Kit folder containing the pinned tools and committed
27
+ packet, local CampaignSpec, Build Context and Assembly Report. The site build
28
+ must produce `_site/<public_route_slug>/`; `campaigns-os build` prepares inputs
29
+ and does not build the site. Do not record a new build merely to clear the check.
30
+ Match the Assembly Report's build environment: the documented local-proof flow
31
+ uses `development`; set `CPK_ENV=production` when the recorded evidence reviewed
32
+ a production render instead. Environment differences also change fingerprints.
33
+
34
+ | CI status | Exit code | Meaning |
35
+ |---|---|---|
36
+ | `current` | `0` | Polish and QA both bind to the build on disk and neither is lifecycle-required, or the only owed stage is advisory Polish for a page-only change. |
37
+ | `owed` | `3` | Polish or QA binds to another build or is lifecycle-required; evidence is owed. For a page-only change only QA on the changed pages counts. |
38
+ | `unknown` | `4` | Output or assembly fingerprints are missing, a stage fingerprint is missing without a lifecycle requirement, output is unreadable, or inspection throws; debt cannot be determined. |
39
+ | `usage` | `2` | The packet is missing, unreadable or invalid JSON. |
40
+
41
+ Any owed stage returns 3; otherwise an unknown stage returns 4. Normal
42
+ doctor errors remain visible but do not determine the CI exit code. Without
43
+ `--ci`, doctor retains its usual inspection output and 0/2 result. `--json`
44
+ adds a `ci` object containing `status`, `exit_code`, `polish`, `qa`, and
45
+ `changes` when assessment completes. Without `--json`, every CI outcome prints
46
+ an escaped Markdown report, including a "could not evaluate" report for packet
47
+ usage errors (exit 2) or inspection failures (exit 4). Page changes are compared
48
+ with the retained assembly manifest, so the page list may be empty if that
49
+ manifest was already updated; old Polish/QA bindings still show debt.
50
+ Without a usable manifest, page changes are unknown, but matching stage
51
+ bindings remain current unless lifecycle-required, and differing bindings remain owed.
52
+ The check reads recorded fingerprints, not machine-local capture files.
53
+ Readback's `evidence_debt` uses the same assessment, showing an agent this debt
54
+ before it edits; its separate time-based staleness assessment is not the CI signal.
55
+
56
+ ### What a change owes
57
+
58
+ Each owed stage carries `change`: what changed since the build that stage
59
+ last reviewed. It is read from the rebuilds `record build` keeps
60
+ (`stages.assembly.build_changes`) plus the drift on disk, so the changed pages
61
+ stay listed after `record build`. A change is one of two kinds:
62
+
63
+ - **Page-only.** Only the HTML of some pages changed (copy, image references,
64
+ inline styling), and none of them is a checkout page or changed its commerce markup:
65
+ `data-next-*`, `os-checkout-*` and other package, offer, shipping or upsell
66
+ attributes, inline event handlers (`onclick` and the like), `next-*` meta
67
+ tags (the upsell accept and decline routes among them), scripts, link
68
+ targets or form actions. A checkout page is one whose type is checkout,
69
+ checkout step or select, or one that carries checkout or payment markup
70
+ (`data-next-checkout*`, `data-next-payment*`, `data-next-express-checkout*`,
71
+ `os-checkout*`). The campaign's own entry in
72
+ `_data/campaigns.json` (SDK pin, analytics ids, store fields) is unchanged.
73
+ It owes QA on the changed pages only: `qa.scope` is `pages` and
74
+ `qa.owed_pages` lists them. Polish is advisory (`polish.advisory: true`): it
75
+ is reported, and `next`, the CI check and the QA gate do not require it.
76
+ - **Commerce-affecting.** Anything else: a checkout page, commerce markup or
77
+ the config entry changed, a page was added or removed (a route change), or a
78
+ built file other than a page's HTML changed (a script, stylesheet or image
79
+ file cannot be mapped to one page). It owes the full Polish and QA, as
80
+ does a change that cannot be read (a record made before `record build`
81
+ kept a commerce signature, or a rebuild history that no longer reaches the
82
+ reviewed build). A Polish or QA requirement a brief or spec change made is
83
+ always full.
84
+
85
+ A page-only change extends only a review that completed. Polish is advisory
86
+ only when the last Polish completed or was skipped; a blocked Polish owes the
87
+ full stage. QA is owed on the changed pages only when the last QA completed,
88
+ or when it was a page-scoped run that did not complete, in which case the
89
+ pages are counted from the full QA that run extended, so its unpaid pages stay
90
+ owed. A blocked or pending full QA owes a full run.
91
+
92
+ The PR comment opens with the kind it found and what that owes, for example
93
+ "Change found: page-only. It owes QA on `upsell1`; Polish is advisory." A
94
+ commerce-affecting change names its reasons.
95
+
96
+ Copy the [workflow example](https://github.com/NextCommerceCo/campaigns-os/blob/main/docs/examples/campaign-repo-evidence-check.yml) into the
97
+ campaign repository and adjust the packet path. It posts or updates one PR
98
+ comment using the fixed `<!-- campaign-evidence-check -->` marker. Evidence
99
+ reports use stdout only; stderr stays in the job log. If the command produces
100
+ no stdout, the comment points to the job log with a fixed fallback message.
101
+ If install or build fails before doctor runs, the comment is updated with
102
+ "Campaign build failed; evidence not evaluated. See the job log."
103
+ Evidence findings are **report-only by default**. Opt in to failing on a nonzero doctor
104
+ exit by setting the repository Actions variable
105
+ `CAMPAIGNS_OS_EVIDENCE_CHECK` to `fail`. Install/build failures still fail normally.
106
+ Fork pull requests may lack comment-write permission; comment failures are
107
+ nonfatal and the report remains in the job log. The example uses `pull_request`
108
+ and does not run untrusted code with `pull_request_target` privileges.
109
+
110
+ To repay a page-only change, rebuild, run `record build`, then run
111
+ `qa run --packet <packet> --page <page_id>` against that build, repeating
112
+ `--page` for each changed page the report names. A page-scoped run checks only
113
+ those pages and places no test orders; the QA stage it records keeps the
114
+ purchase proof of the full run it extends. It refuses, before checking
115
+ anything, when the output on disk is not the recorded build, when the change
116
+ is commerce-affecting, when a changed page is left out, when the QA it would
117
+ extend did not complete, or when QA's inputs (the brief's `qa_policy` and the
118
+ CampaignSpec) changed or cannot be confirmed current. Running Polish for a
119
+ page-only change is optional.
120
+
121
+ To repay fingerprint-drift debt for any other change, rebuild and record the output, re-run `polish capture` against
122
+ that served build, record Polish with `record polish --evidence <evidence.json>`,
123
+ then run `qa run` to record QA for the current build. For lifecycle-required
124
+ stages with matching build bindings, re-run the required stages against the
125
+ existing build: `polish capture` and `record polish` when Polish is owed, and
126
+ `qa run` when QA is owed. The CI report names each owed stage's cause and
127
+ selects the remedy accordingly. The workflow only inspects;
128
+ it does not renew evidence or deploy.
129
+
20
130
  ## Partial Builds
21
131
 
22
132
  Partial builds are valid campaign work. A pass may build only new presell pages,
@@ -83,11 +193,11 @@ the assembly report.
83
193
  - Landing and presell pages should preserve prepared HTML when it is a real standalone design. Use page-kit passthrough structure, inject the SDK/config requirements, and repoint CTAs into the CampaignSpec flow.
84
194
  - **Pre-checkout pages must ship the same SDK bootstrap as the checkout layout.** Presell and landing pages are SDK `page_type: product`; they need `config.js` (before the loader), the `campaign-cart@v{sdk_version}/dist/loader.js` module script, and the `next-funnel` + `next-page-type` meta tags — not just inert `data-next-*` attributes. Without the loader the SDK silently no-ops: `data-next-hide` conditional visibility (`param.banner` / `param.seen`), `utmTransfer` UTM/query carry-through to checkout (top-of-funnel ad attribution), and SDK analytics never fire. Treat `param.banner` / `param.seen` visibility and `utmTransfer` as standard pre-checkout wiring, not per-campaign discoveries. Doctor enforces this with `built_output.pre_checkout_sdk_bootstrap`.
85
195
  - **Every page names the same campaign.** A page borrowed from another funnel (a copied upsell or receipt) must not keep the other campaign's `next-api-key` / `config.js` API key, `next-funnel` meta, or `setAttribution({ funnel })` call; the SDK reads these per page and reconciles nothing, so the order lands on or attributes to the wrong campaign with no visible error. Doctor blocks this, unwaivably, with `built_output.campaign_identity` (one error per drift, naming both files and both values).
86
- - **SDK markup must do what it says.** `data-next-checkout` goes on the `<form>`; `data-next-checkout-field` values are the SDK's fixed names (`fname`, `lname`, `postal` — never `firstName`, `lastName`, `zip`); an `add-to-cart` button linked by `data-next-selector-id` needs a selector with that id and that selector in `select` mode (swap mode plus the button writes the cart twice); every `data-next-upsell-action` sits inside its `data-next-upsell` container, on any page type; one default-selected card per selector; single-brace tokens inside SDK templates. Doctor enforces the first five as blockers and the last two as warnings under `built_output.sdk_markup` (codes `SWAP_WITH_ADD_TO_CART`, `CHECKOUT_NOT_FORM`, `WRONG_FIELD_NAME`, `MISSING_SELECTOR_ID_MATCH`, `ORPHANED_UPSELL_ACTION`, `DOUBLE_SELECTED`, `TEMPLATE_DOUBLE_BRACE`).
196
+ - **SDK markup must do what it says.** `data-next-checkout` goes on the `<form>`; `data-next-checkout-field` values are the SDK's fixed names (`fname`, `lname`, `postal` — never `firstName`, `lastName`, `zip`); an `add-to-cart` button linked by `data-next-selector-id` needs a selector with that id and that selector in `select` mode (swap mode plus the button writes the cart twice); every `data-next-upsell-action` sits inside its `data-next-upsell` container, on any page type; one default-selected card per selector; single-brace tokens inside SDK templates; the page type in `<meta name="next-page-type">` (`upsell` on every upsell offer page), never only in a `data-next-page-type` attribute. Doctor enforces the first five as blockers and the last three as warnings under `built_output.sdk_markup` (codes `SWAP_WITH_ADD_TO_CART`, `CHECKOUT_NOT_FORM`, `WRONG_FIELD_NAME`, `MISSING_SELECTOR_ID_MATCH`, `ORPHANED_UPSELL_ACTION`, `DOUBLE_SELECTED`, `TEMPLATE_DOUBLE_BRACE`, `PAGE_TYPE_ATTRIBUTE`, `UPSELL_PAGE_TYPE_MISSING`).
87
197
  - **Campaign scripts must parse.** A hand-edited script with a stray or missing bracket throws a `SyntaxError` on every page that loads it, and nothing in it runs. Doctor parses every campaign-owned `.js` file a built page loads by a local `<script src>` and blocks, unwaivably, with `built_output.script_syntax.parse_failure`, naming the file, line and column.
88
198
  - Checkout, upsell, downsell, and receipt pages should preserve starter-template SDK contracts while keeping the campaign/source visual language. Treat starter templates as the reference for required `data-next-*` controls and wiring, not as a mandate to carry their visual chrome into the final campaign.
89
199
  - If source HTML declares SDK-owned zones such as `data-commerce-zone="checkout-form"` or `data-commerce-zone="order-summary"`, keep the selected starter-template family's SDK wiring for that zone (the checkout form, bound fields, payment submit and cart summary). The wrapper and page composition around it stay source-owned; do not add family class names to the campaign's own layout to satisfy QA.
90
- - If `context.theme` names a generated `brand-theme.css`, copy it into campaign assets and load it after `next-core.css` on checkout, upsell, downsell, and receipt pages. Generated brand-theme v0 is root-variable-only; do not use it as permission to edit SDK-owned selectors or runtime structure.
200
+ - If `context.theme` names a generated `brand-theme.css`, copy it into campaign assets and load it after `next-core.css` on checkout, upsell, downsell, and receipt pages. When `context.theme.generated.fonts_css_path` names a `brand-fonts.css`, copy it beside the brand layer and load it on the same pages, so headings and body copy use the design's faces (`record theme` refuses a styled page that skips it). Generated brand-theme v0 is root-variable-only; do not use it as permission to edit SDK-owned selectors or runtime structure.
91
201
  - Buy-more-save-more selectors should use selected quantity plus Offer-aware price displays. Do not swap in stale package-per-tier IDs unless the CampaignSpec explicitly represents an older campaign that still owns separate packages for each option.
92
202
  - SDK routing meta tags should be emitted as campaign-root paths, for example `/campaign-slug/upsell/`, even when the CampaignSpec source value is slug-relative like `upsell/`.
93
203
  - One-time prepurchase/order-bump packages outside the main bundle should default to fixed quantity and fixed line total display unless the spec explicitly requires syncing quantity with the main bundle.
@@ -0,0 +1,141 @@
1
+ # CampaignSpec Authoring Examples
2
+
3
+ Use these examples when authoring or generating common Campaign Map shapes. They
4
+ are public-package guidance: the goal is clearer exported CampaignSpec fields,
5
+ not a private merchant workflow.
6
+
7
+ ## Hero Product + Order Bump + Upsell + Receipt
8
+
9
+ Reference fixture: [single-step checkout, upsell and receipt](https://github.com/NextCommerceCo/campaigns-os/blob/main/contracts/fixtures/campaign-specs/shop-single-step-upsell-receipt.json).
10
+ Fixtures are repository test data and are not included in the installed package.
11
+
12
+ For CampaignSpec 5.0 multi-step shapes, compare the [three-step checkout](https://github.com/NextCommerceCo/campaigns-os/blob/main/contracts/fixtures/campaign-specs/shop-three-step-dynamic-shipping.json)
13
+ and [select-to-checkout](https://github.com/NextCommerceCo/campaigns-os/blob/main/contracts/fixtures/campaign-specs/olympus-mv-two-step-configurable.json) examples.
14
+ Author new specs against the shipped [v5 schema](../schemas/campaign-spec.v5.schema.json).
15
+
16
+ Canonical page shape:
17
+
18
+ ```text
19
+ checkout -> upsell -> receipt
20
+ ```
21
+
22
+ Authoring guidance:
23
+
24
+ - Checkout page:
25
+ - `type: "checkout"` with `sdk_hints.sdk_page_type: "checkout"`
26
+ - `page_url: "/checkout/"` — `page_url` is the route field.
27
+ - `packages[]` entries use `ref_id` + `qty` (the real export key is `qty`,
28
+ not `quantity`). A plain entry is the main package; add-ons carry the
29
+ boolean role flags `is_order_bump: true` / `is_upsell: true` — there is
30
+ no `role` field in exports. The flags mean "add-on" on `checkout`,
31
+ `checkout_step` and `select` pages only. On an upsell or downsell page
32
+ they mark nothing (see the upsell page below).
33
+ - `sdk_hints.template_family` names the intended starter family only as a
34
+ hint; the Build Packet still locks the family.
35
+ - `sdk_hints.meta_tags.next-success-url` points to the next runtime route.
36
+ - `success_url` holds the next PAGE ID (never a URL, despite the name).
37
+ - A checkout may equally declare its forward step as `next_page`; twelve
38
+ pages across ten certified fixtures do. Forward links resolve from
39
+ whichever routing field a page declares — `on_accept`, then `success_url`,
40
+ then `next_page` — so an unconventional journey is wired rather than
41
+ dropped.
42
+ - `next_page` is the generic "wherever this page goes next" and is honoured on
43
+ every page type. The other two forward fields carry a page-shaped meaning
44
+ and are honoured only where that meaning exists:
45
+ - `success_url` means "after payment succeeds" — only on `type: "checkout"`.
46
+ - `on_accept` means "after accepting the offer on this page" — only on
47
+ `type: "upsell"` or `"downsell"`.
48
+
49
+ Declared anywhere else they are inert, `next_page` wins, and the
50
+ `RouteFieldIgnoredForPageType` warning says which field was skipped and
51
+ where the shopper actually goes. A `select` page is the first step of a
52
+ two-step checkout and a `checkout_step` is a non-final page of a split
53
+ checkout: neither takes payment nor presents an accept/decline offer, so a
54
+ copy-pasted `success_url` or `on_accept` there does not route.
55
+ - Multi-step checkout (CampaignSpec 5.0): `checkout` is the page that takes
56
+ payment, and it forwards to an upsell, downsell or thank-you page. A page
57
+ with a checkout form that collects details and moves on without placing an
58
+ order is `type: "checkout_step"` and routes with `next_page`; a bundle
59
+ picker before checkout is `type: "select"`. Every path from an entry page
60
+ to an upsell passes exactly one `checkout`; a campaign may hold several, one
61
+ per split-test path. A 4.2/4.3 spec that chained `checkout → checkout` is
62
+ read as `checkout_step` pages ahead of the last one.
63
+ See the [v5 schema](../schemas/campaign-spec.v5.schema.json) for the
64
+ authoring fields; runtime routing is projected by Campaigns OS.
65
+
66
+ - Upsell page:
67
+ - `type: "upsell"` or `type: "downsell"`, with
68
+ `sdk_hints.sdk_page_type: "upsell"`
69
+ - `page_url` is a Page Kit route, not a source filename.
70
+ - `packages[]` or `offers[]` names the post-purchase offer refs. Write each
71
+ package as a plain `{ ref_id, qty }` entry. On an upsell or downsell page
72
+ every package row is the page's offer and never an order bump, so
73
+ `is_upsell: true` on that row is accepted and changes nothing about
74
+ its role. Offers are identified by `ref_id` into the root `offers[]` catalog
75
+ (`package_ref_id` is not part of the contract).
76
+ - `on_accept` and `on_decline` hold the next PAGE IDs; route paths live
77
+ in `page_url` / derived `resolved_routing`.
78
+
79
+ - Receipt page:
80
+ - `type: "thankyou"` — the canonical terminal page type. The SDK-layer
81
+ `receipt` projection is expressed as `sdk_hints.sdk_page_type: "receipt"`
82
+ (and `meta_tags["next-page-type"]`), never as `page.type`.
83
+ - `page_url: "/receipt/"`
84
+ - Receipt summary/frontmatter hints preserve SDK-owned order item templates.
85
+
86
+ Generator checklist:
87
+
88
+ - Always emit stable `page.id` values; page IDs are the join key for
89
+ `source_html.pages[]`.
90
+ - Prefer Page Kit public routes (`checkout/`, `upsell/`, `receipt/`) over
91
+ filenames (`checkout.html`).
92
+ - Keep package, shipping, voucher, and offer refs in CampaignSpec/API fields,
93
+ not in source HTML.
94
+ - Put template-family preference in `campaign.preferred_template_family` when
95
+ useful, but require the Build Packet/template lock before commerce wiring.
96
+ - Keep checkout/upsell/downsell/receipt runtime surfaces template-clone-first;
97
+ source HTML can own visual composition and copy, not payment/order mutation.
98
+
99
+ ## Campaign Currencies
100
+
101
+ - `campaign.currency` is the default currency: a three-letter upper-case
102
+ ISO 4217 code such as `"USD"`. A lower-case code (`"usd"`) or a symbol
103
+ (`"US$"`) is an error, because Campaign Cart can fall back to a cached USD
104
+ campaign and price the page in the wrong currency.
105
+ - `campaign.available_currencies` lists every currency the campaign sells in,
106
+ the default included, as code strings: `["USD", "EUR", "GBP"]`. The
107
+ Campaigns API returns this list as `{ code, label }` objects; Map Builder
108
+ writes the codes, and a hand-written spec should too.
109
+ - The Campaigns App API also has `additional_currencies`, which is the same
110
+ list without the default. It is not a CampaignSpec field: put every
111
+ currency in `available_currencies`.
112
+ - When `available_currencies` is non-empty and does not contain
113
+ `campaign.currency`, the validator warns. An empty list states nothing and
114
+ is not checked.
115
+
116
+ These checks are the `CampaignCurrencies` validator rule, which
117
+ `campaigns-os doctor` runs with the rest of the spec rules.
118
+
119
+ ## Order Bump Notes
120
+
121
+ An order bump is still a checkout package/offer surface. It should be encoded as
122
+ a checkout page package with `is_order_bump: true`, a `qty`, and its source
123
+ `ref_id`.
124
+ When the selected starter family does not support a bump, the build should
125
+ remove the bump surface and record the decision in the Assembly Report instead
126
+ of leaving demo bump refs in copied frontmatter.
127
+
128
+ ## Multi-Upsell Depth
129
+
130
+ For more than one post-purchase page, keep each upsell/downsell page explicit in
131
+ `funnels[].pages[]` and wire `on_accept` / `on_decline` to the next page. QA
132
+ then derives typed-card proof coverage from topology. `common` runs every
133
+ actual terminal path when they fit under the flood cap; above it, it runs
134
+ checkout, first-offer accept and decline, and a deduplicated shortest real
135
+ receipt path, then adds the shortest path that clicks the decline on each offer
136
+ or downsell page not yet declined, up to the cap. Explicit paths support targeted
137
+ repair, while `full` walks every actual terminal path in the selected checkout
138
+ topology. Cycles, missing routes, and reachable nonterminals block exhaustive
139
+ proof before browser launch. The accidental-flood cap remains `6`; when the
140
+ planned count exceeds it, the command names the exact explicit
141
+ `--max-test-orders` raise.
@@ -32,6 +32,48 @@ Unknown top-level fields are rejected.
32
32
 
33
33
  ## Contributions and coverage
34
34
 
35
+ **A design file behind an HTML export.** Name it on the page's source-html
36
+ manifest entry as `design_file: {type, file_url, breakpoints}`. `breakpoints`
37
+ holds optional desktop, tablet and mobile frame links. Intake records a separate,
38
+ non-renderable contribution (`figma_frames` with native trust for Figma, otherwise
39
+ `other` with opaque trust), with `reference_only` coverage. The page's
40
+ `html_funnel` mapping links to it through `derived_from`. Pages sharing a file
41
+ share one contribution. A CampaignSpec page's Figma `design_source` records lineage
42
+ only with figma-sections-export provenance that passes doctor's
43
+ `source_html.producer_provenance` check without a waiver; then it wins when both
44
+ channels are present. Otherwise, a spec pointer alone adds no contribution or
45
+ lineage at intake, and no `design.access` row unless the package already records
46
+ a spec contribution. A Figma `design_source` that names a different file or
47
+ frames from the manifest's `design_file` is reported as a conflict for review
48
+ even when it records no lineage, as is a file key that differs from the
49
+ exporter's. A file shared by several pages keeps the manifest's provenance if
50
+ any page declared it in the manifest.
51
+
52
+ Malformed `design_file` values remain non-fatal manifest warnings; the schema
53
+ accepts them so validation agrees with intake. File URL schemes are
54
+ case-insensitive. Explicit tool types are lowercased for identity. For a type
55
+ other than `figma` or `other`, the design-file source ref's optional
56
+ `design_file_type` string preserves the type doctor needs to reconstruct the
57
+ recorded identity even after a declaration edit. This new field is material;
58
+ source-reference `notes` remain nonmaterial, including on `design_file` refs.
59
+
60
+ Recording a file adds no readiness requirement, Source TODO or blocker. A
61
+ manifest edit, including `design_file`, follows the existing stale-package rule:
62
+ rerun intake with `--force`. A later spec declaration leaves the package current;
63
+ when eligible under the Figma provenance rule above, doctor reports the
64
+ unrecorded file with the same recovery guidance.
65
+ `design.access` reports `unexercised` / `design_access_missing` when the recorded
66
+ file agrees with the declaration. For a recorded spec contribution, doctor
67
+ compares the current spec declaration even if today's manifest or provenance is
68
+ unavailable. This version cannot read design files. This
69
+ records authority only; Polish element parity compares the built page with its
70
+ source HTML, not the design file (see [Polish evidence](polish-evidence.md)).
71
+ A changed design file is listed for review; changing only breakpoint frames on
72
+ that file does not change its identity. If the package records a file but the
73
+ current declaration is absent, `design.access` reports `review` /
74
+ `design_reference_missing`: restore the declaration, or rerun intake with
75
+ `--force` to drop the recorded file.
76
+
35
77
  Each `contributions[]` record identifies one source contribution and records:
36
78
 
37
79
  - `id`, `kind`, `trust`, and whether it is `renderable`;
@@ -275,10 +317,10 @@ The projected fields are explicit:
275
317
  | Contribution core | `id`, `kind`, `trust`, `renderable`. |
276
318
  | Provenance | `source_type`, `adapter`, `producer`, `generator`, `generator_version`, `source_root`, `manifest_schema_version`, `manifest_path`, `manifest_sha256`, `producer_material_fingerprint`, `asset_crawl_schema_version`. |
277
319
  | Presentation intent | `summary`, `composition`, `content_hierarchy`, `imagery`, `copy`, `brand`, `responsive_behavior`. |
278
- | Source reference | `id`, `kind`, `path`, `url`, `sha256`, `role`, `media_type`. |
320
+ | Source reference | `id`, `kind`, `path`, `url`, `sha256`, `role`, `media_type`, optional `design_file_type` (only on `design_file` refs). |
279
321
  | Visual reference | `id`, `kind`, `viewport`, `availability`, `url`, `path`, `sha256`, `width`, `height`, `device_profile`, `scale_factor`, `browser`, `source_ref_id`, `unavailable_reason`. |
280
322
  | Template Reference | `id`, `family`, `version`, `contract_path`, `artifact_path`, `sha256`, `standard_viewport_refs` using the visual projection above. |
281
- | Coverage mapping | `id`, `surface_id`, `coverage_role`, `confidence`, `source_refs`, `screenshot_refs`, `reference_refs`, `template_reference_id`. |
323
+ | Coverage mapping | `id`, `surface_id`, `coverage_role`, `confidence`, `source_refs`, `screenshot_refs`, `reference_refs`, `template_reference_id`, optional `derived_from`. |
282
324
  | Source Gap | `id`, `kind`, `scope`, `applies_to`, `reason`, `status`, `attributed_by`, `attributed_at`, `evidence_refs`. |
283
325
  | Source TODO | `id`, `kind`, `scope`, `applies_to`, `description`, `status`, `owner`, `required_viewports`, `source_ref_ids`. |
284
326
  | Approved waiver | `id`, `scope`, `applies_to`, `reason`, `status`, `waived_by`, `waived_at`, `expires_at`, `review_condition`, `evidence_refs`. |
@@ -397,12 +439,24 @@ left by a process that died is recovered automatically, and a lock directory
397
439
  without an owner record (only an older release leaves one) is never taken
398
440
  over: the command refuses it after about a second and names it. Confirm no
399
441
  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.
442
+ at the lock path (a file, a symbolic link, a directory the command cannot read,
443
+ or a directory with no owner record that holds other entries) is not a lock:
444
+ once the wait ends the command names what is there and never removes it. The
445
+ holder of the lock removes staging, recovery, released and abandoned
446
+ directories beside it whose recorded owner process no longer exists.
447
+
448
+ Run one Campaigns OS release against a target at a time. Each owner record
449
+ carries a `lock_format` number (the lock layout the writing release uses).
450
+ Releases before 1.43.2 took the lock in two steps, creating the directory and
451
+ then writing its owner, and a later release can replace that directory in the
452
+ gap between them, so both runs enter. A command that finds the lock held by a
453
+ live process whose owner record has no `lock_format`, or a different one,
454
+ refuses at once and names that process instead of waiting for it. Every
455
+ release before `lock_format` existed writes no number, so a holder from one of
456
+ those is refused too, even though only releases before 1.43.2 can race. The
457
+ check only sees a release that already holds the lock: two releases that start
458
+ at the same instant can still meet in that gap. A dead holder from any release
459
+ is recovered as usual. A waiver's `--dry-run` preview takes no lock.
406
460
 
407
461
  Before writing any output, `prepare-build` also requires distinct paths for the
408
462
  Build Packet, Build Context, Assembly Report, Doctor output, normalized Build
@@ -458,6 +512,8 @@ only when filename matching was ambiguous, and is `null` otherwise.)
458
512
 
459
513
  ### The record shape
460
514
 
515
+ Polish now reads these files. Record `scale_factor` (the device pixel ratio the screenshot was taken at) and keep `width`/`height` equal to the image's pixel size: a screenshot is compared with the built page only when its width divided by `scale_factor` equals the capture width, and a screenshot without `scale_factor` is not compared. Link it to its source page (`source_ref_id`, filled in for you) and record `captured_at`, or it is listed for review.
516
+
461
517
  One manifest page entry with its proof, inside the envelope the validator requires
462
518
  (the mobile record is elided):
463
519
 
package/docs/effects.md CHANGED
@@ -70,10 +70,12 @@ with one of these:
70
70
  | Token | Resolves to |
71
71
  | --- | --- |
72
72
  | `{target}` | The target the invocation names: the directory given to `--target`, or the Page Kit target repository the Build Packet points at. |
73
+ | `{runtime}` | The funnel's runtime directory under `{target}`: `{target}/.campaign-runtime` for a Build Packet that records no `assembly.runtime_dir` (every packet before per-funnel records, and the first funnel of a project), else `{target}/<assembly.runtime_dir>`, which is `.campaign-runtime/funnels/<public_route_slug>` for each later funnel of the same project. The funnel's Build Context, Assembly Report, doctor output, theme output and intake inputs live here. |
73
74
  | `{cwd}` | The working directory the invocation runs in. |
74
75
  | `{spec}` | The CampaignSpec file the Build Packet names (`spec.local_path`), which need not live inside the target repository. |
75
76
  | `{home}` | Your machine: the home directory and the config root under it (`XDG_CONFIG_HOME` when set). |
76
77
  | `{packet}` | The Build Packet the invocation names (`--packet`), wherever it lives — it need not be the copy inside the target repository. |
78
+ | `{packet-runtime}` | The same funnel runtime directory beside the Build Packet: `<packet directory>/.campaign-runtime`, or `<packet directory>/<assembly.runtime_dir>` when the packet records one. The committed QA verdict sidecar and the Run Records live here. |
77
79
  | `{lifecycle-journal}` | The command-lifecycle journal wherever it was selected for this invocation. |
78
80
  | `{proxy-base}` | The endpoint `--proxy-base` names, or the canonical NEXT endpoint when it does not. |
79
81
  | `{base-url}` | The campaign under test, as `--base-url` names it or as the packet derives it. |
@@ -81,8 +83,8 @@ with one of these:
81
83
 
82
84
  The tokens matter because effects are not all under the target. `install-skills`
83
85
  writes your **home** directory, not the campaign. `telemetry on` writes your
84
- **machine** config. `run-record` writes beside the **working directory**, not the
85
- target repo. A row that said "writes the target" would be wrong about all three.
86
+ **machine** config. `run-record` writes beside the **Build Packet**
87
+ (`{packet-runtime}`), not the target repo. A row that said "writes the target" would be wrong about all three.
86
88
 
87
89
  ### The Map Builder fetch
88
90
 
@@ -109,14 +111,14 @@ campaign. No store or Admin credential is used. The proxy's answer is an
109
111
  envelope whose `data` field holds the campaign. A failed read, an `ok: false`
110
112
  envelope, one with an `error` and no campaign, or several campaigns with no
111
113
  `campaign.ref_id` to pick one is recorded as `not_run` with its reason, never
112
- as a pass. `--no-live-refs` on `doctor` or `qa run` skips only this read and
114
+ as a pass. `--no-live-refs` on `doctor`, `qa run` or `spec derive` skips only this read and
113
115
  records `not_run` with reason `disabled`; every other declared write and send
114
116
  is unchanged. Each form has its own row, including `doctor --write
115
117
  --no-live-refs` and `qa run --no-live-refs` with each `qa run` modifier.
116
118
  `doctor --no-write --no-live-refs` has the effects of the `doctor
117
119
  --no-live-refs` row.
118
120
 
119
- Only `doctor` and `qa run` make this read. The other commands that run doctor
121
+ Only `doctor`, `qa run` and `spec derive` (see `spec derive` and the live campaign, below) make this read. The other commands that run doctor
120
122
  internally make no request and record `derived.live_campaign_refs` as
121
123
  `not_run` with reason `not_read`, with no warning: `start`, `prepare-build`
122
124
  and `build` (the intake alias for prepare-build + doctor), `theme waive`,
@@ -130,11 +132,13 @@ itself carries QA's own read).
130
132
 
131
133
  ### `record brief` and `record spec`
132
134
 
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.
135
+ **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 the funnel's runtime directory (`{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
136
 
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.
137
+ **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, and it sends nothing. It writes the Build Packet and the Design Source Package only when it binds a page change: a move of routes alone, or on a handed-off campaign (deploy or QA completed) a page added or removed; then a deploy recorded without a build fingerprint is owed again too when the set of built pages changes (a fingerprint-bound deploy is made owed by the next `record build`). A rerun after a run that stopped part way moves the Design Source Package references that run left behind. `spec derive` no longer rebinds identity; run `record spec` after it. A change to the order of active pages, or a page added or removed before handoff, still needs intake.
136
138
 
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.
139
+ Both `record` 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.
140
+
141
+ **`spec derive` and the live campaign.** Once the campaign is built (its `_site/<route>/` exists) and a public Campaigns API key resolves, `spec derive` makes the same read-only `GET {proxy-base}/api/campaign` doctor makes and refreshes the offer and package fields the spec mirrors from the Campaigns App; `--dry-run` makes it too, so the diff shows those fields. `--no-live-refs` sends nothing (its own row, tier B). `spec derive` and its `--dry-run` are tier A.
138
142
 
139
143
  ## How to read a row
140
144
 
@@ -164,8 +168,9 @@ flags that change what the invocation does to the world are listed once, in
164
168
  `vocabulary.effect_changing_flags`: `--apply`, `--browser`, `--built`, `--dry-run`,
165
169
  `--emit-packet`, `--example`, `--force`, `--from-store`, `--list`,
166
170
  `--no-live-refs`, `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
167
- `--no-write`, `--republish`, `--test-order`, `--write`, `--write-map`. Flags
168
- that only change the output shape (`--json`, `--report`) deliberately do not.
171
+ `--no-write`, `--republish`, `--test-order`, `--vendor-beacons`, `--write`,
172
+ `--write-map`. Flags that only change the output shape (`--json`, `--report`)
173
+ deliberately do not.
169
174
 
170
175
  **"The help text" is every help block the CLI prints**, not one file's.
171
176
  `campaigns-os qa` prints its own from `src/qa-node.mjs`, and while the coverage
@@ -253,6 +258,9 @@ input resolution. They append no lifecycle entry. Accepted modes remain
253
258
  `accept`, `decline`, and `both` (case-insensitive); browser `--test-order` still
254
259
  takes precedence and does not require the legacy cart. API credentials are
255
260
  still checked only inside the legacy handler and failures there are journaled.
261
+ A `qa run --vendor-beacons` value other than `live`, or `--vendor-beacons` on
262
+ a run without browser `--test-order`, is likewise an argv-only refusal before
263
+ QA input resolution.
256
264
  The nested run-record refusal scope in session closeout guards against future
257
265
  changes. No internal closeout can currently create a refusal before its
258
266
  invoking command journals.