@nextcommerce/campaigns-os 1.60.0 → 1.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +244 -0
  3. package/README.md +86 -438
  4. package/agents/claude/CLAUDE.md +20 -0
  5. package/agents/codex/AGENTS.md +19 -0
  6. package/agents/copilot/copilot-instructions.md +19 -0
  7. package/agents/cursor/campaigns-os.mdc +21 -0
  8. package/campaign-spec/README.md +7 -0
  9. package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
  10. package/campaign-spec/dist/analytics-vendors.js +44 -0
  11. package/campaign-spec/dist/checkout-flow.d.ts +18 -0
  12. package/campaign-spec/dist/checkout-flow.js +28 -0
  13. package/campaign-spec/dist/index.d.ts +4 -2
  14. package/campaign-spec/dist/index.js +5 -1
  15. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
  16. package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
  17. package/campaign-spec/dist/rules/campaign-currencies.d.ts +29 -0
  18. package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
  19. package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
  20. package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
  21. package/campaign-spec/dist/rules/index.d.ts +2 -0
  22. package/campaign-spec/dist/rules/index.js +6 -0
  23. package/campaign-spec/dist/types.d.ts +43 -9
  24. package/compatibility.json +1 -1
  25. package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
  26. package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
  27. package/contracts/commerce-surface-catalog.json +68 -61
  28. package/contracts/effects.v1.json +549 -165
  29. package/contracts/fixtures/runtime-recipe/accept/current.json +3 -0
  30. package/contracts/fixtures/runtime-recipe/accept/minimal.json +3 -0
  31. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  32. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +3 -0
  33. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +3 -0
  34. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +3 -0
  35. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +3 -0
  36. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +3 -0
  37. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +3 -0
  38. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +3 -0
  39. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +3 -0
  40. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +3 -0
  41. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +3 -0
  42. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +3 -0
  43. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +3 -0
  44. package/contracts/release-ledger.json +1767 -0
  45. package/contracts/runtime-recipe.campaigns-os-node-v1.json +3 -0
  46. package/contracts/supported-surface.json +10 -10
  47. package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
  48. package/docs/access-model.md +20 -0
  49. package/docs/brand-theme-bridge.md +62 -3
  50. package/docs/build-packet.md +456 -36
  51. package/docs/campaign-build-brief.md +16 -0
  52. package/docs/campaign-standardization-report.md +4 -4
  53. package/docs/campaigns-os-build-flow.md +112 -2
  54. package/docs/campaignspec-authoring-examples.md +141 -0
  55. package/docs/design-source-package.md +64 -8
  56. package/docs/effects.md +17 -9
  57. package/docs/entry-points.md +188 -0
  58. package/docs/local-setup.md +1 -1
  59. package/docs/orientation-contract-reference.md +1 -1
  60. package/docs/polish-evidence.md +42 -2
  61. package/docs/progress-snapshots.md +7 -4
  62. package/docs/qa-and-test-orders.md +197 -3
  63. package/docs/quickstart.md +438 -0
  64. package/docs/readback.md +154 -6
  65. package/docs/runtime-readiness.md +6 -3
  66. package/docs/sdk-storage-compatibility.md +4 -2
  67. package/docs/skills-revision.md +10 -10
  68. package/docs/source-adapters.md +213 -0
  69. package/package.json +10 -3
  70. package/schemas/campaign-design-source-package.v0.schema.json +8 -0
  71. package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
  72. package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
  73. package/schemas/campaign-spec.v4.schema.json +19 -3
  74. package/schemas/campaign-spec.v5.schema.json +19 -3
  75. package/schemas/campaigns-os-readback.v2.schema.json +238 -1
  76. package/schemas/source-html-manifest.v0.schema.json +3 -0
  77. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  78. package/skills/campaign-readback-classification/SKILL.md +3 -3
  79. package/skills/campaign-run-evidence/SKILL.md +3 -3
  80. package/skills/contribution-intake/SKILL.md +3 -3
  81. package/skills/next-campaigns-build/SKILL.md +5 -4
  82. package/skills/next-campaigns-os/SKILL.md +7 -4
  83. package/skills/next-campaigns-os-setup/SKILL.md +5 -3
  84. package/skills/next-campaigns-polish/SKILL.md +8 -10
  85. package/skills/next-campaigns-qa/SKILL.md +7 -4
  86. package/skills/next-campaigns-sdk-bump/SKILL.md +151 -25
  87. package/skills.json +11 -11
  88. package/src/brand-theme.mjs +415 -9
  89. package/src/brief-summary.mjs +6 -2
  90. package/src/browser-launch.mjs +2 -1
  91. package/src/build-brief.mjs +13 -3
  92. package/src/built-site-scope.mjs +2 -2
  93. package/src/campaign-workspace.mjs +288 -15
  94. package/src/change-scope.mjs +284 -0
  95. package/src/cli.mjs +358 -97
  96. package/src/commercial-journey.mjs +16 -3
  97. package/src/commercial-parity.mjs +15 -7
  98. package/src/content-residue.mjs +4 -2
  99. package/src/design-file-reference.mjs +93 -0
  100. package/src/design-source-package.mjs +91 -1
  101. package/src/design-source-publication.mjs +9 -6
  102. package/src/directory-lock.mjs +73 -19
  103. package/src/doctor/checks.mjs +87 -81
  104. package/src/doctor/ci.mjs +177 -0
  105. package/src/doctor/inspect.mjs +41 -6
  106. package/src/doctor/next-step.mjs +7 -0
  107. package/src/doctor/source-provenance.mjs +74 -0
  108. package/src/doctor-sidecar.mjs +3 -2
  109. package/src/evidence-debt.mjs +159 -0
  110. package/src/finding-cause.mjs +12 -11
  111. package/src/input-refresh.mjs +339 -32
  112. package/src/invocation.mjs +7 -1
  113. package/src/live-campaign-refs.mjs +7 -2
  114. package/src/local-preview-policy.mjs +35 -2
  115. package/src/page-kit-analytics-vendors.mjs +132 -0
  116. package/src/parity/compare.mjs +316 -0
  117. package/src/parity/design-access.mjs +54 -0
  118. package/src/parity/element-parity-record.mjs +569 -0
  119. package/src/parity/probe.mjs +205 -0
  120. package/src/parity/snapshot.mjs +110 -0
  121. package/src/png-header.mjs +87 -0
  122. package/src/polish-browser.mjs +189 -10
  123. package/src/polish-captures-record.mjs +52 -0
  124. package/src/polish-deadline.mjs +14 -0
  125. package/src/polish-gate.mjs +25 -6
  126. package/src/polish-node.mjs +81 -7
  127. package/src/polish-readability.mjs +221 -9
  128. package/src/polish-visual-fixtures.mjs +74 -0
  129. package/src/polish-visual.mjs +613 -0
  130. package/src/progress-node.mjs +3 -2
  131. package/src/qa-analytics-correctness.mjs +14 -2
  132. package/src/qa-analytics-parity.mjs +111 -6
  133. package/src/qa-browser.mjs +40 -4
  134. package/src/qa-commercial-parity.mjs +50 -3
  135. package/src/qa-node.mjs +235 -22
  136. package/src/qa-publish.mjs +9 -8
  137. package/src/qa-sidecar.mjs +16 -4
  138. package/src/qa-vendor-beacons.mjs +232 -0
  139. package/src/qa-vendor-endpoints.mjs +306 -0
  140. package/src/qc-accept.mjs +176 -6
  141. package/src/qc-check-registry.mjs +42 -2
  142. package/src/qc-results.mjs +477 -42
  143. package/src/readback.mjs +113 -21
  144. package/src/route-identity.mjs +10 -0
  145. package/src/run-record.mjs +18 -10
  146. package/src/runtime-state-ignore.mjs +6 -2
  147. package/src/sdk-attribute-index.mjs +11 -11
  148. package/src/sdk-markup.mjs +77 -2
  149. package/src/sdk-storage-compatibility.mjs +115 -6
  150. package/src/source-html-intake.mjs +15 -1
  151. package/src/source-html-manifest.mjs +7 -0
  152. package/src/source-html-server.mjs +129 -0
  153. package/src/spec-derive-campaign.mjs +216 -0
  154. package/src/spec-derive.mjs +473 -53
  155. package/src/spec-identity.mjs +14 -0
  156. package/src/stage-ledger.mjs +40 -1
  157. package/src/stage-record.mjs +100 -17
  158. package/src/target-lock.mjs +11 -1
  159. package/src/theme-gate.mjs +2 -2
  160. package/src/visual-diff.mjs +296 -0
  161. package/src/visual-reference.mjs +126 -0
