@nextcommerce/campaigns-os 1.50.0 → 1.53.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 (87) hide show
  1. package/CHANGELOG.md +474 -0
  2. package/agents/claude/CLAUDE.md +4 -2
  3. package/agents/codex/AGENTS.md +3 -1
  4. package/agents/copilot/copilot-instructions.md +3 -1
  5. package/agents/cursor/campaigns-os.mdc +3 -1
  6. package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
  7. package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
  8. package/campaign-spec/dist/rules/design-source-shape.js +13 -3
  9. package/campaign-spec/dist/rules/sdk-version.js +2 -1
  10. package/compatibility.json +1 -1
  11. package/contracts/commerce-surface-catalog.json +26 -46
  12. package/contracts/effects.v1.json +282 -14
  13. package/contracts/release-ledger.json +1233 -0
  14. package/contracts/supported-surface.json +2 -2
  15. package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
  16. package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
  17. package/docs/brand-theme-bridge.md +2 -0
  18. package/docs/build-packet.md +148 -23
  19. package/docs/campaign-build-brief.md +57 -2
  20. package/docs/effects.md +14 -0
  21. package/docs/local-setup.md +1 -1
  22. package/docs/orientation-contract-reference.md +1 -1
  23. package/docs/polish-evidence.md +12 -0
  24. package/docs/progress-snapshots.md +2 -4
  25. package/docs/qa-and-test-orders.md +47 -4
  26. package/docs/readback.md +19 -2
  27. package/docs/runtime-readiness.md +1 -1
  28. package/docs/sdk-storage-compatibility.md +1 -1
  29. package/docs/skills-revision.md +10 -10
  30. package/package.json +1 -1
  31. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  32. package/skills/campaign-readback-classification/SKILL.md +3 -3
  33. package/skills/campaign-run-evidence/SKILL.md +3 -3
  34. package/skills/contribution-intake/SKILL.md +3 -3
  35. package/skills/next-campaigns-build/SKILL.md +5 -3
  36. package/skills/next-campaigns-os/SKILL.md +11 -20
  37. package/skills/next-campaigns-os/references/session-intake.md +17 -9
  38. package/skills/next-campaigns-os-setup/SKILL.md +4 -3
  39. package/skills/next-campaigns-polish/SKILL.md +7 -5
  40. package/skills/next-campaigns-qa/SKILL.md +8 -5
  41. package/skills.json +10 -10
  42. package/src/adapter-decision-contract.mjs +1 -1
  43. package/src/brand-theme.mjs +33 -20
  44. package/src/brief-summary.mjs +275 -0
  45. package/src/build-brief.mjs +229 -30
  46. package/src/built-site-scope.mjs +39 -6
  47. package/src/built-smoke-qc.mjs +1117 -0
  48. package/src/campaign-identity.mjs +36 -2
  49. package/src/cart-placeholders.mjs +730 -0
  50. package/src/cli.mjs +467 -143
  51. package/src/commercial-journey.mjs +65 -4
  52. package/src/commercial-parity.mjs +6 -1
  53. package/src/contrast.mjs +649 -0
  54. package/src/design-source-publication.mjs +110 -3
  55. package/src/doctor/checks.mjs +415 -33
  56. package/src/doctor/inspect.mjs +53 -2
  57. package/src/doctor/next-step.mjs +16 -7
  58. package/src/input-currency.mjs +394 -0
  59. package/src/input-refresh.mjs +708 -0
  60. package/src/invocation.mjs +5 -2
  61. package/src/local-preview-policy.mjs +1 -1
  62. package/src/local-proof.mjs +4 -1
  63. package/src/polish-browser.mjs +660 -141
  64. package/src/polish-capture.mjs +1 -1
  65. package/src/polish-deadline.mjs +7 -0
  66. package/src/polish-media-weight.mjs +492 -0
  67. package/src/polish-node.mjs +400 -28
  68. package/src/polish-readability.mjs +828 -0
  69. package/src/progress-node.mjs +19 -3
  70. package/src/qa-browser.mjs +439 -180
  71. package/src/qa-content-params.mjs +889 -0
  72. package/src/qa-node.mjs +116 -13
  73. package/src/qa-order-bump.mjs +22 -1
  74. package/src/qa-policy-links.mjs +1019 -0
  75. package/src/qa-tracking-params.mjs +1389 -0
  76. package/src/qa-url-privacy.mjs +168 -0
  77. package/src/qc-accept.mjs +482 -0
  78. package/src/qc-check-registry.mjs +91 -0
  79. package/src/qc-results.mjs +1341 -0
  80. package/src/readback.mjs +58 -10
  81. package/src/sdk-attribute-index.mjs +71 -0
  82. package/src/sdk-markup.mjs +2 -2
  83. package/src/sdk-storage-compatibility.mjs +63 -3
  84. package/src/source-prep.mjs +37 -7
  85. package/src/stage-ledger.mjs +235 -44
  86. package/src/stage-record.mjs +165 -33
  87. package/src/tooling-setup.mjs +1 -1
