@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
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "observed_in": "The conditions in which the effect test must SEE this write or send. An empty list declares an effect the offline fixture cannot reach; it then requires a not_observed_reason, and check-effects refuses the row without one. The test always refuses an effect that is NOT declared, in every condition.",
43
43
  "effect_changing_flags": [
44
+ "--apply",
44
45
  "--browser",
45
46
  "--built",
46
47
  "--dry-run",
@@ -233,6 +234,72 @@
233
234
  "test_scope": "full",
234
235
  "notes": "A static scan of tracked source in a Git checkout. Exempted from lifecycle capture by name in persistLifecycleIfRequested, and it bypasses the stale-session sweep."
235
236
  },
237
+ {
238
+ "command": "sdk",
239
+ "subcommand": "repin",
240
+ "flags": [],
241
+ "annotations": {
242
+ "readOnlyHint": true,
243
+ "destructiveHint": false,
244
+ "openWorldHint": false,
245
+ "idempotentHint": true
246
+ },
247
+ "tier": "none",
248
+ "writes": [],
249
+ "sends": [],
250
+ "effect_test": "effects: sdk repin",
251
+ "test_scope": "full",
252
+ "notes": "The preview: it reads the target's HTML and script files and prints each semver-pinned Campaign Cart loader.js / campaign-cart.css reference below the target version with its rewrite, and the references it leaves alone. It writes nothing, appends no lifecycle entry (only --apply journals; the subcommand is declared journal-exempt without it in src/invocation.mjs), runs no stale-session closeout and sends nothing. A target holding _data/campaigns.json is refused (exit 2) with the same result: nothing written."
253
+ },
254
+ {
255
+ "command": "sdk",
256
+ "subcommand": "repin",
257
+ "flags": [
258
+ "--apply"
259
+ ],
260
+ "annotations": {
261
+ "readOnlyHint": false,
262
+ "destructiveHint": false,
263
+ "openWorldHint": false,
264
+ "idempotentHint": true
265
+ },
266
+ "tier": "B",
267
+ "writes": [
268
+ {
269
+ "path": "{target}/**",
270
+ "when": "an HTML or script file under the target holds a semver-pinned Campaign Cart loader.js / campaign-cart.css URL below the target version; only the version segment of each such URL changes",
271
+ "observed_in": [
272
+ "no_session",
273
+ "ambient_session",
274
+ "stale_session",
275
+ "lifecycle_log",
276
+ "persisted_consent"
277
+ ]
278
+ },
279
+ {
280
+ "path": "{target}/.campaign-runtime/sdk-repin.json",
281
+ "when": "at least one reference was rewritten: the change record (campaigns-os-sdk-repin/v0) is (re)written",
282
+ "observed_in": [
283
+ "no_session",
284
+ "ambient_session",
285
+ "stale_session",
286
+ "lifecycle_log",
287
+ "persisted_consent"
288
+ ]
289
+ },
290
+ {
291
+ "path": "{lifecycle-journal}",
292
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG",
293
+ "observed_in": [
294
+ "lifecycle_log"
295
+ ]
296
+ }
297
+ ],
298
+ "sends": [],
299
+ "effect_test": "effects: sdk repin --apply",
300
+ "test_scope": "full",
301
+ "notes": "Rewrites the version segment of each semver-pinned reference below the target and writes the change record; @latest, @main, commit and prerelease refs and pins at or above the target are left alone. A second run with the same target finds nothing to change and writes nothing. `sdk repin --target <repo>` names no packet, so an ambient session bound to a packet elsewhere is not resolved for it and no entry is appended under that session. A target holding _data/campaigns.json is refused (exit 2) and nothing is written."
302
+ },
236
303
  {
237
304
  "command": "tooling",
238
305
  "subcommand": "diagnose",
@@ -2202,11 +2269,10 @@
2202
2269
  {
2203
2270
  "destination": "{proxy-base}/api/qa/verdicts",
2204
2271
  "what": "the QA verdict for the attempt, published to the QA portal",
2205
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2272
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2206
2273
  "requires_consent": true,
2207
- "observed_in": [
2208
- "persisted_consent"
2209
- ]
2274
+ "observed_in": [],
2275
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
2210
2276
  },
2211
2277
  {
2212
2278
  "destination": "{base-url}",
@@ -2244,7 +2310,7 @@
2244
2310
  ],
2245
2311
  "effect_test": "effects: qa run",
2246
2312
  "test_scope": "full",
2247
- "notes": "Destructive and not idempotent: each attempt overwrites the committed verdict sidecar and the report's QA stage, and a ready verdict clears the ambient run session. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict. When a served page was read and a public Campaigns API key resolves, it also makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check the served pages' shipping and package refs against the live campaign (#533); without a key or a page, or on a failed read, the verdict records the check as not_run with its reason. --no-live-refs skips that read (its own row)."
2313
+ "notes": "Destructive and not idempotent: each attempt overwrites the committed verdict sidecar and the report's QA stage, and a ready verdict clears the ambient run session. It SENDS on a blocked attempt too: the QA stage progress observation to {proxy-base}/api/progress under persisted consent for the endpoint, and the verdict to {proxy-base}/api/qa/verdicts under the same consent unless the base URL is a local address, which keeps the verdict local by default (#486; --post-verdict still sends it). Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict. When a served page was read and a public Campaigns API key resolves, it also makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check the served pages' shipping and package refs against the live campaign (#533); without a key or a page, or on a failed read, the verdict records the check as not_run with its reason. --no-live-refs skips that read (its own row)."
2248
2314
  },