package/docs/readback.md CHANGED
@@ -8,7 +8,9 @@ most once, plus two fixed Git metadata files (the nearest `.git` entry at the
8
8
  target or an ancestor, and that Git directory's `logs/HEAD` reflog) for the
9
9
  freshness comparison, and the CampaignSpec and normalized Campaign Build Brief
10
10
  the Build Packet names, whose current content decides each stage's effective
11
- status.
11
+ status. It also reads and hashes regular files in the packet's built output
12
+ at `_site/<public_route_slug>/` to assess build-bound evidence debt. Symlinks
13
+ are skipped by the build fingerprint machinery.
12
14
 
13
15
  The safety contract is the point of the command. The readback **writes nothing**
14
16
  under the target — not even a lifecycle journal entry, which every other command
@@ -21,7 +23,7 @@ For the same reason the readback resolves **no run session** and sweeps no stale
21
23
  one. Its `--packet` names the Build Packet to *project*, not a Build Packet to
22
24
  act on, so an active run session — here, at the target, or bound to some other
23
25
  packet entirely — never changes what this command reads or what it exits with.
24
- That also keeps every read bounded: the only reader of a `--packet` file is the
26
+ The only reader of a `--packet` file is the
25
27
  readback's own 32 MiB-bounded one, which refuses an oversized packet as an
26
28
  `unreadable` artifact row rather than loading it.
27
29
 
@@ -41,8 +43,8 @@ campaigns-os readback --example [--json]
41
43
  The default output is the rendered human view. `--json` emits the same
42
44
  projection as one `campaigns-os-readback/v2` object on stdout so a caller can
43
45
  gate on it; `schemas/campaigns-os-readback.v2.schema.json` is that object's
44
- shape, and this document is its prose twin. The two modes share one computation
45
- path: the JSON serializes what the text view already computes and adds no
46
+ shape, including the additive `evidence_debt` field. The two modes share one
47
+ computation path: the JSON serializes what the text view already computes and adds no
46
48
  interpretation the text view does not also carry.
47
49
 
48
50
  ## Exit codes
@@ -118,6 +120,15 @@ STAGES [assembly report; report status: prepared; effective: prepared]
118
120
  deploy pending
119
121
  qa pending
120
122
 
123
+ EVIDENCE DEBT [the readback's own projection layer: recorded build bindings versus built output]
124
+ polish: unknown — evidence currency is unknown (build_output_missing); absence of proof is not no debt
125
+ recorded: unknown; current: unknown
126
+ qa: unknown — evidence currency is unknown (build_output_missing); absence of proof is not no debt
127
+ recorded: unknown; current: unknown
128
+ changes against assembly manifest: unknown (build_output_missing)
129
+ recorded: unknown; current: unknown
130
+ Page changes use the assembly baseline; older stage manifests are not reconstructed.
131
+
121
132
  BUILD CONTEXT [build context; source adapter: html_funnel, status: prepared]
122
133
 
123
134
  DOCTOR [doctor output; status: ready_with_warnings]
@@ -182,6 +193,23 @@ STAGES [assembly report; report status: completed; effective: prepared]
182
193
  },
183
194
  "skip_cascades": [{ "blocked_by": "polish.evidence_incomplete", "families": ["meta-tags"] }],
184
195
  "divergences": [{ "stage": "polish", "assertion_ids": ["polish.evidence_incomplete"] }],
196
+ "evidence_debt": {
197
+ "polish": {
198
+ "status": "unknown", "matches_current_build": null,
199
+ "recorded_build_fingerprint": null, "current_build_fingerprint": null,
200
+ "reason": "build_output_missing"
201
+ },
202
+ "qa": {
203
+ "status": "unknown", "matches_current_build": null,
204
+ "recorded_build_fingerprint": null, "current_build_fingerprint": null,
205
+ "reason": "build_output_missing"
206
+ },
207
+ "changes": {
208
+ "status": "unknown", "reason": "build_output_missing",
209
+ "recorded_build_fingerprint": null, "current_build_fingerprint": null,
210
+ "changed_pages": [], "unmapped_files": [], "extra": [], "missing": [], "changed": []
211
+ }
212
+ },
185
213
  "clean": false
186
214
  }
