@nextcommerce/campaigns-os 1.43.1 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/AGENTS.md +9 -2
  2. package/CHANGELOG.md +1099 -5103
  3. package/README.md +34 -13
  4. package/agents/claude/CLAUDE.md +1 -1
  5. package/agents/codex/AGENTS.md +1 -1
  6. package/agents/copilot/copilot-instructions.md +1 -1
  7. package/agents/cursor/campaigns-os.mdc +1 -1
  8. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
  9. package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
  10. package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
  11. package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
  12. package/campaign-spec/dist/types.d.ts +2 -2
  13. package/contracts/agent-relevant-change-policy.v1.json +5 -0
  14. package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
  15. package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
  16. package/contracts/effects.v1.json +1184 -121
  17. package/contracts/orientation-reason-codes.v1.json +7 -0
  18. package/contracts/release-ledger.json +2190 -5260
  19. package/contracts/supported-surface.json +7 -4
  20. package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
  21. package/docs/brand-theme-bridge.md +81 -0
  22. package/docs/build-packet.md +222 -23
  23. package/docs/campaigns-os-build-flow.md +4 -3
  24. package/docs/design-source-package.md +162 -15
  25. package/docs/effects.md +66 -12
  26. package/docs/gateway-login.md +3 -0
  27. package/docs/local-setup.md +1 -1
  28. package/docs/orientation-contract-reference.md +42 -2
  29. package/docs/polish-evidence.md +74 -0
  30. package/docs/progress-snapshots.md +10 -6
  31. package/docs/qa-and-test-orders.md +230 -20
  32. package/docs/release-ledger-authoring-guide.md +70 -8
  33. package/docs/runtime-readiness.md +1 -1
  34. package/docs/sdk-storage-compatibility.md +1 -1
  35. package/docs/skills-revision.md +10 -10
  36. package/docs/supported-surface.md +2 -2
  37. package/docs/versioning.md +4 -1
  38. package/package.json +1 -1
  39. package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
  40. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
  41. package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
  42. package/skills/campaign-readback-classification/SKILL.md +3 -3
  43. package/skills/campaign-run-evidence/SKILL.md +7 -6
  44. package/skills/contribution-intake/SKILL.md +3 -3
  45. package/skills/next-campaigns-build/SKILL.md +7 -6
  46. package/skills/next-campaigns-os/SKILL.md +7 -7
  47. package/skills/next-campaigns-os/references/session-intake.md +9 -3
  48. package/skills/next-campaigns-os-setup/SKILL.md +5 -5
  49. package/skills/next-campaigns-polish/SKILL.md +28 -9
  50. package/skills/next-campaigns-qa/SKILL.md +7 -4
  51. package/skills.json +10 -10
  52. package/src/brand-theme.mjs +320 -20
  53. package/src/build-brief.mjs +6 -4
  54. package/src/built-script-syntax.mjs +480 -0
  55. package/src/built-site-scope.mjs +16 -4
  56. package/src/campaigns-api-key.mjs +99 -0
  57. package/src/cli-helpers.mjs +118 -0
  58. package/src/cli.mjs +1530 -7580
  59. package/src/commercial-parity.mjs +48 -2
  60. package/src/design-source-package.mjs +1 -1
  61. package/src/design-source-publication.mjs +898 -0
  62. package/src/deviation.mjs +13 -1
  63. package/src/diagnostic.mjs +6 -2
  64. package/src/directory-lock.mjs +270 -0
  65. package/src/doctor/checks.mjs +4654 -0
  66. package/src/doctor/inspect.mjs +678 -0
  67. package/src/doctor/next-step.mjs +731 -0
  68. package/src/doctor/source-provenance.mjs +184 -0
  69. package/src/install-invocation.mjs +29 -0
  70. package/src/invocation.mjs +183 -0
  71. package/src/live-campaign-refs.mjs +466 -0
  72. package/src/login.mjs +2 -2
  73. package/src/page-kit-store-profile.mjs +69 -12
  74. package/src/page-kit-sync.mjs +31 -12
  75. package/src/private-template-source.mjs +1 -1
  76. package/src/progress-node.mjs +9 -36
  77. package/src/proof-policy.mjs +1 -1
  78. package/src/qa-analytics-correctness.mjs +3 -0
  79. package/src/qa-binding-evidence.mjs +76 -11
  80. package/src/qa-browser.mjs +1316 -105
  81. package/src/qa-build-scope.mjs +47 -0
  82. package/src/qa-commercial-parity.mjs +48 -5
  83. package/src/qa-node.mjs +339 -19
  84. package/src/qa-test-order-topology.mjs +148 -0
  85. package/src/sdk-markup.mjs +72 -8
  86. package/src/source-html-intake.mjs +117 -1
  87. package/src/source-html-manifest.mjs +9 -2
  88. package/src/stage-ledger.mjs +28 -0
  89. package/src/stage-record.mjs +551 -0
  90. package/src/target-lock.mjs +54 -0
  91. package/src/template-brand-contract.mjs +17 -1
  92. package/src/upsell-selector-scope.mjs +112 -2