2249
2315
  {
2250
2316
  "command": "qa",
@@ -2335,11 +2401,10 @@
2335
2401
  {
2336
2402
  "destination": "{proxy-base}/api/qa/verdicts",
2337
2403
  "what": "the QA verdict for the attempt, published to the QA portal",
2338
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2404
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2339
2405
  "requires_consent": true,
2340
- "observed_in": [
2341
- "persisted_consent"
2342
- ]
2406
+ "observed_in": [],
2407
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
2343
2408
  },
2344
2409
  {
2345
2410
  "destination": "{base-url}",
@@ -2583,11 +2648,10 @@
2583
2648
  {
2584
2649
  "destination": "{proxy-base}/api/qa/verdicts",
2585
2650
  "what": "the QA verdict for the attempt, published to the QA portal",
2586
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2651
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2587
2652
  "requires_consent": true,
2588
- "observed_in": [
2589
- "persisted_consent"
2590
- ]
2653
+ "observed_in": [],
2654
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
2591
2655
  },
2592
2656
  {
2593
2657
  "destination": "{base-url}",
@@ -2625,7 +2689,7 @@
2625
2689
  ],
2626
2690
  "effect_test": "effects: qa run --test-order",
2627
2691
  "test_scope": "full",
2628
- "notes": "The most consequential row in the file: --test-order places REAL orders against the store under test. Destructive, open-world, not idempotent. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict. When a served page was read and a public Campaigns API key resolves, it also makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check the served pages' shipping and package refs against the live campaign (#533); without a key or a page, or on a failed read, the verdict records the check as not_run with its reason. --no-live-refs skips that read (its own row)."
2692
+ "notes": "The most consequential row in the file: --test-order places REAL orders against the store under test. Destructive, open-world, not idempotent. It SENDS on a blocked attempt too: the QA stage progress observation to {proxy-base}/api/progress under persisted consent for the endpoint, and the verdict to {proxy-base}/api/qa/verdicts under the same consent unless the base URL is a local address, which keeps the verdict local by default (#486; --post-verdict still sends it). Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict. When a served page was read and a public Campaigns API key resolves, it also makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check the served pages' shipping and package refs against the live campaign (#533); without a key or a page, or on a failed read, the verdict records the check as not_run with its reason. --no-live-refs skips that read (its own row)."
2629
2693
  },
2630
2694
  {
2631
2695
  "command": "qa",
@@ -2716,11 +2780,10 @@
2716
2780
  {
2717
2781
  "destination": "{proxy-base}/api/qa/verdicts",
2718
2782
  "what": "the QA verdict for the attempt, published to the QA portal",
2719
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2783
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2720
2784
  "requires_consent": true,
2721
- "observed_in": [
2722
- "persisted_consent"
2723
- ]
2785
+ "observed_in": [],
2786
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
2724
2787
  },
2725
2788
  {
2726
2789
  "destination": "{base-url}",
@@ -2766,7 +2829,7 @@
2766
2829
  ],
2767
2830
  "effect_test": "effects: qa run --browser (preflight)",
2768
2831
  "test_scope": "preflight",
2769
- "notes": "PREFLIGHT-PROVED: --browser needs an installed Chromium and a reachable campaign, neither of which exists offline. The case proves the attempt is blocked at the same gate as the node run and writes exactly the blocked-attempt evidence. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict. When a served page was read and a public Campaigns API key resolves, it also makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check the served pages' shipping and package refs against the live campaign (#533); without a key or a page, or on a failed read, the verdict records the check as not_run with its reason. --no-live-refs skips that read (its own row). Once the page checks run, it also sends the header-only GETs to the configured http(s) store policy URLs (their own row) to check the served pages' policy links; offline the run is blocked at the gate before any page is checked, so none is sent.",
2832
+ "notes": "PREFLIGHT-PROVED: --browser needs an installed Chromium and a reachable campaign, neither of which exists offline. The case proves the attempt is blocked at the same gate as the node run and writes exactly the blocked-attempt evidence. It SENDS on a blocked attempt too: the QA stage progress observation to {proxy-base}/api/progress under persisted consent for the endpoint, and the verdict to {proxy-base}/api/qa/verdicts under the same consent unless the base URL is a local address, which keeps the verdict local by default (#486; --post-verdict still sends it). Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict. When a served page was read and a public Campaigns API key resolves, it also makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check the served pages' shipping and package refs against the live campaign (#533); without a key or a page, or on a failed read, the verdict records the check as not_run with its reason. --no-live-refs skips that read (its own row). Once the page checks run, it also sends the header-only GETs to the configured http(s) store policy URLs (their own row) to check the served pages' policy links; offline the run is blocked at the gate before any page is checked, so none is sent.",
2770
2833
  "preflight": {
2771
2834
  "may_write": [
2772
2835
  "{target}/qa-output/**",
@@ -2872,11 +2935,10 @@
2872
2935
  {
2873
2936
  "destination": "{proxy-base}/api/qa/verdicts",
2874
2937
  "what": "the QA verdict for the attempt, published to the QA portal",
2875
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2938
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2876
2939
  "requires_consent": true,
2877
- "observed_in": [
2878
- "persisted_consent"
2879
- ]
2940
+ "observed_in": [],
2941
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
2880
2942
  },
2881
2943
  {
2882
2944
  "destination": "{base-url}",
@@ -2998,11 +3060,10 @@
2998
3060
  {
2999
3061
  "destination": "{proxy-base}/api/qa/verdicts",
3000
3062
  "what": "the QA verdict for the attempt, published to the QA portal",
3001
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
3063
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
3002
3064
  "requires_consent": true,
3003
- "observed_in": [
3004
- "persisted_consent"
3005
- ]
3065
+ "observed_in": [],
3066
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
3006
3067
  },
3007
3068
  {
3008
3069
  "destination": "{base-url}",
@@ -3232,11 +3293,10 @@
3232
3293
  {
3233
3294
  "destination": "{proxy-base}/api/qa/verdicts",
3234
3295
  "what": "the QA verdict for the attempt, published to the QA portal",
3235
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
3296
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
3236
3297
  "requires_consent": true,
3237
- "observed_in": [
3238
- "persisted_consent"
3239
- ]
3298
+ "observed_in": [],
3299
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
3240
3300
  },
3241
3301
  {
3242
3302
  "destination": "{base-url}",
@@ -3358,11 +3418,10 @@
3358
3418
  {
3359
3419
  "destination": "{proxy-base}/api/qa/verdicts",
3360
3420
  "what": "the QA verdict for the attempt, published to the QA portal",
3361
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
3421
+ "when": "--post-verdict is given; or, by default (no --no-post-verdict), the base URL is not a local address (localhost, *.localhost, 127.x.x.x, 0.0.0.0, [::1], an IPv4-mapped loopback) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
3362
3422
  "requires_consent": true,
3363
- "observed_in": [
3364
- "persisted_consent"
3365
- ]
3423
+ "observed_in": [],
3424
+ "not_observed_reason": "the offline fixture serves the campaign from a 127.0.0.1 base URL, a local address, so the default keeps the verdict local (#486); qa publish proves this send"
3366
3425
  },
3367
3426
  {
3368
3427
  "destination": "{base-url}",
@@ -4406,7 +4465,7 @@
4406
4465
  ],
4407
4466
  "effect_test": "effects: start",
4408
4467
  "test_scope": "full",
4409
- "notes": "Intake: Build Packet, sidecars, agent context and the auto-opened run session. Not idempotent — a second `start` over an assembly report that carries stage evidence is refused without --force."
4468
+ "notes": "Intake: Build Packet, sidecars, agent context and the auto-opened run session. Not idempotent — a second `start` is refused without --force when the report carries stage evidence, stage history or operator decisions (recorded waivers, warning accepts, an applied theme or report evidence), or when the run would change the packet's recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation. To save brief answers or a CampaignSpec change without discarding anything, run `record brief` or `record spec` instead."
4410
4469
  },
4411
4470
  {
4412
4471
  "command": "start",
@@ -4565,7 +4624,7 @@
4565
4624
  ],
4566
4625
  "effect_test": "effects: start --force",
4567
4626
  "test_scope": "full",
4568
- "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, printing the stage keys it cleared. This is the flag that makes `start` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path."
4627
+ "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, stage history or operator decisions, and the packet's recorded deploy target, preview URL, production URL, order-path depth and allowed-domains confirmation, printing the stage keys and the operator state it cleared. It archives completed build, Polish and QA records into stage history, which is kept, and keeps each stage's record that its inputs changed. This is the flag that makes `start` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path."
4569
4628
  },
4570
4629
  {
4571
4630
  "command": "start",
@@ -4831,7 +4890,7 @@
4831
4890
  ],
4832
4891
  "effect_test": "effects: prepare-build",
4833
4892
  "test_scope": "full",
4834
- "notes": "Intake without doctor and without the agent-context install: the packet, the sidecars and the auto-opened session. Same stale-session closeout as `start`."
4893
+ "notes": "Intake without doctor and without the agent-context install: the packet, the sidecars and the auto-opened session. Same stale-session closeout as `start`. A second `prepare-build` is refused without --force when the report carries stage evidence, stage history or operator decisions (recorded waivers, warning accepts, an applied theme or report evidence), or when the run would change the packet's recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation. To save brief answers or a CampaignSpec change without discarding anything, run `record brief` or `record spec` instead."
4835
4894
  },
4836
4895
  {
4837
4896
  "command": "prepare-build",
@@ -5077,7 +5136,7 @@
5077
5136
  ],
5078
5137
  "effect_test": "effects: prepare-build --force",
5079
5138
  "test_scope": "full",
5080
- "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, printing the stage keys it cleared. This is the flag that makes `prepare-build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5139
+ "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, stage history or operator decisions, and the packet's recorded deploy target, preview URL, production URL, order-path depth and allowed-domains confirmation, printing the stage keys and the operator state it cleared. It archives completed build, Polish and QA records into stage history, which is kept, and keeps each stage's record that its inputs changed. This is the flag that makes `prepare-build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5081
5140
  },
5082
5141
  {
5083
5142
  "command": "build",
@@ -5223,7 +5282,7 @@
5223
5282
  ],
5224
5283
  "effect_test": "effects: build",
5225
5284
  "test_scope": "full",
5226
- "notes": "The intake alias for prepare-build + doctor: the same packet and sidecars, plus the doctor sidecar. It does not install the agent context."
5285
+ "notes": "The intake alias for prepare-build + doctor: the same packet and sidecars, plus the doctor sidecar. It does not install the agent context. A second `build` is refused without --force when the report carries stage evidence, stage history or operator decisions (recorded waivers, warning accepts, an applied theme or report evidence), or when the run would change the packet's recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation. To save brief answers or a CampaignSpec change without discarding anything, run `record brief` or `record spec` instead."
5227
5286
  },
5228
5287
  {
5229
5288
  "command": "build",
@@ -5491,7 +5550,7 @@
5491
5550
  ],
5492
5551
  "effect_test": "effects: build --force",
5493
5552
  "test_scope": "full",
5494
- "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, printing the stage keys it cleared. This is the flag that makes `build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5553
+ "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, stage history or operator decisions, and the packet's recorded deploy target, preview URL, production URL, order-path depth and allowed-domains confirmation, printing the stage keys and the operator state it cleared. It archives completed build, Polish and QA records into stage history, which is kept, and keeps each stage's record that its inputs changed. This is the flag that makes `build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5495
5554
  },
5496
5555
  {
5497
5556
  "command": "spec",
@@ -5683,6 +5742,195 @@
5683
5742
  "may_contact": []
5684
5743
  }
5685
5744
  },