187
215
  ```
@@ -267,6 +295,20 @@ carries no parseable value — the readback does not choose. It names the
267
295
  candidates on stderr, says `--packet` is required, and exits `2`. No JSON object
268
296
  is emitted in that case.
269
297
 
298
+ **Freshness chooses among runs of one funnel, never among funnels.** A project
299
+ that holds several funnels has one packet per funnel
300
+ (`campaign-runtime-<route>.build.json` beside the default; see
301
+ [Several funnels in one project](build-packet.md#several-funnels-in-one-project)).
302
+ When the valid candidates name more than one `campaign.public_route_slug`, the
303
+ readback refuses the same way, naming each packet with its route, and exits
304
+ `2`. Pass `--packet` to project one funnel. The doctor output, Build Context,
305
+ Assembly Report, QA verdict and findings export rows then read from the runtime
306
+ directory that packet records (`assembly.runtime_dir`), or from
307
+ `.campaign-runtime/` when it records none. A packet whose `assembly.runtime_dir`
308
+ is not its own funnel's directory is refused with exit `2`, since the readback
309
+ cannot tell whose records to show. An explicit `--doctor`, `--context`,
310
+ `--report`, `--qa-verdict` or `--findings` path still wins.
311
+
270
312
  Values are compared at **microsecond precision** — the precision Python's
271
313
  `datetime.fromisoformat` keeps, and the precision the Python readback this
272
314
  command ports compared at; a seventh or later fractional digit is truncated, not
@@ -397,6 +439,106 @@ in first-seen order:
397
439
  Empty when no QA verdict loaded or none of its assertions were skipped. This
398
440
  grouping is the readback's own projection layer.
399
441
 
442
+ ### `evidence_debt`
443
+
444
+ An additive top-level object in `campaigns-os-readback/v2`, emitted in JSON and
445
+ as an **EVIDENCE DEBT** text section. This assessment reads built bytes; it uses
446
+ no timestamps or reflog entries, starts no process, and writes nothing. It
447
+ does not rerun doctor, Polish, or QA and does not reinterpret their verdicts.
448
+
449
+ `polish` and `qa` each carry:
450
+
451
+ - `status`: the closed set `current`, `owed`, `unknown`;
452
+ - `matches_current_build`: fingerprint equality only (`true` or `false`), or `null`
453
+ when either fingerprint is absent;
454
+ - `recorded_build_fingerprint`: the stage's build binding, or `null`;
455
+ - `current_build_fingerprint`: the fingerprint computed from current built
456
+ output with `computeBuildFingerprint`, or `null`;
457
+ - `reason`: `null` for `current`; `build_fingerprint_changed`,
458
+ `stage_required_by_build`, `stage_required_by_brief`,
459
+ `stage_required_by_spec`, or `stage_required` for `owed`; or an explicit
460
+ reason for `unknown`.
461
+
462
+ Polish uses `stages.polish.source_build_fingerprint`, falling back to
463
+ `stages.polish.evidence.source_build_fingerprint`, with the same precedence as
464
+ the Polish gate. QA uses `stages.qa.evidence.qc_build_fingerprint`, falling back
465
+ to `source_build_fingerprint`, and the same build-binding comparison as `next`.
466
+ It does not require uncommitted capture files to be present.
467
+
468
+ `current` means the stage's binding equals the bytes on disk and the assembly
469
+ fingerprint is available and the stage is not recorded as `required`. `owed`
470
+ means that binding differs from the current build, or the lifecycle recorded
471
+ the stage as `required`, even without a stage fingerprint. Its `required_by`
472
+ selects `stage_required_by_build`, `stage_required_by_brief`, or
473
+ `stage_required_by_spec`; an absent or unrecognized source gives
474
+ `stage_required`. Both comparisons work without a manifest, including reports recorded
475
+ before manifests existed. **`unknown` never means no debt.** Missing or unreadable
476
+ built output or a missing assembly fingerprint still makes the affected stage
477
+ explicitly `unknown`; a missing stage fingerprint does too unless the stage is
478
+ `required`. The unknown stage reason codes are
479
+ `packet_unavailable`, `build_output_location_unknown`, `build_output_missing`,
480
+ `build_output_unreadable`, `assembly_build_fingerprint_missing`, and
481
+ `stage_build_fingerprint_missing`.
482
+ When both stage and disk fingerprints are available, `matches_current_build`
483
+ reports their equality even if a missing assembly fingerprint makes status unknown.
484
+ `matches_current_build: true` together with `status: owed` means the lifecycle
485
+ requires the stage again over matching evidence. Consumers decide whether
486
+ evidence is owed from `status`, never from `matches_current_build`.
487
+
488
+ `changes` compares current output to **the retained assembly manifest**, using
489
+ the existing file drift machinery. It carries:
490
+
491
+ - `status`: `known` or `unknown`, with `reason` (`null` when known);
492
+ - `recorded_build_fingerprint` and `current_build_fingerprint`: the assembly
493
+ manifest baseline and the disk fingerprint;
494
+ - `extra`, `missing`, `changed`: complete file path lists from the manifest
495
+ comparison, without the human drift summary's truncation;
496
+ - `changed_pages`: `{ "page_id": "landing", "built_path": "landing/index.html" }`
497
+ entries for added, removed, or modified HTML in the built-site page map;
498
+ - `unmapped_files`: every differing path outside that map, including shared
499
+ CSS, scripts and assets. These changes still make build-bound evidence owed.
500
+
501
+ Paths are relative to `_site/<public_route_slug>/` and sorted; page IDs use
502
+ `resolveBuiltSiteScope`'s route-derived identities (nested routes use hyphens).
503
+ Removed HTML uses that same route/ID projection. Empty lists with
504
+ `changes.status: unknown` do not establish that no pages changed.
505
+ A missing or unusable manifest makes only `changes.status` unknown, with
506
+ `build_manifest_unavailable` as its reason. A manifest is usable only if its
507
+ entries hash to the assembly's recorded fingerprint.
508
+ If the built page map cannot be resolved (for example, output contains assets
509
+ but no HTML), page changes are unknown with `build_output_pages_unresolved`;
510
+ stage bindings still follow the fingerprint comparison.
511
+
512
+ `changes.kind` classifies this drift (`none`, `page_only`,
513
+ `commerce_affecting` or `unknown`) and `changes.reasons` lists why it counts as
514
+ commerce, each `{ "code", "path"? }`. The rule is in
515
+ [what a change owes](campaigns-os-build-flow.md#what-a-change-owes).
516
+
517
+ The assembly baseline is explicit because it need not equal an older stage's
518
+ binding. If a later `record build` replaced that manifest, `changes` can be
519
+ empty while older Polish/QA bindings are still owed. An empty **known** list then means only that
520
+ no files differ from the retained assembly manifest. The pages changed since
521
+ the build a stage reviewed are on that stage instead. An owed stage carries:
522
+
523
+ - `change`: `{ "kind", "since_build_fingerprint", "changed_pages",
524
+ "unmapped_files", "reasons" }`, read from the rebuilds `record build` kept in
525
+ `stages.assembly.build_changes` plus the drift on disk. `kind` is `unknown`
526
+ when that history does not reach the reviewed build (reason
527
+ `change_history_unavailable`) or the drift cannot be read. `null` on a stage
528
+ that is not owed.
529
+ - Polish only, `advisory`: `true` when the change is `page_only` and no brief or
530
+ spec change requires Polish. Advisory Polish is reported; `next`, `doctor
531
+ --ci` and the QA gate do not require it.
532
+ - QA only, `scope` and `owed_pages`: `pages` with the changed pages when the
533
+ change is `page_only` (`qa run --page` pays it), `full` otherwise, `null`
534
+ when QA is not owed.
535
+
536
+ If recording removed a
537
+ stage binding entirely, that stage reads unknown unless its recorded status is
538
+ `required`, which reports owed. `current` is a build-binding
539
+ assessment only; input currency, stage completion, waivers and verdicts remain
540
+ separate.
541
+
400
542
  ### `divergences`
401
543
 
402
544
  Where two artifacts record different states for the same stage: the assembly
@@ -413,7 +555,7 @@ failure lines up with a completed stage. This too is the readback's own layer.
413
555
 
414
556
  ## `clean`
415
557
 
416
- `clean` is true if and only if **all five** of the following hold:
558
+ `clean` is true if and only if **all six** of the following hold:
417
559
 
418
560
  1. **Every artifact the readback found is loaded and recognized.** Formally: no
419
561
  artifact is in state `unreadable` or `unrecognized`. An `absent` artifact
@@ -439,6 +581,12 @@ failure lines up with a completed stage. This too is the readback's own layer.
439
581
  failed to check.
440
582
  4. **`divergences` is empty.**
441
583
  5. **`doctor.error_count` is zero.**
584
+ 6. **Neither `evidence_debt.polish.status` nor `evidence_debt.qa.status` is
585
+ `owed`**, except Polish marked `advisory` for a page-only change. Built
586
+ output that moved since recorded evidence cannot read clean.
587
+ An `unknown` status does not independently make this integrity flag false;
588
+ it remains unknown, never a claim that no evidence is owed. Callers must
589
+ inspect `evidence_debt` even when `clean` is true.
442
590
 
443
591
  Doctor warnings — of either group — do not affect `clean`. Neither does a
444
592
  blocked QA verdict, a blocked assembly report, or a non-empty `skip_cascades`.
@@ -449,7 +597,7 @@ blocked QA verdict, a blocked assembly report, or a non-empty `skip_cascades`.
449
597
  campaign. It means: the readback read every artifact that was there, understood
450
598
  all of them, can show — for every artifact that recorded an age — that none of
451
599
  them is older than the checkout, found no contradiction between them, and saw no
452
- doctor error. Campaigns OS remains the
600
+ doctor error, and found no known build-bound evidence debt. Campaigns OS remains the
453
601
  lifecycle and verdict authority; the readback never reinterprets a verdict.
454
602
 
455
603
  The practical consequence for a caller: `clean: true` says the artifacts are
@@ -8,7 +8,7 @@
8
8
 
9
9
  How a checkout of this repository at one commit becomes a usable installed runtime, and how a consumer decides whether a prepared one is still trustworthy. Everything below is generated from `contracts/runtime-recipe.campaigns-os-node-v1.json`, which is the only authority for these values.
10
10
 
11
- Recipe kind `campaigns-os-node-v1`, revision `1.0.3`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.60.0`.
11
+ Recipe kind `campaigns-os-node-v1`, revision `1.0.3`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.62.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -81,8 +81,9 @@ Not redundant with the install step. Because install runs with lifecycle scripts
81
81
 
