@nextcommerce/campaigns-os 1.43.1 → 1.43.2

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 (56) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +451 -0
  3. package/README.md +2 -2
  4. package/contracts/agent-relevant-change-policy.v1.json +5 -0
  5. package/contracts/effects.v1.json +8 -8
  6. package/contracts/release-ledger.json +672 -0
  7. package/contracts/supported-surface.json +2 -2
  8. package/docs/build-packet.md +64 -2
  9. package/docs/campaigns-os-build-flow.md +1 -0
  10. package/docs/design-source-package.md +89 -15
  11. package/docs/effects.md +16 -4
  12. package/docs/local-setup.md +1 -1
  13. package/docs/orientation-contract-reference.md +1 -1
  14. package/docs/progress-snapshots.md +10 -6
  15. package/docs/qa-and-test-orders.md +131 -7
  16. package/docs/release-ledger-authoring-guide.md +6 -4
  17. package/docs/runtime-readiness.md +1 -1
  18. package/docs/skills-revision.md +10 -10
  19. package/package.json +1 -1
  20. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  21. package/skills/campaign-readback-classification/SKILL.md +3 -3
  22. package/skills/campaign-run-evidence/SKILL.md +3 -3
  23. package/skills/contribution-intake/SKILL.md +3 -3
  24. package/skills/next-campaigns-build/SKILL.md +4 -4
  25. package/skills/next-campaigns-os/SKILL.md +3 -3
  26. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  27. package/skills/next-campaigns-polish/SKILL.md +3 -3
  28. package/skills/next-campaigns-qa/SKILL.md +3 -3
  29. package/skills.json +10 -10
  30. package/src/build-brief.mjs +6 -4
  31. package/src/built-script-syntax.mjs +480 -0
  32. package/src/campaigns-api-key.mjs +99 -0
  33. package/src/cli-helpers.mjs +118 -0
  34. package/src/cli.mjs +1211 -7495
  35. package/src/design-source-package.mjs +1 -1
  36. package/src/design-source-publication.mjs +898 -0
  37. package/src/diagnostic.mjs +2 -1
  38. package/src/directory-lock.mjs +270 -0
  39. package/src/doctor/checks.mjs +4415 -0
  40. package/src/doctor/inspect.mjs +636 -0
  41. package/src/doctor/next-step.mjs +731 -0
  42. package/src/install-invocation.mjs +29 -0
  43. package/src/invocation.mjs +179 -0
  44. package/src/private-template-source.mjs +1 -1
  45. package/src/progress-node.mjs +6 -35
  46. package/src/proof-policy.mjs +1 -1
  47. package/src/qa-analytics-correctness.mjs +3 -0
  48. package/src/qa-binding-evidence.mjs +76 -11
  49. package/src/qa-browser.mjs +778 -77
  50. package/src/qa-build-scope.mjs +47 -0
  51. package/src/qa-node.mjs +218 -13
  52. package/src/source-html-intake.mjs +1 -1
  53. package/src/source-html-manifest.mjs +9 -2
  54. package/src/stage-ledger.mjs +28 -0
  55. package/src/target-lock.mjs +54 -0
  56. package/src/template-brand-contract.mjs +17 -1
@@ -16,7 +16,7 @@ that the copy on disk moved.
16
16
  `skills.json` carries one top-level field:
17
17
 
18
18
  ```json
19
- "bundle_revision": "1.43.1+skills.1"
19
+ "bundle_revision": "1.43.2+skills.1"
20
20
  ```
21
21
 
22
22
  The spelling is `<package version>+skills.<n>`:
@@ -25,7 +25,7 @@ The spelling is `<package version>+skills.<n>`:
25
25
  skills ship with (`check-skill-versions.mjs` fails if the two disagree);
26
26
  - `<n>` is a plain counter, not a semver component. It says "this is the *n*th
27
27
  skill-text revision published against that package version" and it **resets
28
- with the prefix**. `1.43.1+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.43.2+skills.1` is therefore ahead of `1.40.0+skills.7`.
29
29
 
30
30
  It is one identity for the bundle as a whole, on purpose. Per-skill versions
31
31
  still exist and still gate per-skill changes, but an agent that loaded one skill
@@ -37,7 +37,7 @@ The first body line of every bundled `SKILL.md`, immediately after the
37
37
  frontmatter, is exactly:
38
38
 
39
39
  ```
40
- Bundle revision: 1.43.1+skills.1
40
+ Bundle revision: 1.43.2+skills.1
41
41
  ```