package/docs/effects.md CHANGED
@@ -38,8 +38,10 @@ that already understands those hints needs no translation layer.
38
38
  **`readOnlyHint` counts the command-lifecycle journal.** A journal append is a
39
39
  write like any other, so every `readOnlyHint: true` row is an invocation the
40
40
  CLI exempts from lifecycle capture (the converse does not hold: `demo` and the
41
- `--no-write` forms skip the journal but still write other declared files): `help`, `readback`,
42
- `run status`, `doctor` inspection, `doctor --no-write`, `sdk storage-check`,
41
+ `--no-write` forms skip the journal but still write other declared files, and
42
+ `doctor` inspection and `doctor --no-write` skip it and write nothing but may
43
+ send the one live campaign read): `help`, `readback`,
44
+ `run status`, `doctor --no-live-refs` inspection, `sdk storage-check`,
43
45
  `tooling diagnose`, a refused invocation, `run-record --no-write`, and every
44
46
  `--dry-run` form on the commands that implement the flag. Everything else
45
47
  appends an entry when a journal is selected — an active run session,
@@ -81,6 +83,46 @@ writes your **home** directory, not the campaign. `telemetry on` writes your
81
83
  **machine** config. `run-record` writes beside the **working directory**, not the
82
84
  target repo. A row that said "writes the target" would be wrong about all three.
83
85
 