5745
+ {
5746
+ "command": "record",
5747
+ "subcommand": "brief",
5748
+ "flags": [],
5749
+ "annotations": {
5750
+ "readOnlyHint": false,
5751
+ "destructiveHint": true,
5752
+ "openWorldHint": false,
5753
+ "idempotentHint": true
5754
+ },
5755
+ "tier": "C",
5756
+ "writes": [
5757
+ {
5758
+ "path": "{packet}",
5759
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — build_brief's mode, status, input_path, question and gate counts and brief bindings",
5760
+ "observed_in": [
5761
+ "no_session",
5762
+ "ambient_session",
5763
+ "stale_session",
5764
+ "lifecycle_log",
5765
+ "persisted_consent"
5766
+ ]
5767
+ },
5768
+ {
5769
+ "path": "{target}/.campaign-runtime/input/*",
5770
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the normalized brief, re-derived from the saved brief file",
5771
+ "observed_in": [
5772
+ "no_session",
5773
+ "ambient_session",
5774
+ "stale_session",
5775
+ "lifecycle_log",
5776
+ "persisted_consent"
5777
+ ]
5778
+ },
5779
+ {
5780
+ "path": "{target}/.campaign-runtime/build-context.json",
5781
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the brief summary, its bindings and the brief's prompts",
5782
+ "observed_in": [
5783
+ "no_session",
5784
+ "ambient_session",
5785
+ "stale_session",
5786
+ "lifecycle_log",
5787
+ "persisted_consent"
5788
+ ]
5789
+ },
5790
+ {
5791
+ "path": "{target}/.campaign-runtime/assembly-report.json",
5792
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the brief summary and bindings, and stages.prepare_build re-derived from the brief's blockers; on a material change (saved_with_invalidation) build, Polish and QA become required (QA alone when only qa_policy changed), each recording input_change and keeping the record it replaced in its history, and evidence[] gains a line",
5793
+ "observed_in": [
5794
+ "no_session",
5795
+ "ambient_session",
5796
+ "stale_session",
5797
+ "lifecycle_log",
5798
+ "persisted_consent"
5799
+ ]
5800
+ },
5801
+ {
5802
+ "path": "{target}/.campaign-runtime/doctor-output.json",
5803
+ "when": "a save changes the brief (outcome saved or saved_with_invalidation) and a retained doctor sidecar exists — the write postdates the doctor snapshot, so it is stamped stale",
5804
+ "observed_in": [
5805
+ "no_session",
5806
+ "ambient_session",
5807
+ "stale_session",
5808
+ "lifecycle_log",
5809
+ "persisted_consent"
5810
+ ]
5811
+ },
5812
+ {
5813
+ "path": "{lifecycle-journal}",
5814
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG — on every outcome, an unchanged or refused save included; only --dry-run, and a flag refused before the command runs (such as an unknown flag), append no entry",
5815
+ "observed_in": [
5816
+ "ambient_session",
5817
+ "lifecycle_log"
5818
+ ]
5819
+ }
5820
+ ],
5821
+ "sends": [],
5822
+ "effect_test": "effects: record brief",
5823
+ "test_scope": "full",
5824
+ "notes": "Destructive: a save whose brief material changed makes build, Polish and QA owed again (QA alone when only qa_policy changed); each superseded record moves to that stage's history, which keeps the last five per stage and is never used as current proof. Setup and deploy records, waivers, warning accepts and the applied theme are kept. Idempotent: a repeat reads unchanged. A save whose brief content, mode, status, open questions, gates and file bytes all match what is bound reads unchanged and writes nothing but the lifecycle entry; a reformatted save (key order, an explicit default, `_meta` only) keeps every stage's evidence and status but may rewrite the brief files. Reads the brief file (--brief, otherwise campaign-build-brief.yaml, .yml or .json in the source root or the target) and the CampaignSpec; sends nothing. Refused, writing nothing but the lifecycle entry, when the brief file is over 1 MiB or names more than 512 design_authority entries, names a file Campaigns OS writes itself (the normalized brief, the Build Packet, the Build Context, the Assembly Report or the doctor sidecar), is missing or not a readable regular file, when the source root and the target hold differing brief files and no --brief names one, when the Build Context lacks the intake products evaluating the brief needs, when the CampaignSpec material differs from the one the Assembly Report binds (run record spec first), or when the CampaignSpec's active pages changed (that needs intake)."
5825
+ },
5826
+ {
5827
+ "command": "record",
5828
+ "subcommand": "brief",
5829
+ "flags": [
5830
+ "--dry-run"
5831
+ ],
5832
+ "annotations": {
5833
+ "readOnlyHint": true,
5834
+ "destructiveHint": false,
5835
+ "openWorldHint": false,
5836
+ "idempotentHint": true
5837
+ },
5838
+ "tier": "none",
5839
+ "writes": [],
5840
+ "sends": [],
5841
+ "effect_test": "effects: record brief --dry-run",
5842
+ "test_scope": "full",
5843
+ "notes": "record brief --dry-run runs every check the real invocation runs and prints the outcome and the files it would write; it writes nothing (no packet, brief, Build Context, report or stale stamp), takes no target lock, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
5844
+ },
5845
+ {
5846
+ "command": "record",
5847
+ "subcommand": "spec",
5848
+ "flags": [],
5849
+ "annotations": {
5850
+ "readOnlyHint": false,
5851
+ "destructiveHint": true,
5852
+ "openWorldHint": false,
5853
+ "idempotentHint": true
5854
+ },
5855
+ "tier": "C",
5856
+ "writes": [
5857
+ {
5858
+ "path": "{target}/.campaign-runtime/input/*",
5859
+ "when": "the refresh runs (outcome refreshed) and the brief re-derived from the brief file last saved by intake or `record brief` (or from the generated draft) differs from what is bound, or the normalized brief on disk no longer holds the re-derived brief's material — the normalized brief",
5860
+ "observed_in": [
5861
+ "no_session",
5862
+ "ambient_session",
5863
+ "stale_session",
5864
+ "lifecycle_log",
5865
+ "persisted_consent"
5866
+ ]
5867
+ },
5868
+ {
5869
+ "path": "{target}/.campaign-runtime/build-context.json",
5870
+ "when": "the refresh runs (outcome refreshed) — spec.hash, spec.material_hash and the active pages' labels; the brief summary and bindings when the re-derived brief is saved",
5871
+ "observed_in": [
5872
+ "no_session",
5873
+ "ambient_session",
5874
+ "stale_session",
5875
+ "lifecycle_log",
5876
+ "persisted_consent"
5877
+ ]
5878
+ },
5879
+ {
5880
+ "path": "{target}/.campaign-runtime/assembly-report.json",
5881
+ "when": "the refresh runs (outcome refreshed) — identity.spec_hash and identity.spec_material_hash become the CampaignSpec's; each build, Polish and QA record made against other CampaignSpec or brief content becomes required, recording input_change and keeping the record it replaced in its history; evidence[] gains a line",
5882
+ "observed_in": [
5883
+ "no_session",
5884
+ "ambient_session",
5885
+ "stale_session",
5886
+ "lifecycle_log",
5887
+ "persisted_consent"
5888
+ ]
5889
+ },
5890
+ {
5891
+ "path": "{target}/.campaign-runtime/doctor-output.json",
5892
+ "when": "a refresh runs (outcome refreshed) and a retained doctor sidecar exists — the write postdates the doctor snapshot, so it is stamped stale",
5893
+ "observed_in": [
5894
+ "no_session",
5895
+ "ambient_session",
5896
+ "stale_session",
5897
+ "lifecycle_log",
5898
+ "persisted_consent"
5899
+ ]
5900
+ },
5901
+ {
5902
+ "path": "{lifecycle-journal}",
5903
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG — on every outcome, an unchanged or refused refresh included; only --dry-run, and a flag refused before the command runs (such as an unknown flag), append no entry",
5904
+ "observed_in": [
5905
+ "ambient_session",
5906
+ "lifecycle_log"
5907
+ ]
5908
+ }
5909
+ ],
5910
+ "sends": [],
5911
+ "effect_test": "effects: record spec",
5912
+ "test_scope": "full",
5913
+ "notes": "Destructive: build, Polish and QA records made against earlier CampaignSpec or brief content become owed again, each moving to that stage's history, which keeps the last five per stage and is never used as current proof. Waivers, warning accepts, the applied theme and deploy settings are kept. Idempotent: a repeat reads unchanged. When the bound CampaignSpec material is the spec's and no stage's recorded CampaignSpec or brief content differs from the current inputs it reads unchanged and writes nothing but the lifecycle entry. The Build Packet is read, not written; the brief is re-read from the brief file last saved by intake or `record brief`, never rediscovered. For a saved-Map or gateway packet it compares the copy intake fetched and sends nothing. Refused, writing nothing but the lifecycle entry, when the CampaignSpec cannot be read as a JSON object, names another campaign (local_spec_id or map_id) than the Build Packet or the Assembly Report binds, or its active pages (ids, order or routes) changed (that needs intake), or when that brief file is over 1 MiB or names more than 512 design_authority entries, names a file Campaigns OS writes itself, or is missing or not a readable regular file."
5914
+ },
5915
+ {
5916
+ "command": "record",
5917
+ "subcommand": "spec",
5918
+ "flags": [
5919
+ "--dry-run"
5920
+ ],
5921
+ "annotations": {
5922
+ "readOnlyHint": true,
5923
+ "destructiveHint": false,
5924
+ "openWorldHint": false,
5925
+ "idempotentHint": true
5926
+ },
5927
+ "tier": "none",
5928
+ "writes": [],
5929
+ "sends": [],
5930
+ "effect_test": "effects: record spec --dry-run",
5931
+ "test_scope": "full",
5932
+ "notes": "record spec --dry-run runs every check the real invocation runs and prints the outcome and the files it would write; it writes nothing (no brief, Build Context, report or stale stamp), takes no target lock, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
5933
+ },
5686
5934
  {
5687
5935
  "command": "record",
5688
5936
  "subcommand": "setup",
@@ -5775,7 +6023,7 @@
5775
6023
  "writes": [
5776
6024
  {
5777
6025
  "path": "{target}/.campaign-runtime/assembly-report.json",
5778
- "when": "the record passes every check — stages.assembly becomes completed with build_fingerprint set to doctor's derived.build_output_fingerprint.value, and stages.polish becomes required unless its evidence is bound to that exact output",
6026
+ "when": "the record passes every check — stages.assembly becomes completed with build_fingerprint set to doctor's derived.build_output_fingerprint.value and the brief and CampaignSpec content it was made against, keeping the record it replaced in its history. When the replaced completed record was made against other brief or CampaignSpec content, the record writes input_change and stages.polish and stages.qa become required; otherwise stages.polish becomes required unless its evidence is bound to that exact output. Output identical to the build that change superseded keeps input_change, unless --deviation-reason records the operator's reason (status completed_with_warnings)",
5779
6027
  "observed_in": [
5780
6028
  "no_session",
5781
6029
  "ambient_session",
@@ -5807,7 +6055,7 @@
5807
6055
  "sends": [],
5808
6056
  "effect_test": "effects: record build",
5809
6057
  "test_scope": "full",
5810
- "notes": "Destructive: it replaces stages.assembly and, when the output changed, resets stages.polish to required. The fingerprint is doctor's, never typed. Refused, writing nothing, when next answers prepare-build, when doctor cannot compute the fingerprint (no built output), when setup is still required or stages.setup is not terminal, or when the report would fail its schema. The Build Context is read, not written."
6058
+ "notes": "Destructive: it replaces stages.assembly (the replaced record is kept in its history) and, when the output changed, resets stages.polish to required, and a completed stages.deploy stamped with other output to required, its prior record kept in its history (a deploy with no stamped build is kept). When it detects a brief or CampaignSpec change since the replaced record, it writes input_change and also makes Polish and QA required. Re-recording output identical to the build an input change superseded keeps the build owed unless --deviation-reason records the operator's reason. The fingerprint is doctor's, never typed. Refused, writing nothing but the lifecycle entry, when next answers prepare-build, when doctor cannot compute the fingerprint (no built output), when setup is still required or stages.setup is not terminal, or when the report would fail its schema. The Build Context is read, not written."
5811
6059
  },
5812
6060
  {
5813
6061
  "command": "record",
@@ -5842,7 +6090,7 @@
5842
6090
  "writes": [
5843
6091
  {
5844
6092
  "path": "{target}/.campaign-runtime/assembly-report.json",
5845
- "when": "the record passes every check — stages.polish becomes the file's status (completed, blocked with blockers, or skipped with skip_reason) and evidence, bound to doctor's current fingerprint; theme.repair_loop_defect when the file sets it",
6093
+ "when": "the record passes every check — stages.polish becomes the file's status (completed, blocked with blockers, or skipped with skip_reason) and evidence, bound to doctor's current fingerprint and, for a completed status, the brief and CampaignSpec content it was made against, keeping the record it replaced in its history; theme.repair_loop_defect when the file sets it",
5846
6094
  "observed_in": [
5847
6095
  "no_session",
5848
6096
  "ambient_session",
@@ -5874,7 +6122,7 @@
5874
6122
  "sends": [],
5875
6123
  "effect_test": "effects: record polish",
5876
6124
  "test_scope": "full",
5877
- "notes": "Destructive: it replaces stages.polish (the captured page_load is kept). Refused, writing nothing, when build is not recorded for the current output, when the --evidence file has a shape error (named by field), or, for a completed status, when the polish gate doctor evaluates would not pass on the report it would write. The Build Context is read, not written."
6125
+ "notes": "Destructive: it replaces stages.polish (the captured page_load is kept, and the replaced record is kept in its history). A completed record stamps the brief and CampaignSpec content it was made against; after an input change it stays owed until a new capture postdates that change. Refused, writing nothing but the lifecycle entry, when build is not recorded for the current output or build is owed again after a brief or CampaignSpec change or its record does not say which content it was made against, when the --evidence file has a shape error (named by field), or, for a completed status, when the polish gate doctor evaluates would not pass on the report it would write. The Build Context is read, not written."
5878
6126
  },
5879
6127
  {
5880
6128
  "command": "record",
@@ -5941,7 +6189,7 @@
5941
6189
  "sends": [],
5942
6190
  "effect_test": "effects: record theme",
5943
6191
  "test_scope": "full",
5944
- "notes": "Destructive: it replaces report.theme's status, css_path, load_order, commerce_pages, evidence and waiver. Refused, writing nothing, when build is not recorded for the current output, when the campaign ships no commerce pages, when a built commerce page that loads next-core.css does not load brand-theme.css (or checkout-brand.css) after it, or links one that is not in the built output, or when no built commerce page loads next-core.css. The Build Context is read, not written."
6192
+ "notes": "Destructive: it replaces report.theme's status, css_path, load_order, commerce_pages, evidence and waiver. Refused, writing nothing but the lifecycle entry, when build is not recorded for the current output or build is owed again after a brief or CampaignSpec change or its record does not say which content it was made against, when the campaign ships no commerce pages, when a built commerce page that loads next-core.css does not load brand-theme.css (or checkout-brand.css) after it, or links one that is not in the built output, or when no built commerce page loads next-core.css. The Build Context is read, not written."
5945
6193
  },
5946
6194
  {
5947
6195
  "command": "record",
@@ -6033,7 +6281,7 @@
6033
6281
  ],
6034
6282
  "effect_test": "effects: record deploy",
6035
6283
  "test_scope": "full",
6036
- "notes": "Destructive: it replaces stages.deploy and the packet's deploy.preview_url. The requests stay on this machine: --base-url must be a loopback origin. Refused, writing nothing, when the packet is not local-serve, the URL is not loopback or not 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. The Build Context is read, not written."
6284
+ "notes": "Destructive: it replaces stages.deploy, stamped with the build fingerprint it probed, and the packet's deploy.preview_url. The requests stay on this machine: --base-url must be a loopback origin. Refused, writing nothing but the lifecycle entry, when the packet is not local-serve, the URL is not loopback or not the campaign's route root, a built page does not answer 2xx, polish is not recorded, build or Polish is owed again after a brief or CampaignSpec change or its record does not say which content it was made against, the built output changed since build was recorded, or the theme gate is blocked. The Build Context is read, not written."
6037
6285
  },
6038
6286
  {
6039
6287
  "command": "record",