82
82
  The complete input set, enumerated explicitly rather than globbed. The compiler's configured include globs are NOT the input set: two root modules enter the compilation transitively through imports from the entry module and are emitted, so a fingerprint derived from the globs would cover 36 of the 38 compiled sources and miss one of the larger emitted surfaces. Test fixtures and the package's own tests are not inputs; nothing under them is emitted. A checker resolves the compiler's actual file list and asserts it equals this enumeration, so this list cannot rot silently.
83
83
 
84
- Fingerprint algorithm `sha256`, over 47 enumerated files:
84
+ Fingerprint algorithm `sha256`, over 50 enumerated files:
85
85
 
86
+ - `campaign-spec/analytics-vendors.ts`
86
87
  - `campaign-spec/analytics-vocabulary.ts`
87
88
  - `campaign-spec/checkout-flow.ts`
88
89
  - `campaign-spec/index.ts`
@@ -91,9 +92,11 @@ Fingerprint algorithm `sha256`, over 47 enumerated files:
91
92
  - `campaign-spec/routing.ts`
92
93
  - `campaign-spec/rules/analytics-contract-shape.ts`
93
94
  - `campaign-spec/rules/assembly-hints-shape.ts`
95
+ - `campaign-spec/rules/campaign-currencies.ts`
94
96
  - `campaign-spec/rules/campaign-metadata.ts`
95
97
  - `campaign-spec/rules/checkout-forward-target.ts`
96
98
  - `campaign-spec/rules/checkout-has-success-url.ts`
99
+ - `campaign-spec/rules/checkout-path-has-packages.ts`
97
100
  - `campaign-spec/rules/checkout-step-reaches-checkout.ts`
98
101
  - `campaign-spec/rules/cycle-detection.ts`
99
102
  - `campaign-spec/rules/design-source-shape.ts`
@@ -137,7 +140,7 @@ The output directory is a build product. It is untracked and git-ignored, no com
137
140
 
138
141
  Directory `campaign-spec/dist`. Committed: `false`. Type entry `campaign-spec/dist/index.d.ts`.
139
142
 