42
42
 
43
43
  followed by a short paragraph telling the agent to run the check below at the
@@ -48,7 +48,7 @@ text the agent is actually reading, not from a file it would have to go and open
48
48
  ## The check
49
49
 
50
50
  ```bash
51
- npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1
52
52
  ```
53
53
 
54
54
  The value is compared against the bundle revision of the **CLI the command runs
@@ -90,20 +90,20 @@ reports the choice as `skills.scope` (`requested`, `installed_platforms`, or
90
90
  "revision_check": "match",
91
91
  "skills_revision": {
92
92
  "status": "match",
93
- "requested": "1.43.1+skills.1",
93
+ "requested": "1.43.2+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.43.1+skills.1",
95
+ "on_disk": "1.43.2+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.43.1+skills.1)"
97
+ "message": "match (1.43.2+skills.1)"
98
98
  }
99
99
  ```
100
100
 
101
101
  The text view prints one named line, as a header above the rest of the status:
102
102
 
103
103
  ```
104
- Skills revision: match (1.43.1+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.43.1+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.43.1+skills.1)
104
+ Skills revision: match (1.43.2+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.43.2+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.43.2+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.43.1",
3
+ "version": "1.43.2",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-lifecycle-orientation
3
- version: 1.0.7
3
+ version: 1.0.9
4
4
  description: Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing any stage or changing any state.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-readback-classification
3
- version: 1.0.7
3
+ version: 1.0.9
4
4
  description: Classify a selected campaign from the readback projection's v2 fields and write a read-only handoff without turning diagnosis into permission.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-run-evidence
3
- version: 1.0.7
3
+ version: 1.0.9
4
4
  description: Interpret existing Campaigns OS doctor, QA and proof-depth evidence without claiming more proof than the artifacts contain.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: contribution-intake
3
- version: 1.0.7
3
+ version: 1.0.9
4
4
  description: Turn a suggestion about the agent surface into a classified, evidence-checked proposal and, only with attended approval, one issue on this repository's tracker.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-build
3
- version: 1.0.12
3
+ version: 1.0.14
4
4
  description: Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -84,7 +84,7 @@ Build rules:
84
84
  - Replace values named by `frontmatter.replaceFromSpecOrApi`.
85
85
  - Remove unsupported surfaces named by `frontmatter.removeWhenUnsupported`.
86
86
  - Preserve SDK-owned checkout/cart/upsell/receipt/payment/address/totals/submit surfaces.
87
- - If `doctor` (tier `none`: read-only inspection; `--write`/`--built` are tier `B`) reports `derived.scope.mode = "partial"`, build the pages listed in `derived.scope.built_pages` from their prepared source. A page in `derived.scope.out_of_scope_pages` whose assembly-report decision `dec_page_scope_<page>` carries `template_stock: true` is template stock: materialise it from the locked family's own page for that role (`decision.template_family`; the `next build` prompt lists them), copied atomically with its dependent `_includes/`, `_layouts/`, and assets, and wired from CampaignSpec — a pre-checkout `select` step first, because it seeds the cart the runtime pages read. Do not look for prepared source HTML for it, and do not attest a screenshot of it as a design source. Once its built HTML exists at the page's route, doctor lists it among the previewable routes and lifts the runtime-QA block for it. An out-of-scope page without that marker stays unbuilt: carry its `skip_reason` into the assembly report and label the preview as route/visual-testable rather than full-funnel launch-ready.
87
+ - If `doctor` (tier `none`: read-only inspection; `--write`/`--built` are tier `B`) reports `derived.scope.mode = "partial"`, build the pages listed in `derived.scope.built_pages` from their prepared source. Keep every out-of-scope route unbuilt by default, including pages marked `template_stock: true`; never publish placeholder presell/landing pages that remain on another host. Materialize a stock page only with explicit per-page operator opt-in (including a required pre-checkout `select` stand-in). For opted-in pages use the locked family's own page for that role with its dependent `_includes/`, `_layouts/`, and assets, wire it from CampaignSpec, and build a required select step first. Do not attest stock screenshots as design source. Built stock pages rejoin preview QA once their HTML exists. Carry remaining `skip_reason` declarations into the report and label the preview route/visual-testable rather than full-funnel launch-ready.
88
88
  - For `landing` and `presell` pages, prefer the prepared source HTML when `source_html.pages[].path` points at a real standalone page. Preserve the design/content through a passthrough page-kit layout, inject the SDK loader/config as needed, and repoint CTAs into the CampaignSpec flow. Treat `source_html.pages[].path` and `context.page_map[].source_path` as source provenance. Treat `source_html.pages[].page_kit`, `context.page_map[].page_kit`, and `context.page_map[].output_path` as the Page Kit target file, route, CPK `page_type`, and frontmatter projection.
89
89
  - Prepared source HTML means page-kit-ready markup, not a wholesale Liquid rewrite. Standalone AI/exported HTML should keep page-owned body markup, remove document wrappers, add YAML frontmatter, move shared CSS/assets into the campaign structure, and use Liquid helpers only where page-kit needs campaign-rooted links/assets/includes.
90
90
  - For `checkout`, `upsell`, `downsell`, and `receipt` pages, treat the selected starter-template commerce surface as the SDK contract reference: preserve required `data-next-*` controls, hidden fields, payment/address/totals/submit wiring, and `next_dont_touch` regions. The surrounding HTML wrapper, page composition, imagery, copy hierarchy, and brand layer are campaign/source-owned. Do not carry starter visual chrome forward when prepared source design should own that surface.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os
3
- version: 1.0.27
3
+ version: 1.0.29
4
4
  description: Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os-setup
3
- version: 2.0.10
3
+ version: 2.0.12
4
4
  description: Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly installed as next-campaigns-setup; renamed 2026-08 to stop colliding with the published NextCommerceCo/skills scaffolder of that name.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-polish
3
- version: 1.1.11
3
+ version: 1.1.13
4
4
  description: Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-qa
3
- version: 1.3.11
3
+ version: 1.3.13
4
4
  description: Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof.
5
5
  ---
6
6
 
7
- Bundle revision: 1.43.1+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.1+skills.1`
7
+ Bundle revision: 1.43.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.43.2+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
package/skills.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "campaigns-os-skills",
3
3
  "description": "Skills bundled with Campaigns OS. `skills.sh` installs these into the shared agent skill directories (~/.claude/skills, ~/.codex/skills), so each one is a versioned package: bump the version whenever a package changes. `bundle_revision` identifies the bundle as a whole — `<package version>+skills.<n>`, where the prefix is this package's version and `<n>` counts the skill-text revisions published against it. It advances whenever ANY bundled skill changes, every SKILL.md states it on its first body line, and `campaigns-os tooling status --skills-revision <value>` compares the value an agent read from a skill against the bundle on disk. See docs/skills-revision.md.",
4
- "bundle_revision": "1.43.1+skills.1",
4
+ "bundle_revision": "1.43.2+skills.1",
5
5
  "homepage": "https://github.com/NextCommerceCo/campaigns-os",
6
6
  "retired_skills": [
7
7
  {
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "id": "next-campaigns-os",
18
18
  "name": "Campaigns OS Lifecycle",
19
- "version": "1.0.27",
19
+ "version": "1.0.29",
20
20
  "path": "skills/next-campaigns-os/SKILL.md",
21
21
  "domain": "campaigns",
22
22
  "description": "Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth."
@@ -24,7 +24,7 @@
24
24
  {
25
25
  "id": "next-campaigns-os-setup",
26
26
  "name": "Campaigns OS Setup",
27
- "version": "2.0.10",
27
+ "version": "2.0.12",
28
28
  "path": "skills/next-campaigns-os-setup/SKILL.md",
29
29
  "domain": "campaigns",
30
30
  "description": "Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly next-campaigns-setup; renamed to release that name to the published NextCommerceCo/skills scaffolder."
@@ -32,7 +32,7 @@
32
32
  {
33
33
  "id": "next-campaigns-build",
34
34
  "name": "Campaign Build",
35
- "version": "1.0.12",
35
+ "version": "1.0.14",
36
36
  "path": "skills/next-campaigns-build/SKILL.md",
37
37
  "domain": "campaigns",
38
38
  "description": "Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts."
@@ -40,7 +40,7 @@
40
40
  {
41
41
  "id": "next-campaigns-polish",
42
42
  "name": "Campaign Polish",
43
- "version": "1.1.11",
43
+ "version": "1.1.13",
44
44
  "path": "skills/next-campaigns-polish/SKILL.md",
45
45
  "domain": "campaigns",
46
46
  "description": "Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign."
@@ -48,7 +48,7 @@
48
48
  {
49
49
  "id": "next-campaigns-qa",
50
50
  "name": "Campaign QA",
51
- "version": "1.3.11",
51
+ "version": "1.3.13",
52
52
  "path": "skills/next-campaigns-qa/SKILL.md",
53
53
  "domain": "campaigns",
54
54
  "description": "Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof."
@@ -56,7 +56,7 @@
56
56
  {
57
57
  "id": "campaign-lifecycle-orientation",
58
58
  "name": "Campaign Lifecycle Orientation",
59
- "version": "1.0.7",
59
+ "version": "1.0.9",
60
60
  "path": "skills/campaign-lifecycle-orientation/SKILL.md",
61
61
  "domain": "campaigns",
62
62
  "description": "Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing their state: the shared vocabulary, doctor as a gate, the stage record, and the store-theme/Page Kit two-worlds trap."
@@ -64,7 +64,7 @@
64
64
  {
65
65
  "id": "campaign-run-evidence",
66
66
  "name": "Campaign Run Evidence",
67
- "version": "1.0.7",
67
+ "version": "1.0.9",
68
68
  "path": "skills/campaign-run-evidence/SKILL.md",
69
69
  "domain": "campaigns",
70
70
  "description": "Interpret existing doctor, QA verdict and proof-depth evidence without claiming more proof than the artifacts contain, and keep funnel proof apart from merchant launch readiness."
@@ -72,7 +72,7 @@
72
72
  {
73
73
  "id": "campaign-readback-classification",
74
74
  "name": "Campaign Readback Classification",
75
- "version": "1.0.7",
75
+ "version": "1.0.9",
76
76
  "path": "skills/campaign-readback-classification/SKILL.md",
77
77
  "domain": "campaigns",
78
78
  "description": "Classify a selected campaign from the campaigns-os-readback/v2 fields (artifacts, staleness.stale_keys, clean, doctor, divergences, skip_cascades) and write a read-only handoff without turning diagnosis into permission."
@@ -80,7 +80,7 @@
80
80
  {
81
81
  "id": "contribution-intake",
82
82
  "name": "Contribution Intake",
83
- "version": "1.0.7",
83
+ "version": "1.0.9",
84
84
  "path": "skills/contribution-intake/SKILL.md",
85
85
  "domain": "campaigns",
86
86
  "description": "Turn a suggestion about the agent surface into a classified, evidence-checked and redacted proposal, filed on this repository tracker only with attended approval."
@@ -170,7 +170,7 @@ export function createCampaignBuildBriefArtifact({
170
170
  const loaded = inputPath ? loadCampaignBuildBriefFile(inputPath) : null;
171
171
  const mode = loaded ? "prepared" : "guided_draft";
172
172
  const baseBrief = loaded
173
- ? cloneJson(loaded.value)
173
+ ? cloneJsonOrEmpty(loaded.value)
174
174
  : draftCampaignBuildBrief({ spec, activePages, pageMappings, templateFamily, sourceAssetCrawl });
175
175
 
176
176
  const errors = [];
@@ -306,7 +306,7 @@ export function validateCampaignBuildBriefArtifact(brief, { spec = null } = {})
306
306
  }
307
307
 
308
308
  function normalizeCampaignBuildBrief(value, meta, errors) {
309
- const brief = isObject(value) ? cloneJson(value) : {};
309
+ const brief = isObject(value) ? cloneJsonOrEmpty(value) : {};
310
310
  if (!isObject(value)) {
311
311
  errors.push({ code: "build_brief.type", field: null, message: "Campaign Build Brief must be a YAML/JSON object." });
312
312
  }
@@ -596,7 +596,7 @@ function normalizeQaPolicy(policy = {}) {
596
596
  const value = objectOrEmpty(policy);
597
597
  return {
598
598
  ...value,
599
- enforcement: cloneJson(QA_POLICY_ENFORCEMENT),
599
+ enforcement: cloneJsonOrEmpty(QA_POLICY_ENFORCEMENT),
600
600
  require_desktop_mobile_screenshots: value.require_desktop_mobile_screenshots !== false,
601
601
  require_checkout_flow: value.require_checkout_flow !== false,
602
602
  require_post_purchase_flow: value.require_post_purchase_flow !== false,
@@ -753,7 +753,9 @@ function objectOrEmpty(value) {
753
753
  return isObject(value) ? value : {};
754
754
  }
755
755
 
756
- function cloneJson(value) {
756
+ // Unlike cli-helpers' cloneJson, a nullish input clones to {}: an empty brief
757
+ // file parses to null and is normalized as an empty object.
758
+ function cloneJsonOrEmpty(value) {
757
759
  return JSON.parse(JSON.stringify(value ?? {}));
758
760
  }
759
761