86
+ ### The Map Builder fetch
87
+
88
+ `start`, `prepare-build` and `build` take their CampaignSpec from `--spec` or
89
+ from `--map-id`. With `--map-id` (and no `--cached-spec`) the invocation sends
90
+ `{proxy-base}/api/spec/{map-id}` and writes what comes back to
91
+ `{target}/.campaign-runtime/fetched-specs/<map-id>.json`, replacing any earlier
92
+ copy of that Map; the intake then reads the spec from that file. Both effects
93
+ are on every row for those three commands. The effect test runs each row with
94
+ `--spec` under four conditions and with `--map-id` under `persisted_consent`,
95
+ the one condition whose `--proxy-base` is the loopback receiver, so the fetch
96
+ and the write are observed rather than taken on trust.
97
+
98
+ ### The live campaign read
99
+
100
+ `doctor --packet` (with or without `--write` / `--no-write`) and every `qa run`
101
+ form check each page's shipping and package refs against the live campaign.
102
+ Doctor reads when the packet's built `_site/<route>/` exists; QA reads when it
103
+ has read at least one served page. Both need a public Campaigns API key from
104
+ the packet, its local CampaignSpec or the declared campaign-key env var. The
105
+ read is one `GET {proxy-base}/api/campaign` with the key in `X-Campaign-Key`,
106
+ plus `?ref_id=<id>` when the CampaignSpec's `campaign.ref_id` names the
107
+ campaign. No store or Admin credential is used. The proxy's answer is an
108
+ envelope whose `data` field holds the campaign. A failed read, an `ok: false`
109
+ envelope, one with an `error` and no campaign, or several campaigns with no
110
+ `campaign.ref_id` to pick one is recorded as `not_run` with its reason, never
111
+ as a pass. `--no-live-refs` on `doctor` or `qa run` skips only this read and
112
+ records `not_run` with reason `disabled`; every other declared write and send
113
+ is unchanged. Each form has its own row, including `doctor --write
114
+ --no-live-refs` and `qa run --no-live-refs` with each `qa run` modifier.
115
+ `doctor --no-write --no-live-refs` has the effects of the `doctor
116
+ --no-live-refs` row.
117
+
118
+ Only `doctor` and `qa run` make this read. The other commands that run doctor
119
+ internally make no request and record `derived.live_campaign_refs` as
120
+ `not_run` with reason `not_read`, with no warning: `start`, `prepare-build`
121
+ and `build` (the intake alias for prepare-build + doctor), `theme waive`,
122
+ `checkpoint`, `findings harvest`, `run-record`, `next`, and the doctor
123
+ sidecar refresh `qa run` makes after it records the QA stage (the verdict
124
+ itself carries QA's own read).
125
+
84
126
  ## How to read a row
85
127
 
86
128
  ```jsonc
@@ -108,7 +150,7 @@ There is **one row per command and per effect-changing flag combination**. The
108
150
  flags that change what the invocation does to the world are listed once, in
109
151
  `vocabulary.effect_changing_flags`: `--browser`, `--built`, `--dry-run`,
110
152
  `--emit-packet`, `--example`, `--force`, `--from-store`, `--list`,
111
- `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
153
+ `--no-live-refs`, `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
112
154
  `--no-write`, `--republish`, `--test-order`, `--write`, `--write-map`. Flags
113
155
  that only change the output shape (`--json`, `--report`) deliberately do not.
114
156
 
@@ -134,7 +176,9 @@ a flag the command rejects up front. It writes nothing, journals nothing, and is
134
176
  the row to read when you want to know what a typo costs. The one exception is
135
177
  declared on the rows it belongs to: `start`, `prepare-build`, `build`,
136
178
  `run start` and `run end` close out a **stale** run session at the root they are
137
- about to act on *before* argv is refused.
179
+ about to act on *before* argv is refused. Commands that implement `--dry-run`
180
+ skip this closeout whenever that flag is present, including a valued flag that
181
+ will be refused: `run end --dry-run yes` writes, sends, and deletes nothing.
138
182
 
139
183
  A refusal is decided by argv alone. When file content or state on disk decides
140
184
  the outcome, the command has reached a handler failure and journals it.
@@ -143,9 +187,13 @@ For intake, run-record, built-site QA, and `next`, argv-only checks run before
143
187
  their handler reads the target; invalid values are refused without a journal
144
188
  entry. For `start`, `prepare-build`, and `build`, bare, empty, and whitespace-only
145
189
  values of `--spec`, `--map-id`, `--source`, `--target`, `--source-kind`,
146
- `--proxy-base`, `--wrapper-policy`, `--design-manifest`, and
147
- `--order-path-depth` are refused before local spec reads, Map fetches, or cache
148
- writes on the `--spec`, `--map-id`, and `--map-id --cached-spec` paths.
190
+ `--proxy-base`, `--wrapper-policy`, `--design-manifest`, `--order-path-depth`,
191
+ `--template-family`, `--allow-uncertified-template`, `--theme-policy`, and
192
+ `--brief` are refused before local spec reads, Map fetches, or cache writes on
193
+ the `--spec`, `--map-id`, and `--map-id --cached-spec` paths. So is a
194
+ `--theme-policy` outside `inspect_only`, `auto`, and `off`. Whether a named
195
+ template family is certified, and whether a named brief can be read, depend on
196
+ file content: those checks still run in the handler and are journaled.
149
197
  The operator-facing `run-record` and `run end` commands refuse bare, empty, or
150
198
  whitespace-only values for every value-taking inherited run-record flag before
151
199
  packet work. The five agent
@@ -189,6 +237,12 @@ flag without a value is refused with "Missing value for --<flag>". If a named
189
237
  packet yields neither a Map ID nor a valid local-spec identity after checkpoint
190
238
  preflight reads the packet, spec, and report, the requirement is a journaled
191
239
  handler failure. A conflicting local/Map identity is also a handler failure.
240
+ When `qa run` selects `--legacy-api-test-order`, a missing, bare, empty, or
241
+ unusable `--cart` and an unknown legacy mode are argv-only refusals before QA
242
+ input resolution. They append no lifecycle entry. Accepted modes remain
243
+ `accept`, `decline`, and `both` (case-insensitive); browser `--test-order` still
244
+ takes precedence and does not require the legacy cart. API credentials are
245
+ still checked only inside the legacy handler and failures there are journaled.
192
246
  The nested run-record refusal scope in session closeout guards against future
193
247
  changes. No internal closeout can currently create a refusal before its
194
248
  invoking command journals.
@@ -205,7 +259,7 @@ sha256) before and after while a loopback `node:http` receiver counts requests.
205
259
  | `ambient_session` | An active ambient run session opened by `run start` at the target. |
206
260
  | `stale_session` | A run session idle past the 12 h TTL, at the target and at the working directory. |
207
261
  | `lifecycle_log` | `CAMPAIGNS_OS_LIFECYCLE_LOG` names a journal outside the runtime directory. |
208
- | `persisted_consent` | Run Telemetry consent **persisted on the machine for the loopback receiver's scope**, a synthetic campaign key in the environment, no run session, and `--proxy-base <loopback>` wherever the command takes it. |
262
+ | `persisted_consent` | Run Telemetry consent **persisted on the machine for the loopback receiver's scope**, a synthetic campaign key in the environment, no run session, and `--proxy-base <loopback>` wherever the command takes it. `start`, `prepare-build` and `build` name their spec by `--map-id` here, and the receiver serves it. |
209
263
 
210
264
  Five conditions rather than one, because the CLI's effects are not a function of
211
265
  argv alone: an ambient session redirects the journal and is itself touched by
@@ -307,10 +361,10 @@ loopback receiver only stands in for (`{base-url}`, the login gateway) matched
307
361
  refuses the contradiction rather than letting the test find it.
308
362
 
309
363
  A `full` row may still carry an individual effect the offline fixture cannot
310
- reach — the Map Builder spec fetch behind `--map-id`, the `codex` and `agents`
311
- destinations of `install-skills`. Each such entry has an empty `observed_in`
312
- **and** a `not_observed_reason`, and `check-effects.mjs` refuses one without the
313
- reason. What it may not be is silent.
364
+ reach — the `codex` and `agents` destinations of `install-skills`. Each such
365
+ entry has an empty `observed_in` **and** a `not_observed_reason`, and
366
+ `check-effects.mjs` refuses one without the reason. What it may not be is
367
+ silent.
314
368
 
315
369
  ## The rule
316
370
 
@@ -17,6 +17,9 @@ campaigns-os login --store example
17
17
  There is no discovery or guess from the current project. Noninteractive calls
18
18
  must supply `--store`. URLs, paths and unrelated hosts are refused before any
19
19
  request. Login uses the fixed `https://mcp.nextcommerce.com` gateway.
20
+ It asks for the `https://mcp.nextcommerce.com/mcp` resource and the
21
+ `campaigns.read` capability. A login saved before 1.43.2+agent.3, which named
22
+ the earlier `/campaigns` resource, is refused; sign in again.
20
23
 
21
24
  Open the displayed device page in one browser tab and enter the displayed code.
22
25
  Keep that tab: if installation is needed, follow its Install Campaigns link,
@@ -3,7 +3,7 @@
3
3
  For a new campaign, choose its working folder and run this from that folder:
4
4
 
5
5
  ```sh
6
- npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.43.1 next-campaign-page-kit@0.2.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
6
+ npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.46.0 next-campaign-page-kit@0.2.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
7
7
  ```
8
8
 
9
9
  Review the release source/provenance before installation as described in
@@ -9,6 +9,7 @@
9
9
  contracts/orientation-reason-codes.v1.json
10
10
  contracts/orientation-limits.v1.json
11
11
  contracts/supported-surface.json
12
+ contracts/release-ledger.json (baseline_floor only)
12
13
  contracts/fixtures/orientation/canonicalization/v1.json
13
14
  Regenerate: node ./scripts/generate-orientation-reference.mjs --write
14
15
  CI runs the same script with --check, so a stale copy of this file fails the build.
@@ -25,7 +26,7 @@ Ledger schema id: `campaigns-os-release-ledger/v1`
25
26
  Change policy version: `1.0.0`
26
27
  Reason-code vocabulary version: `1.0.0`
27
28
  Limits version: `1.0.0`
28
- Supported surface at generation time: `1.43.1`
29
+ Supported surface at generation time: `1.46.0`
29
30
 
30
31
  ## Forward compatibility
31
32
 
@@ -60,6 +61,43 @@ position, so a consumer reading history in array order reads it in `sequence` or
60
61
  its number to `sequence`; the two diverge legitimately, because when concurrent pull requests
61
62
  land the later one restamps its `sequence` to follow the earlier while keeping the id it was
62
63
  written with. Order by `sequence`, identify by `id`, and do not infer one from the other.
64
+ After a baseline rotation the file starts at `baseline_floor.last_archived_sequence + 1`, not at 1;
65
+ entries are never renumbered, so position plus that offset is still the sequence.
66
+
67
+ ## Baseline rotation
68
+
69
+ The ledger and the changelog are bounded as whole files, so they are rotated rather than allowed to
70
+ outgrow the limits. A rotation moves every entry up to a reviewed cut, and the changelog from the first section
71
+ those entries link to the end of the file (unlinked sections in that range included), byte-for-byte and in order
72
+ into a dated archive pair under `contracts/archive/`, and declares the cut as
73
+ `baseline_floor` in `contracts/release-ledger.json`. Archived entries keep their `sequence`, `entry_sha256` and
74
+ `changelog_sha256`; each section hash verifies against the archive changelog. The floor names the archive
75
+ files with their SHA-256, the last archived entry and its sequence, the first kept entry, the live entry that
76
+ recorded the rotation, and the reason code to refuse with. The floor only moves forward, only with a new
77
+ rotation entry, and an archive file never changes once merged; the next rotation writes a new dated pair.
78
+
79
+ Rotation changes no mandatory read. The reading order in `AGENTS.md` is unchanged, and `source_bytes` measures
80
+ exactly its data files (steps 1 to 8):
81
+
82
+ - `contracts/supported-surface.json`
83
+ - `contracts/release-ledger.json`
84
+ - `CHANGELOG.md`
85
+ - `contracts/orientation-limits.v1.json`
86
+ - `contracts/orientation-reason-codes.v1.json`
87
+ - `schemas/campaigns-os-tooling-orientation.v1.schema.json`
88
+ - `schemas/campaigns-os-release-ledger.v1.schema.json`
89
+ - `contracts/agent-relevant-change-policy.v1.json`
90
+
91
+ The archive files are not among them: they are optional history reads and count against no limit, and their
92
+ sections and entries are not in `section_count` or `ledger_entries`.
93
+
94
+ Current floor: last archived entry `RL-0124` (sequence 124), first kept entry
95
+ `RL-0125`, recorded by `RL-0190`. Archives, oldest rotation first:
96
+
97
+ - [`contracts/archive/release-ledger.2026-09-30.json`](../contracts/archive/release-ledger.2026-09-30.json) and [`contracts/archive/CHANGELOG.2026-09-30.md`](../contracts/archive/CHANGELOG.2026-09-30.md)
98
+
99
+ A consumer whose reviewed baseline's newest ledger entry is older than `RL-0124` refuses with
100
+ `baseline_below_floor`. Adopt a newer reviewed baseline whose ledger reaches the floor's last_archived_id; every commit at or after the floor's first_kept_id qualifies. Do not orient on the partial live window, and do not stitch the archive in as a substitute: the archive holds the history for reference, not for this read.
63
101
 
64
102
  ## Release-ledger digest canonicalization
65
103
 
@@ -120,6 +158,7 @@ consumer's parser tests.
120
158
  |---|---|---|---|---|---|
121
159
  | `ahead_of_upstream` | `refused` | campaigns-agent | `TP-D4-ahead-of-upstream` | The checkout carries commits the upstream default branch does not, so its contracts are not a published generation. | Push or drop the local commits, or run against a managed generation at the published OID. |
122
160
  | `already_current` | `current` | campaigns-agent | `TP-B3-already-current` | The verified target equals the active generation; nothing changed and no restart is required. | None. |
161
+ | `baseline_below_floor` | `refused` | campaigns-os | `A1-baseline-below-floor` | The target ledger declares a baseline_floor, and the reviewed baseline's newest ledger entry is older than the floor's last archived entry. Part of the baseline-to-target window was rotated into the archive files the floor names, which are not mandatory orientation reads, so the window cannot be read from the live ledger and changelog. | Adopt a newer reviewed baseline whose ledger reaches the floor's last_archived_id; every commit at or after the floor's first_kept_id qualifies. Do not orient on the partial live window, and do not stitch the archive in as a substitute: the archive holds the history for reference, not for this read. |
123
162
  | `checkout_acquisition_failed` | `refused` | campaigns-agent | `TP-D1-acquisition-failed` | Managed acquisition did not complete: interrupted clone, remote validation failure, or a broken linked-worktree backpointer. Partial state is quarantined, never exposed as ready. | Rerun acquisition. If it fails repeatedly, inspect the quarantined partial named in the diagnostic and confirm remote reachability. |
124
163
  | `checkout_already_exists` | `refused` | campaigns-agent | `TP-D1-checkout-already-exists` | The managed root reserved for acquisition appeared concurrently and is not an empty reservation this run owns. | Rerun so the existing root is validated as a managed store, or remove the unexpected directory after confirming it holds no needed state. |
125
164
  | `checkout_missing` | `refused` | campaigns-agent | `TP-D1-checkout-missing` | No checkout exists at the configured location and the mode does not authorize acquiring one. | Supply the operator checkout at the configured path, or switch to managed mode so a generation can be acquired. |
@@ -205,7 +244,7 @@ nowhere in the schemas, or in the schemas and not here, is a generation failure.
205
244
  | `$defs.baseline.properties.kind` | `legacy_commit`, `supported_surface` |
206
245
  | `$defs.change_class` | `schema`, `hashed_surface`, `named_surface`, `cli_surface`, `skill`, `package_export`, `compatibility_policy`, `documentation`, `workflow`, `generated_runtime` |
207
246
  | `$defs.disposition` | `current`, `orientation_available`, `updated`, `restart_required`, `recovered_interrupted_update`, `legacy_baseline`, `freshness_unknown`, `refused` |
208
- | `$defs.reason_code` | `already_current`, `ahead_of_upstream`, `checkout_acquisition_failed`, `checkout_already_exists`, `checkout_missing`, `checkout_mutation_not_authorized`, `detached_head`, `dirty_checkout`, `diverged_history`, `evidence_budget_exceeded`, `fetch_failed`, `git_environment_unsafe`, `history_incomplete`, `missing_upstream`, `orientation_contract_missing`, `orientation_in_progress`, `orientation_incomplete`, `orientation_rendered`, `orientation_too_large`, `pointer_race`, `runtime_commit_mismatch`, `runtime_refresh_failed`, `runtime_refresh_required`, `surface_incompatible`, `transaction_incomplete`, `transaction_reconciled`, `wrong_remote` |
247
+ | `$defs.reason_code` | `already_current`, `ahead_of_upstream`, `baseline_below_floor`, `checkout_acquisition_failed`, `checkout_already_exists`, `checkout_missing`, `checkout_mutation_not_authorized`, `detached_head`, `dirty_checkout`, `diverged_history`, `evidence_budget_exceeded`, `fetch_failed`, `git_environment_unsafe`, `history_incomplete`, `missing_upstream`, `orientation_contract_missing`, `orientation_in_progress`, `orientation_incomplete`, `orientation_rendered`, `orientation_too_large`, `pointer_race`, `runtime_commit_mismatch`, `runtime_refresh_failed`, `runtime_refresh_required`, `surface_incompatible`, `transaction_incomplete`, `transaction_reconciled`, `wrong_remote` |
209
248
 
210
249
  ### `schemas/campaigns-os-release-ledger.v1.schema.json`
211
250
 
@@ -225,6 +264,7 @@ so a renamed command fails here as well as at the supported-surface gate.
225
264
  - `campaigns-os prepare-build`
226
265
  - `campaigns-os build`
227
266
  - `campaigns-os polish`
267
+ - `campaigns-os record`
228
268
  - `campaigns-os checkpoint`
229
269
  - `campaigns-os page-kit`
230
270
  - `campaigns-os spec`
@@ -21,6 +21,11 @@ Three layers must all be satisfied:
21
21
  `campaigns-os polish capture` from the current packet, report, served build,
22
22
  mapped routes, and fixed desktop/mobile viewports.
23
23
 
24
+ Record all three with `campaigns-os record polish` (§7) rather than editing
25
+ the report by hand: it stamps the stage-record fields from doctor's current
26
+ state, keeps the captured `page_load`, and writes nothing unless this gate would
27
+ pass on the result.
28
+
24
29
  ## 1. Stage-record requirements
25
30
 
26
31
  The gate only applies once assembly is complete
@@ -500,3 +505,72 @@ current value.
500
505
  Polish evidence certifies the polish pass only — it is not QA and does not
501
506
  certify launch readiness (`docs/qa-and-test-orders.md` owns the QA proof
502
507
  stack).
508
+
509
+ ## 7. Recording with `record polish`
510
+
511
+ ```bash
512
+ campaigns-os record polish --packet <campaign-runtime.build.json> --evidence <polish-evidence.json> [--report <json>] [--dry-run] [--json]
513
+ ```
514
+
515
+ Run it after `campaigns-os polish capture`, against the build `campaigns-os
516
+ record build` recorded. The `--evidence` file is a JSON object with these keys;
517
+ any other key is refused:
518
+
519
+ | Key | Required | Written to |
520
+ |---|---|---|
521
+ | `status` | no (default `completed`) | `stages.polish.status`; `completed`, `completed_with_warnings`, `blocked` or `skipped` |
522
+ | `evidence` | for `completed*` | `stages.polish.evidence`, the seven categories of §2. Leave out `visual_review.page_load`: the value `polish capture` recorded is kept, and a file that carries one is refused. Without it, a `blocked` or `skipped` record keeps the evidence already on the report. |
523
+ | `blockers` | for `blocked` only | `stages.polish.blockers`: a non-empty array of `{"code", "message"}` objects. |
524
+ | `skip_reason` | for `skipped` only | `stages.polish.skip_reason`: a non-empty string. |
525
+ | `repair_loop_defect` | no | `report.theme.repair_loop_defect`: `null`, or the first brand-layer repair-loop defect as an object (`code`, `message`, `path`, `detail`). A non-null defect needs a recorded `report.theme`. |
526
+
527
+ The command itself stamps `performed_by: "next-campaigns-polish"`,
528
+ `source_build_fingerprint` (doctor's `derived.build_output_fingerprint.value`,
529
+ which must equal the recorded `stages.assembly.build_fingerprint`),
530
+ `source_package_material_fingerprint` when the report fingerprints a Design
531
+ Source Package, and `completed_at` (not for `blocked`). It then validates the
532
+ report it would write against
533
+ `schemas/campaign-runtime-assembly-report.v0.schema.json` and doctor's report
534
+ checks and, for a `completed*` status, evaluates this gate and the hidden
535
+ eager-media checkpoint over it exactly as doctor does. A `blocked` or `skipped`
536
+ record keeps this gate blocked, so `next` stays at Polish and QA stays blocked. Any failure is printed by field or gate code
537
+ (for example `repair_loop_defect must be null or an object ... (got string)`, or
538
+ `polish.evidence_incomplete` with its per-field problems), the command exits
539
+ non-zero, and nothing is written. `--dry-run` runs every check and writes
540
+ nothing. Like `record setup` and `record build`, it refuses a report that is not
541
+ bound to the packet (`docs/build-packet.md`, "Recording stage completion") and
542
+ output that changes while it records.
543
+
544
+ A complete file:
545
+
546
+ ```json
547
+ {
548
+ "status": "completed",
549
+ "evidence": {
550
+ "visual_review": {
551
+ "screenshots": ["qa-output/polish/landing-desktop.png", "qa-output/polish/landing-mobile.png"]
552
+ },
553
+ "brand_review": {
554
+ "logo_checked": true,
555
+ "favicon": { "status": "confirmed_non_template" },
556
+ "colors": ["#1f4d3a"],
557
+ "brand_bleed": { "cleared": true }
558
+ },
559
+ "checkout_review": {
560
+ "field_labels": "initial field labels and placeholders are legible on desktop and mobile",
561
+ "phone_alignment": "checked",
562
+ "payment_display": "checked",
563
+ "bump_compare_price_rule": "no equal or no-discount compare price renders"
564
+ },
565
+ "template_residue_review": {
566
+ "next_blue": "not found",
567
+ "starter_favicon": { "status": "confirmed_non_template" },
568
+ "lorem": "not found"
569
+ },
570
+ "commerce_flow_review": "direct-entry package selection reviewed",
571
+ "issues": [],
572
+ "commands": ["next-campaigns-polish", "campaigns-os polish capture"]
573
+ },
574
+ "repair_loop_defect": null
575
+ }
576
+ ```
@@ -59,12 +59,16 @@ across them. A progress stream is independent of a run-session ID.
59
59
 
60
60
  Sanitized immutable snapshots are written under the target repository's
61
61
  `.campaign-runtime/progress/` before any request. Allocation uses an exclusive
62
- local lock with a process owner. Dead owners are recovered through an exclusive
63
- recovery claim and an atomic rename; a live process is never evicted. An ownerless
64
- crash gap is recoverable after ten seconds. If recovery itself is interrupted,
65
- capture fails closed: stop all Campaigns OS writers for that target, then remove
66
- the abandoned `.allocation-lock` directory in the affected progress scope before
67
- retrying `next`. Do not remove a lock while a writer is active.
62
+ local lock with a process owner; the lock directory and its owner record are
63
+ published together in one rename, so a lock never exists without its owner.
64
+ Dead owners are recovered through an exclusive recovery claim and an atomic
65
+ rename; a live process is never evicted. A lock directory with no owner record
66
+ (left by an older release) is never taken over: capture refuses it after about
67
+ a second. If one is found, or if recovery itself is interrupted, capture fails
68
+ closed and the warning names the affected `.allocation-lock` directory: stop
69
+ 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
71
+ older Campaigns OS release against the same target at the same time.
68
72
 
69
73
  An unchanged projection reuses its ID, timestamp and sequence. Identity
70
74
  changes start a new stream. Each local scope retains at most 32 snapshots and