140
- Expected inventory is derived, never listed twice. For every enumerated input under module_source_root whose path ends in .ts, the build is expected to emit campaign-spec/dist/<path relative to module_source_root, with .ts replaced> once per entry in emitted_extensions. The expected inventory is exactly that set: nothing missing, nothing extra. Emitted extensions: `.js`, `.d.ts`. At this revision that derivation yields 86 files.
143
+ Expected inventory is derived, never listed twice. For every enumerated input under module_source_root whose path ends in .ts, the build is expected to emit campaign-spec/dist/<path relative to module_source_root, with .ts replaced> once per entry in emitted_extensions. The expected inventory is exactly that set: nothing missing, nothing extra. Emitted extensions: `.js`, `.d.ts`. At this revision that derivation yields 92 files.
141
144
 
142
145
  ### Mandatory checks
143
146
 
@@ -8,7 +8,7 @@ npx --no-install campaigns-os sdk storage-check --target . --target-sdk 0.4.38 -
8
8
 
9
9
  Omit `--json` for the concise human report. Exit 0 means source-compatible; exit 2 means incompatible or unknown; invalid arguments or manifests exit 1. No merchant files or pins are rewritten. This is independent of doctor's built HTML markup check.
10
10
 
11
- The manifest is generated and owned by Campaign Cart, from its storage registry. The scanner has no independent SDK key list and fetches no remote code. Supply `docs/compatibility/storage-migrations.v1.json` from an unmodified checkout of a Campaign Cart release tag, v0.4.40 or later. The report records the full file SHA-256, SDK version, supported target range, registry/extractor/input digest, and Git provenance. When the supplied bytes equal their repository HEAD blob, provenance is `verified-git-blob` with commit and repository path. Proposed, modified, or copied manifests are labeled `unverified-local-file`; the label never certifies a release. Review/pin SDK provenance separately before acting on findings. A release manifest covers the target range it declares, which may end below its own SDK version: the v0.4.40 manifest, for example, covers 0.4.38 only. Targets outside the manifest's supported SDK range report unknown. A manifest whose SDK version is below its supported maximum is invalid.
11
+ The manifest is generated and owned by Campaign Cart, from its storage registry. The scanner has no independent SDK key list and fetches no remote code. Supply `docs/compatibility/storage-migrations.v1.json` from an unmodified checkout of a Campaign Cart release tag, v0.4.40 or later. The report records the full file SHA-256, SDK version, supported target range, registry/extractor/input digest, and Git provenance. When the supplied bytes equal their repository HEAD blob, provenance is `verified-git-blob` with commit and repository path. Proposed, modified, or copied manifests are labeled `unverified-local-file`; the label never certifies a release. Review/pin SDK provenance separately before acting on findings. A release manifest covers the target range it declares, which may end below its own SDK version: the v0.4.40 manifest, for example, covers 0.4.38 only. Targets outside the manifest's supported SDK range report unknown (exit 2), and the human report says so before anything else: its first line names the range the manifest covers and the requested target it does not, and the count line repeats it. With a v0.4.43 manifest, `--target-sdk 0.4.43` reads `checked against SDK 0.4.38, not the requested target 0.4.43`. `--json` carries `targetCovered: false`. A manifest whose SDK version is below its supported maximum is invalid.
12
12
 
13
13
  `--target` must name the Git root. `--scope` is mandatory: comma-separated literal repository-relative directory/file paths; use `.` only when the complete repository is intended. Add shared JavaScript directories explicitly. `--exclude` uses the same syntax and records explicit exclusions. Archives receive no implicit exemption. Only Git-tracked `.html`, `.htm`, `.js`, `.mjs`, and `.cjs` files in the selected scope are scanned, using current working-tree bytes and per-file digests; untracked files and built dependencies are outside this evidence. A selected HTML page's local script outside the selected files reports unknown. In a Page Kit campaign (a page under `src/<slug>/`), a script src written `{{ 'js/x.js' | campaign_asset }}` resolves to `src/<slug>/assets/js/x.js`, as Page Kit serves it, and a page's frontmatter `scripts:` entries resolve the same way, since a layout's `{% for script in scripts %}` loop loads them through that filter. Any other `campaign_asset` value, a path that resolves outside the assets folder, and frontmatter that is not valid YAML or whose `scripts` is not a list of paths report unknown. Relative scripts affected by an HTML `<base href>` also report unknown; review their actual dependency paths. Remote SDK and third-party scripts are not fetched or analyzed.
14
14
 
@@ -16,7 +16,9 @@ Acorn parses JavaScript ASTs; parse5 identifies executable inline HTML scripts a
16
16
 
17
17
  Dynamic storage keys, unresolved storage methods, shadowed storage names, missing migration release evidence, unsupported target versions, and JavaScript parse failures report unknown. This bounded analysis does not execute merchant code or prove computed keys correct. It does not follow arbitrary data flow, dynamic imports, event-handler attributes, generated code or runtime-loaded script graphs. Inspect unresolved findings and include all campaign sources in scope; a clean result is only static tracked source compatibility evidence. Browser behavior, order creation, tracking and analytics still require their own proof, especially where multiple campaigns share an origin. Use the SDK public store API where the manifest recommends it; do not invent a prefix-first merchant repair.
18
18
 