@@ -362,6 +362,29 @@
362
362
  "test_scope": "full",
363
363
  "notes": "qa publish --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout. Local-spec packets are refused before publication because they have no saved Map destination."
364
364
  },
365
+ {
366
+ "command": "checkpoint",
367
+ "subcommand": "accept",
368
+ "flags": [
369
+ "--dry-run"
370
+ ],
371
+ "annotations": {
372
+ "readOnlyHint": true,
373
+ "destructiveHint": false,
374
+ "openWorldHint": false,
375
+ "idempotentHint": true
376
+ },
377
+ "tier": "none",
378
+ "writes": [],
379
+ "sends": [],
380
+ "effect_test": "effects: checkpoint accept --dry-run (preflight)",
381
+ "test_scope": "preflight",
382
+ "notes": "checkpoint accept --dry-run runs every check the real invocation runs and prints the accepts it would record; it writes nothing under the target, does not mark the doctor sidecar stale, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout. PREFLIGHT-PROVED: doctor, Polish and QA checks produce accept-eligible warnings, but the offline fixture names a synthetic result ref that matches no current result, so its dry run is refused (changed_since_handoff) before it reaches an accept that passes its checks; the case proves the refused dry run writes nothing and contacts nothing.",
383
+ "preflight": {
384
+ "may_write": [],
385
+ "may_contact": []
386
+ }
387
+ },
365
388
  {
366
389
  "command": "checkpoint",
367
390
  "subcommand": "waive",
@@ -2731,11 +2754,19 @@
2731
2754
  "requires_consent": false,
2732
2755
  "observed_in": [],
2733
2756
  "not_observed_reason": "the offline fixture is blocked at the theme/proof gate before a served page is read, so there is no page to compare and the live campaign is not read"
2757
+ },
2758
+ {
2759
+ "destination": "the http(s) store policy URLs the CampaignSpec configures (campaign.store_terms, campaign.store_privacy, campaign.store_returns, campaign.store_shipping, and campaign.store_contact when it is an http(s) URL), and the redirects they answer with; a mailto: campaign.store_contact is checked for presence only and is never requested",
2760
+ "what": "GET requests that read headers only: at most 5 configured URLs, at most 6 requests per URL (the URL and up to 5 redirects), at most 30 per run",
2761
+ "when": "--browser runs the page checks and the CampaignSpec configures at least one http(s) store policy URL",
2762
+ "requires_consent": false,
2763
+ "observed_in": [],
2764
+ "not_observed_reason": "the offline fixture's browser run is blocked at the gate before any page is checked, so no store policy URL is requested"
2734
2765
  }
2735
2766
  ],
2736
2767
  "effect_test": "effects: qa run --browser (preflight)",
2737
2768
  "test_scope": "preflight",
2738
- "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).",
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.",
2739
2770
  "preflight": {
2740
2771
  "may_write": [
2741
2772
  "{target}/qa-output/**",
@@ -3357,11 +3388,19 @@
3357
3388
  "observed_in": [
3358
3389
  "persisted_consent"
3359
3390
  ]
3391
+ },
3392
+ {
3393
+ "destination": "the http(s) store policy URLs the CampaignSpec configures (campaign.store_terms, campaign.store_privacy, campaign.store_returns, campaign.store_shipping, and campaign.store_contact when it is an http(s) URL), and the redirects they answer with; a mailto: campaign.store_contact is checked for presence only and is never requested",
3394
+ "what": "GET requests that read headers only: at most 5 configured URLs, at most 6 requests per URL (the URL and up to 5 redirects), at most 30 per run",
3395
+ "when": "--browser runs the page checks and the CampaignSpec configures at least one http(s) store policy URL",
3396
+ "requires_consent": false,
3397
+ "observed_in": [],
3398
+ "not_observed_reason": "the offline fixture's browser run is blocked at the gate before any page is checked, so no store policy URL is requested"
3360
3399
  }
3361
3400
  ],
3362
3401
  "effect_test": "effects: qa run --browser --no-live-refs (preflight)",
3363
3402
  "test_scope": "preflight",
3364
- "notes": "PREFLIGHT-PROVED like `qa run --browser`: no installed Chromium or reachable campaign offline. The `qa run --browser` row's writes and sends, without the {proxy-base}/api/campaign read: the verdict records the live-campaign-refs check skipped with reason disabled, never a pass. The other sends keep their own conditions and opt-outs.",
3403
+ "notes": "PREFLIGHT-PROVED like `qa run --browser`: no installed Chromium or reachable campaign offline. The `qa run --browser` row's writes and sends, without the {proxy-base}/api/campaign read: the verdict records the live-campaign-refs check skipped with reason disabled, never a pass. The other sends keep their own conditions and opt-outs. The header-only GETs to the configured http(s) store policy URLs are sent as in the `qa run --browser` row, and offline none is sent.",
3365
3404
  "preflight": {
3366
3405
  "may_write": [
3367
3406
  "{target}/qa-output/**",
@@ -4076,6 +4115,46 @@
4076
4115
  ]
4077
4116
  }
4078
4117
  },
4118
+ {
4119
+ "command": "checkpoint",
4120
+ "subcommand": "accept",
4121
+ "flags": [],
4122
+ "annotations": {
4123
+ "readOnlyHint": false,
4124
+ "destructiveHint": false,
4125
+ "openWorldHint": false,
4126
+ "idempotentHint": false
4127
+ },
4128
+ "tier": "B",
4129
+ "writes": [
4130
+ {
4131
+ "path": "{target}/.campaign-runtime/assembly-report.json",
4132
+ "when": "an accept is recorded — every listed result is a current accept-eligible warning that was on record before the command; the accepts are appended to qc_accepts[] and evidence[], and nothing already there is replaced",
4133
+ "observed_in": [],
4134
+ "not_observed_reason": "the offline fixture names a synthetic result ref that matches no current result, so its accept is refused (changed_since_handoff) before the one write; the case proves the refusal writes nothing"
4135
+ },
4136
+ {
4137
+ "path": "{target}/.campaign-runtime/doctor-output.json",
4138
+ "when": "an accept is recorded; it marks the retained doctor sidecar stale",
4139
+ "observed_in": [],
4140
+ "not_observed_reason": "the offline fixture names a synthetic result ref that matches no current result, so its accept is refused (changed_since_handoff) before the one write; the case proves the refusal writes nothing"
4141
+ },
4142
+ {
4143
+ "path": "{lifecycle-journal}",
4144
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG; the entry records the attempt and its exit status, whether the accept was recorded or the handler failed (such as on malformed packet JSON). A refused invocation journals nothing: a typed accept refusal (any refusal_code), or any other refusal the command raises — a missing, repeated or malformed flag, a missing assembly report, or a qc_accepts or evidence value that is not an array",
4145
+ "observed_in": [],
4146
+ "not_observed_reason": "the offline fixture reaches neither a recorded accept nor a handler failure: it names a synthetic result ref that matches no current result, so its accept is refused (changed_since_handoff), and the case proves that refusal journals nothing"
4147
+ }
4148
+ ],
4149
+ "sends": [],
4150
+ "effect_test": "effects: checkpoint accept (preflight)",
4151
+ "test_scope": "preflight",
4152
+ "notes": "Tier B, not C: tier C means the invocation overwrites, clears or discards state that was already there, and `checkpoint waive` is tier C because a waiver overrides a blocking gate. An accept appends, overrides no gate and changes no readiness, and the doctor-sidecar stale stamp is the same write `theme generate` makes at tier B. Not idempotent: a repeat appends another record. It reads only what is already on disk and sends nothing. PREFLIGHT-PROVED: doctor, Polish and QA checks produce accept-eligible warnings, but the offline fixture names a synthetic result ref that matches no current result, so its accept is refused (changed_since_handoff) before the write; the case proves the refused accept writes nothing — no report, no stale stamp, no journal entry — and contacts nothing.",
4153
+ "preflight": {
4154
+ "may_write": [],
4155
+ "may_contact": []
4156
+ }
4157
+ },
4079
4158
  {
4080
4159
  "command": "checkpoint",
4081
4160
  "subcommand": "waive",
@@ -4327,7 +4406,7 @@
4327
4406
  ],