19
- Storage `length` property reads are unaffected metadata (`storage-metadata-read`). Storage `key()` enumeration, extracted method references, and mutations of reserved members remain unknown; enumeration cannot prove an SDK key lookup safe. Git discovery failures return one bounded CLI error. Local script diagnostics retain repository-relative spellings from HTML source, including `../` paths; excluded/outside sources are not read. The Git root and operator target are compared by filesystem device/inode identity, so supported case aliases and symlinks can name the same root. Source containment uses realpaths below the Git root without case folding. Extra positional CLI arguments are rejected so an accidental scope or version value cannot silently disappear.
19
+ A `dynamic-key`, `unsupported-storage-method` or `storage-object-escape` line inside a loop, or an array callback such as `filter` or `forEach`, that tests a key it read from storage (a `k = sessionStorage.key(i)` variable, a `for…of Object.keys(sessionStorage)` or `for…in sessionStorage` variable, or the element parameter, the second for `reduce`, of a callback chained on `Object.keys(sessionStorage)`) with `k.indexOf('<prefix>') === 0` (or `!== 0`) or `k.startsWith('<prefix>')`, where the prefix is a scoped SDK key from the manifest (`next-cart-state` for `next-cart-state{__scope}`), ends in `[prefix loop: <prefix>]` and carries `prefixScan` in `--json`, when the line is where that key came from or reads that key by name. Other storage use in the loop, such as passing `sessionStorage` to a function, is not marked: the prefix test does not explain it. One note below the findings explains them all. Such a loop finds the SDK key with or without its scope suffix, so scoping alone does not break it. The lines still report unknown: Campaign Cart scopes these keys so campaigns on one origin cannot read each other's copy, and a prefix match reads whichever copy it meets first. Where the manifest names a public store for the key, as it does `useOrderStore` for `next-order`, the note names it too.
20
+
21
+ Storage `length` property reads are unaffected metadata (`storage-metadata-read`). Storage `key()` enumeration, extracted method references, and mutations of reserved members remain unknown; enumeration cannot prove an SDK key lookup safe. Git discovery failures return one bounded CLI error. The tracked-file inventory lists only the paths under `--scope`, so repository size does not limit it; when it fails, the error reads `Cannot inventory tracked Git source files: <reason>` with Git's own first stderr line as the reason, or, when Git printed nothing, how it ended (its exit status, the signal that stopped it, or a spawn error code), or a request to narrow a scope whose list passes 256 MiB. npm 1.61.0 and earlier listed the whole repository and stopped with `Cannot inventory tracked Git source files.` once that list passed 1 MiB (about 25,000 paths); update the install rather than running the check on a copy, which can lose tracked files. Local script diagnostics retain repository-relative spellings from HTML source, including `../` paths; excluded/outside sources are not read. The Git root and operator target are compared by filesystem device/inode identity, so supported case aliases and symlinks can name the same root. Source containment uses realpaths below the Git root without case folding. Extra positional CLI arguments are rejected so an accidental scope or version value cannot silently disappear.
20
22
 
21
23
  ## Rewriting hardcoded pins in a static repo
22
24
 
@@ -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.60.0+skills.1"
19
+ "bundle_revision": "1.62.0+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.60.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.62.0+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.60.0+skills.1
40
+ Bundle revision: 1.62.0+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.60.0+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.62.0+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.60.0+skills.1",
93
+ "requested": "1.62.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.60.0+skills.1",
95
+ "on_disk": "1.62.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.60.0+skills.1)"
97
+ "message": "match (1.62.0+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.60.0+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.60.0+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.60.0+skills.1)
104
+ Skills revision: match (1.62.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.62.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.62.0+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an
@@ -0,0 +1,213 @@
1
+ # Source Adapters
2
+
3
+ The current release uses `html_funnel`: prepared HTML/CSS/assets provided by the developer.
4
+
5
+ The adapter is deliberately explicit so the Build Packet, doctor, build, polish, deploy, and QA gates can reason about the source pages the same way every time.
6
+
7
+ > **Where does the source HTML come from?** That's the entry-point question, covered in a separate doc: [docs/entry-points.md](./entry-points.md) names the five recognized shapes (template-stock, Figma-driven, AI-generated, hand-authored, mixed) and explains how each populates the inputs this adapter consumes.
8
+
9
+ Source adapter inputs are compatibility contracts, not the final design-source
10
+ contract. `prepare-build` may translate them into Design Source Contributions in
11
+ the public Design Source Package while keeping the existing source-html manifest
12
+ available for current producers.
13
+
14
+ ## `html_funnel`
15
+
16
+ Input:
17
+
18
+ - local folder
19
+ - one or more `.html` pages
20
+ - assets referenced by those pages
21
+ - explicit or inferred page mapping to CampaignSpec pages
22
+ - Page Kit target projection in `source_html.pages[].page_kit` after
23
+ `prepare-build`
24
+
25
+ When no source-html manifest exists, `prepare-build` may infer page mappings
26
+ from filenames. If more than one HTML file can satisfy the same active
27
+ CampaignSpec page, or if every matching file was already assigned to a sibling
28
+ page, Campaigns OS blocks `prepare_build`, records
29
+ `context.source.ambiguous_candidates`, and drafts
30
+ `context.source.manifest_draft`. Copy that draft to
31
+ `.campaigns-os/source-html-manifest.json`, choose the intended candidate path for
32
+ each ambiguous page, and rerun `start`/`prepare-build`. The fallback matcher
33
+ should not silently choose among ambiguous source or asset-mirror pages.
34
+
35
+ The `.html` files should be prepared for page-kit ingestion. This does not mean
36
+ "rewrite everything into Liquid." Page Kit source files are HTML files with
37
+ optional YAML frontmatter and optional Liquid filters/includes. Liquid is only
38
+ needed where the page must call page-kit helpers such as `campaign_link`,
39
+ `campaign_asset`, or `campaign_include`.
40
+
41
+ `source_html.pages[].path` is the source file path relative to the source root.
42
+ For mixed producers it may point at `figma-export/landing.html` or
43
+ `checkout/index.html`. Build agents should use the sibling `page_kit` block for
44
+ the target Page Kit file, route, CPK `page_type`, and frontmatter projection.
45
+
46
+ ## Adapter Decisions
47
+
48
+ `prepare-build` records the reusable conversion contract in three places:
49
+
50
+ - `packet.source_html.adapter_contract`
51
+ - `context.adapter_decisions`
52
+ - `report.adapter_decisions`
53
+
54
+ These fields are intentionally machine-readable so doctor can name unfinished
55
+ work instead of relying on chat history. After the build, record each true
56
+ scalar choice with `record build --adapter-decision <key>=<value>`; put several
57
+ choices in one comma-separated flag. A repeated flag keeps only the last list.
58
+ `wrapper_policy` is selected at intake, not through this record flag.
59
+
60
+ - `raw_html_conversion_status`: wrapper stripping, frontmatter, asset moves,
61
+ script/style refs, CTA rewrites, route policy, and layout choice are still
62
+ `pending` until build records `completed` or `not_required` with
63
+ `record build --adapter-decision raw_html_conversion_status=<value>`.
64
+ - `source_asset_strategy`: page-kit campaigns should normally use
65
+ `pagekit_campaign_asset_root`, where `src/<slug>/assets/*` publishes as
66
+ `/<slug>/*`.
67
+ - `commerce_shell_adoption`: runtime commerce pages should be
68
+ `template_clone_first_verified` or `sdk_surfaces_preserved` before a completed
69
+ build handoff. `custom_html_experimental` is a doctor blocker on runtime
70
+ pages.
71
+ - `route_rewrite_policy`: record whether CampaignSpec routes and CTAs were
72
+ rewritten through campaign-aware routes/helpers.
73
+ - `template_files_copied`: records the selected template family as an atomic
74
+ slice. A complete/verified slice covers `pages`, `_includes`, `_layouts`,
75
+ `assets/css`, `assets/js`, and `frontmatter_vocabulary`; its `paths` are
76
+ target-repo-relative proof paths that doctor checks once assembly is complete.
77
+ - `config_script_strategy`: records how campaign config scripts load
78
+ (`campaign_asset`, `frontmatter_script`, `inline`, or `not_required`).
79
+ - `wrapper_policy`: records whether document wrappers were stripped, preserved,
80
+ or not required (`strip_document_wrappers`, `preserve_document_wrappers`,
81
+ `not_required`, `unknown`). Unlike the other decisions here, this one is
82
+ selectable before the build stage runs — see
83
+ [Selecting the wrapper policy at intake](#selecting-the-wrapper-policy-at-intake).
84
+ - `frontmatter_policy`: records whether Page Kit YAML frontmatter was created,
85
+ preserved, or intentionally not required.
86
+ - `script_style_reference_policy`: records whether scripts/styles were moved to
87
+ frontmatter, campaign assets, inline blocks, or raw passthrough.
88
+ - `cta_rewrite_policy`: records how CTA destinations were rewritten from
89
+ CampaignSpec routes.
90
+ - `layout_choice`: records the Page Kit layout strategy used for the prepared
91
+ source.
92
+
93
+ Raw AI-generated HTML normally needs this conversion pass before build:
94
+
95
+ 1. Keep the page-owned body markup that represents the design intent.
96
+ 2. Remove document wrappers such as `<!doctype>`, `<html>`, `<head>`, and `<body>` so the page can be wrapped by the campaign layout.
97
+ 3. Add YAML frontmatter for `title`, the `page_kit.frontmatter.page_type`, route/permalink or `next_url`, and any `styles`/`scripts`.
98
+ 4. Move reusable `<style>` blocks into `src/<slug>/assets/css/...` and reference them from frontmatter; keep tiny page-specific styles inline only when that matches the target campaign style.
99
+ 5. Move referenced images/fonts/assets into the campaign asset tree and use `campaign_asset` when paths need to be campaign-rooted. Page Kit copies `src/<slug>/assets/*` to the campaign root in built output: `src/<slug>/assets/config.js` renders as `/<slug>/config.js`, and `src/<slug>/assets/products/foo.png` renders as `/<slug>/products/foo.png`. Do not leave raw `/assets/...` or `/<slug>/assets/...` references in rendered pages unless that nested `assets/` directory exists intentionally.
100
+ 6. Replace internal page links and CTA destinations with CampaignSpec-derived routes, usually through `campaign_link`.
101
+ 7. For checkout, upsell, downsell, receipt, payment, totals, and submit controls, preserve or recreate the starter-template SDK contract rather than trusting raw source commerce markup.
102
+ 8. Run page-kit build and inspect `_site/<slug>/` for a complete body, expected meta tags, campaign-rooted links, and SDK runtime markers.
103
+
104
+ A single-file static landing page is a good source artifact, but it is not
105
+ automatically a complete page-kit page.
106
+
107
+ ## Source preparation check
108
+
109
+ The preparation steps above used to be tacit knowledge; the doctor now checks
110
+ them deterministically (#262). The check runs as the `source_html.preparation`
111
+ slot in the Doctor Check Registry (phase `source`) on every mapped
112
+ `source_html.pages[].path` file, so `start` and `build` — which embed doctor —
113
+ gate on it before assembly, and a plain `campaigns-os doctor` re-checks the
114
+ current files after source edits. Blocking findings are doctor **errors**: they
115
+ set status `blocked` and make doctor's `next` block name the same stage the
116
+ `next` command would (`prepare-build` while the prepare-build stage records
117
+ the blockers, `doctor-blocked` otherwise), like any other unprepared input. The check is scoped to the certified template families'
118
+ page-kit ingestion expectations; it is not a general HTML linter.
119
+
120
+ | Code | Severity | Meaning and fix |
121
+ | --- | --- | --- |
122
+ | `source_html.prep.document_wrapper` | error (warning under a recorded `wrapper_policy: "preserve_document_wrappers"`) | A mapped source page is still a full browser document. Once the converted page-kit page exists at the page's `page_kit.output_path`, that page is checked instead of the design, since it is the file page-kit builds. Strip `<!doctype>`, `<html>`, `<head>`, and `<body>` so the campaign layout can wrap the page (conversion step 2; [docs/quickstart.md](./quickstart.md) "Prepare Raw HTML Source"), or record `preserve_document_wrappers` as an explicit adapter decision — the manifest `wrapper_policy` key or `--wrapper-policy`, see [Selecting the wrapper policy at intake](#selecting-the-wrapper-policy-at-intake). |
123
+ | `source_html.prep.frontmatter_residue` | error | Leftover or broken YAML frontmatter: a leading `---` fence that never closes, or a frontmatter block (page-kit keys such as `page_type:`, `next_url:`) embedded below content where page-kit would render it literally. Keep exactly one closed frontmatter block at the very top of the file, or none when the packet's `page_kit.frontmatter` projection supplies it (conversion step 3). |
124
+ | `source_html.prep.internal_link_unrooted` | warning | An internal link/CTA still targets a source file (`checkout.html`) instead of a CampaignSpec-derived route. Replace it, usually via `campaign_link` (conversion step 6). Warning rather than error because CTA rewrites are sanctioned build-stage work recorded under `cta_rewrite_policy`; the adapter gates own the completed-assembly block. |
125
+
126
+ ### Selecting the wrapper policy at intake
127
+
128
+ `preserve_document_wrappers` is a real choice, so whoever hands over the source
129
+ can make it — without editing the Build Packet the build stage writes. Two
130
+ channels select it, both in the vocabulary above:
131
+
132
+ **The source-html manifest.** Declare it once, next to the pages it applies to,
133
+ in `.campaigns-os/source-html-manifest.json`:
134
+
135
+ ```json
136
+ {
137
+ "schema_version": "source-html-manifest/v0",
138
+ "wrapper_policy": "preserve_document_wrappers",
139
+ "pages": [{ "page_id": "landing", "path": "landing.html" }]
140
+ }
141
+ ```
142
+
143
+ **The CLI flag.** Select it per run on the command that prepares the build:
144
+
145
+ ```bash
146
+ campaigns-os prepare-build --spec <spec.json> --source <html-dir> \
147
+ --target <page-kit-dir> --template-family <family> \
148
+ --wrapper-policy preserve_document_wrappers
149
+ ```
150
+
151
+ `start` and `build` take the same flag, since both run the prepare step.
152
+
153
+ **Order of operations.** Decide the wrapper policy before you capture
154
+ `source_screenshot` proof or record `source_hash`, because both describe the
155
+ file as it is handed over. `source_hash` is the sha256 of the source file at
156
+ manifest generation time; `doctor` recomputes it and reports
157
+ `source_html.pages.source_hash` when the file no longer matches. So under the
158
+ default `strip_document_wrappers` policy, strip first, then capture the
159
+ screenshots and compute the hashes of the stripped files. Under
160
+ `preserve_document_wrappers` nothing is stripped, the existing screenshots and
161
+ hashes stay valid, and the design-source gate and the preparation check read
162
+ the same file. Stripping after the manifest was written invalidates its hashes
163
+ and its screenshots, and a stripped page is no longer the standalone document
164
+ its `source_screenshot` proof was captured from.
165
+
166
+ **Precedence.** The flag wins over the manifest key, and with neither the
167
+ default is `strip_document_wrappers` — the same order the template family uses
168
+ for its CLI override and its spec hint (see
169
+ [docs/build-packet.md](./build-packet.md), "Authoring-Time Hints"). The
170
+ resolved value is written to
171
+ `source_html.adapter_contract.wrapper_policy` on the packet, and a non-default
172
+ selection is echoed on stderr with the channel that set it. A value outside the
173
+ vocabulary fails the run when it comes from the flag. From the manifest key it
174
+ does not: the manifest is used as written — `pages[]`, `producer_provenance`
175
+ and `files[]` all survive — the key alone is ignored, the default policy
176
+ applies, and one warning names the key, the value, and the accepted values.
177
+ Dropping a whole manifest over one optional hint would cost far more than the
178
+ hint is worth.
179
+
180
+ Selecting `preserve_document_wrappers` does not silence the finding: the
181
+ document wrappers are still reported on every page that carries them, as a
182
+ warning rather than a blocking error.
183
+
184
+ Asset-path rooting (raw `/assets/...` references, missing or out-of-root
185
+ files) stays owned by the prepare-build source asset crawl and its
186
+ `source_asset.*` codes (conversion step 5); the preparation check does not
187
+ duplicate them.
188
+
189
+ Known-unprepared and prepared reference fixtures live at
190
+ `fixtures/source-prep/`; `src/source-prep.test.mjs` and
191
+ `src/doctor-source-prep.test.mjs` pin each code and the pass path.
192
+
193
+ Wedge relevance: the Stage 0 wedge run protocol (#259) includes this check —
194
+ wedge runs treat a `source_html.prep.*` blocker as a classified workflow
195
+ signal at the source-preparation handoff, not founder-interpretation territory.
196
+
197
+ Checkout, cart, upsell, receipt, payment, totals, and submit behavior should remain governed by starter-template contracts and SDK wiring. Source files can carry page design, but live commerce behavior comes from the campaign setup and runtime surfaces.
198
+
199
+ If a source page marks a region as SDK-owned, for example with
200
+ `data-commerce-zone="checkout-form"` or comments like `SDK-OWNED: ... provided
201
+ by the limos checkout commerce surface`, treat that as an instruction to adopt
202
+ the selected starter-template family shell for that runtime surface. It is not
203
+ permission to invent a custom checkout shell and paste only borrowed partials
204
+ inside it. `prepare-build` records these regions in the Build Context, and
205
+ doctor warns until the build path has a clear shell-adoption decision.
206
+
207
+ Template families are not single-file dependencies. If setup copies a starter
208
+ family into a target campaign, copy the matching pages together with their
209
+ family `_includes/`, `_layouts/`, `assets/css/`, and `assets/js/` dependencies.
210
+ Copying only `checkout.html` and `receipt.html` can leave Liquid includes,
211
+ layouts, CSS, or JavaScript unresolved. Record the copied or verified target
212
+ paths in `template_files_copied.paths`; a completed report with missing paths is
213
+ not doctor-clean.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.60.0",
3
+ "version": "1.62.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -104,7 +104,9 @@
104
104
  "yaml": "^2.9.1"
105
105
  },
106
106
  "optionalDependencies": {
107
- "playwright": "^1.63.0"
107
+ "pixelmatch": "^7.2.0",
108
+ "playwright": "^1.63.0",
109
+ "pngjs": "^7.0.0"
108
110
  },
109
111
  "devDependencies": {
110
112
  "typescript": "^5.9.3"
@@ -153,6 +155,11 @@
153
155
  "docs/progress-snapshots.md",
154
156
  "demo",
155
157
  "docs/demo-preview.md",
156
- "docs/local-setup.md"
158
+ "docs/local-setup.md",
159
+ "docs/quickstart.md",
160
+ "docs/access-model.md",
161
+ "docs/entry-points.md",
162
+ "docs/source-adapters.md",
163
+ "docs/campaignspec-authoring-examples.md"
157
164
  ]
158
165
  }
@@ -272,8 +272,15 @@
272
272
  "sha256": { "$ref": "#/$defs/sha256" },
273
273
  "role": { "type": "string", "minLength": 1 },
274
274
  "media_type": { "type": "string", "minLength": 1 },
275
+ "design_file_type": { "type": "string", "minLength": 1, "pattern": "\\S" },
275
276
  "notes": { "type": ["string", "null"] }
276
277
  },
278
+ "allOf": [
279
+ {
280
+ "if": { "properties": { "design_file_type": true }, "required": ["design_file_type"] },
281
+ "then": { "properties": { "role": { "const": "design_file" } }, "required": ["role"] }
282
+ }
283
+ ],
277
284
  "anyOf": [
278
285
  { "properties": { "path": true }, "required": ["path"] },
279
286
  { "properties": { "url": true }, "required": ["url"] }
@@ -460,6 +467,7 @@
460
467
  "screenshot_refs": { "$ref": "#/$defs/stringList" },
461
468
  "reference_refs": { "$ref": "#/$defs/stringList" },
462
469
  "template_reference_id": { "type": "string", "minLength": 1 },
470
+ "derived_from": { "type": "string", "minLength": 1, "pattern": "\\S" },
463
471
  "notes": { "type": ["string", "null"] }
464
472
  },
465
473
  "allOf": [