4328
4407
  "effect_test": "effects: start",
4329
4408
  "test_scope": "full",
4330
- "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."
4409
+ "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."
4331
4410
  },
4332
4411
  {
4333
4412
  "command": "start",
@@ -4486,7 +4565,7 @@
4486
4565
  ],
4487
4566
  "effect_test": "effects: start --force",
4488
4567
  "test_scope": "full",
4489
- "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."
4568
+ "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."
4490
4569
  },
4491
4570
  {
4492
4571
  "command": "start",
@@ -4752,7 +4831,7 @@
4752
4831
  ],
4753
4832
  "effect_test": "effects: prepare-build",
4754
4833
  "test_scope": "full",
4755
- "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`."
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`. 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."
4756
4835
  },
4757
4836
  {
4758
4837
  "command": "prepare-build",
@@ -4998,7 +5077,7 @@
4998
5077
  ],
4999
5078
  "effect_test": "effects: prepare-build --force",
5000
5079
  "test_scope": "full",
5001
- "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."
5080
+ "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."
5002
5081
  },
5003
5082
  {
5004
5083
  "command": "build",
@@ -5144,7 +5223,7 @@
5144
5223
  ],
5145
5224
  "effect_test": "effects: build",
5146
5225
  "test_scope": "full",
5147
- "notes": "The intake alias for prepare-build + doctor: the same packet and sidecars, plus the doctor sidecar. It does not install the agent context."
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. 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."
5148
5227
  },
5149
5228
  {
5150
5229
  "command": "build",
@@ -5412,7 +5491,7 @@
5412
5491
  ],
5413
5492
  "effect_test": "effects: build --force",
5414
5493
  "test_scope": "full",
5415
- "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."
5494
+ "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."
5416
5495
  },
5417
5496
  {
5418
5497
  "command": "spec",
@@ -5604,6 +5683,195 @@
5604
5683
  "may_contact": []
5605
5684
  }
5606
5685
  },
5686
+ {
5687
+ "command": "record",
5688
+ "subcommand": "brief",
5689
+ "flags": [],
5690
+ "annotations": {
5691
+ "readOnlyHint": false,
5692
+ "destructiveHint": true,
5693
+ "openWorldHint": false,
5694
+ "idempotentHint": true
5695
+ },
5696
+ "tier": "C",
5697
+ "writes": [
5698
+ {
5699
+ "path": "{packet}",
5700
+ "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",
5701
+ "observed_in": [
5702
+ "no_session",
5703
+ "ambient_session",
5704
+ "stale_session",
5705
+ "lifecycle_log",
5706
+ "persisted_consent"
5707
+ ]
5708
+ },
5709
+ {
5710
+ "path": "{target}/.campaign-runtime/input/*",
5711
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the normalized brief, re-derived from the saved brief file",
5712
+ "observed_in": [
5713
+ "no_session",
5714
+ "ambient_session",
5715
+ "stale_session",
5716
+ "lifecycle_log",
5717
+ "persisted_consent"
5718
+ ]
5719
+ },
5720
+ {
5721
+ "path": "{target}/.campaign-runtime/build-context.json",
5722
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the brief summary, its bindings and the brief's prompts",
5723
+ "observed_in": [
5724
+ "no_session",
5725
+ "ambient_session",
5726
+ "stale_session",
5727
+ "lifecycle_log",
5728
+ "persisted_consent"
5729
+ ]
5730
+ },
5731
+ {
5732
+ "path": "{target}/.campaign-runtime/assembly-report.json",
5733
+ "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",
5734
+ "observed_in": [
5735
+ "no_session",
5736
+ "ambient_session",
5737
+ "stale_session",
5738
+ "lifecycle_log",
5739
+ "persisted_consent"
5740
+ ]
5741
+ },
5742
+ {
5743
+ "path": "{target}/.campaign-runtime/doctor-output.json",
5744
+ "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",
5745
+ "observed_in": [
5746
+ "no_session",
5747
+ "ambient_session",
5748
+ "stale_session",
5749
+ "lifecycle_log",
5750
+ "persisted_consent"
5751
+ ]
5752
+ },
5753
+ {
5754
+ "path": "{lifecycle-journal}",
5755
+ "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",
5756
+ "observed_in": [
5757
+ "ambient_session",
5758
+ "lifecycle_log"
5759
+ ]
5760
+ }
5761
+ ],
5762
+ "sends": [],
5763
+ "effect_test": "effects: record brief",
5764
+ "test_scope": "full",
5765
+ "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)."
5766
+ },
5767
+ {
5768
+ "command": "record",
5769
+ "subcommand": "brief",
5770
+ "flags": [
5771
+ "--dry-run"
5772
+ ],
5773
+ "annotations": {
5774
+ "readOnlyHint": true,
5775
+ "destructiveHint": false,
5776
+ "openWorldHint": false,
5777
+ "idempotentHint": true
5778
+ },
5779
+ "tier": "none",
5780
+ "writes": [],
5781
+ "sends": [],
5782
+ "effect_test": "effects: record brief --dry-run",
5783
+ "test_scope": "full",
5784
+ "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."
5785
+ },
5786
+ {
5787
+ "command": "record",
5788
+ "subcommand": "spec",
5789
+ "flags": [],
5790
+ "annotations": {
5791
+ "readOnlyHint": false,
5792
+ "destructiveHint": true,
5793
+ "openWorldHint": false,
5794
+ "idempotentHint": true
5795
+ },
5796
+ "tier": "C",
5797
+ "writes": [
5798
+ {
5799
+ "path": "{target}/.campaign-runtime/input/*",
5800
+ "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",
5801
+ "observed_in": [
5802
+ "no_session",
5803
+ "ambient_session",
5804
+ "stale_session",
5805
+ "lifecycle_log",
5806
+ "persisted_consent"
5807
+ ]
5808
+ },
5809
+ {
5810
+ "path": "{target}/.campaign-runtime/build-context.json",
5811
+ "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",
5812
+ "observed_in": [
5813
+ "no_session",
5814
+ "ambient_session",
5815
+ "stale_session",
5816
+ "lifecycle_log",
5817
+ "persisted_consent"
5818
+ ]
5819
+ },
5820
+ {
5821
+ "path": "{target}/.campaign-runtime/assembly-report.json",
5822
+ "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",
5823
+ "observed_in": [
5824
+ "no_session",
5825
+ "ambient_session",
5826
+ "stale_session",
5827
+ "lifecycle_log",
5828
+ "persisted_consent"
5829
+ ]
5830
+ },
5831
+ {
5832
+ "path": "{target}/.campaign-runtime/doctor-output.json",
5833
+ "when": "a refresh runs (outcome refreshed) and a retained doctor sidecar exists — the write postdates the doctor snapshot, so it is stamped stale",
5834
+ "observed_in": [
5835
+ "no_session",
5836
+ "ambient_session",
5837
+ "stale_session",
5838
+ "lifecycle_log",
5839
+ "persisted_consent"
5840
+ ]
5841
+ },
5842
+ {
5843
+ "path": "{lifecycle-journal}",
5844
+ "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",
5845
+ "observed_in": [
5846
+ "ambient_session",
5847
+ "lifecycle_log"
5848
+ ]
5849
+ }
5850
+ ],
5851
+ "sends": [],
5852
+ "effect_test": "effects: record spec",
5853
+ "test_scope": "full",
5854
+ "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."
5855
+ },
5856
+ {
5857
+ "command": "record",
5858
+ "subcommand": "spec",
5859
+ "flags": [
5860
+ "--dry-run"
5861
+ ],
5862
+ "annotations": {
5863
+ "readOnlyHint": true,
5864
+ "destructiveHint": false,
5865
+ "openWorldHint": false,
5866
+ "idempotentHint": true
5867
+ },
5868
+ "tier": "none",
5869
+ "writes": [],
5870
+ "sends": [],
5871
+ "effect_test": "effects: record spec --dry-run",
5872
+ "test_scope": "full",
5873
+ "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."
5874
+ },
5607
5875
  {
5608
5876
  "command": "record",
5609
5877
  "subcommand": "setup",
@@ -5696,7 +5964,7 @@
5696
5964
  "writes": [
5697
5965
  {
5698
5966
  "path": "{target}/.campaign-runtime/assembly-report.json",
5699
- "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",
5967
+ "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)",
5700
5968
  "observed_in": [
5701
5969
  "no_session",
5702
5970
  "ambient_session",
@@ -5728,7 +5996,7 @@
5728
5996
  "sends": [],
5729
5997
  "effect_test": "effects: record build",
5730
5998
  "test_scope": "full",
5731
- "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."
5999
+ "notes": "Destructive: it replaces stages.assembly (the replaced record is kept in its history) and, when the output changed, resets stages.polish to required. 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."
5732
6000
  },
5733
6001
  {
5734
6002
  "command": "record",
@@ -5763,7 +6031,7 @@
5763
6031
  "writes": [
5764
6032
  {
5765
6033
  "path": "{target}/.campaign-runtime/assembly-report.json",
5766
- "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",
6034
+ "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",
5767
6035
  "observed_in": [
5768
6036
  "no_session",
5769
6037
  "ambient_session",
@@ -5795,7 +6063,7 @@
5795
6063
  "sends": [],
5796
6064
  "effect_test": "effects: record polish",
5797
6065
  "test_scope": "full",
5798
- "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."
6066
+ "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."
5799
6067
  },
5800
6068
  {
5801
6069
  "command": "record",
@@ -5862,7 +6130,7 @@
5862
6130
  "sends": [],
5863
6131
  "effect_test": "effects: record theme",
5864
6132
  "test_scope": "full",
5865
- "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."
6133
+ "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."
5866
6134
  },
5867
6135
  {
5868
6136
  "command": "record",
@@ -5954,7 +6222,7 @@
5954
6222
  ],
5955
6223
  "effect_test": "effects: record deploy",
5956
6224
  "test_scope": "full",
5957
- "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."
6225
+ "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 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."
5958
6226
  },
5959
6227
  {
5960
6228
  "command": "record",