@nextcommerce/campaigns-os 1.33.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 (310) hide show
  1. package/AGENTS.md +204 -0
  2. package/CHANGELOG.md +5002 -0
  3. package/CONTEXT.md +685 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +4 -0
  6. package/README.md +368 -0
  7. package/agents/claude/CLAUDE.md +32 -0
  8. package/agents/codex/AGENTS.md +27 -0
  9. package/agents/copilot/copilot-instructions.md +14 -0
  10. package/agents/cursor/campaigns-os.mdc +13 -0
  11. package/bin/campaigns-os.mjs +38 -0
  12. package/campaign-spec/README.md +138 -0
  13. package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
  14. package/campaign-spec/dist/analytics-vocabulary.js +74 -0
  15. package/campaign-spec/dist/index.d.ts +40 -0
  16. package/campaign-spec/dist/index.js +77 -0
  17. package/campaign-spec/dist/normalize.d.ts +22 -0
  18. package/campaign-spec/dist/normalize.js +41 -0
  19. package/campaign-spec/dist/routing.d.ts +190 -0
  20. package/campaign-spec/dist/routing.js +263 -0
  21. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
  22. package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
  23. package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
  24. package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
  25. package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
  26. package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
  27. package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
  28. package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
  29. package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
  30. package/campaign-spec/dist/rules/cycle-detection.js +141 -0
  31. package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
  32. package/campaign-spec/dist/rules/design-source-shape.js +142 -0
  33. package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
  34. package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
  35. package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
  36. package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
  37. package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
  38. package/campaign-spec/dist/rules/funnel-count.js +37 -0
  39. package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
  40. package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
  41. package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
  42. package/campaign-spec/dist/rules/funnel-identity.js +57 -0
  43. package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
  44. package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
  45. package/campaign-spec/dist/rules/index.d.ts +69 -0
  46. package/campaign-spec/dist/rules/index.js +131 -0
  47. package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
  48. package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
  49. package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
  50. package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
  51. package/campaign-spec/dist/rules/page-count.d.ts +11 -0
  52. package/campaign-spec/dist/rules/page-count.js +31 -0
  53. package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
  54. package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
  55. package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
  56. package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
  57. package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
  58. package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
  59. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
  60. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
  61. package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
  62. package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
  63. package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
  64. package/campaign-spec/dist/rules/schema-version.js +53 -0
  65. package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
  66. package/campaign-spec/dist/rules/sdk-version.js +96 -0
  67. package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
  68. package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
  69. package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
  70. package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
  71. package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
  72. package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
  73. package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
  74. package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
  75. package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
  76. package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
  77. package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
  78. package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
  79. package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
  80. package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
  81. package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
  82. package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
  83. package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
  84. package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
  85. package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
  86. package/campaign-spec/dist/sdk-version-parse.js +62 -0
  87. package/campaign-spec/dist/types.d.ts +674 -0
  88. package/campaign-spec/dist/types.js +40 -0
  89. package/campaign-spec/package.json +12 -0
  90. package/compatibility.json +25 -0
  91. package/contracts/agent-relevant-change-policy.v1.json +111 -0
  92. package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
  93. package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
  94. package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
  95. package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
  96. package/contracts/commerce-surface-catalog.json +2452 -0
  97. package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
  98. package/contracts/fixtures/orientation/envelope/current.json +95 -0
  99. package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
  100. package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
  101. package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
  102. package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
  103. package/contracts/fixtures/orientation/envelope/refused.json +100 -0
  104. package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
  105. package/contracts/fixtures/orientation/envelope/updated.json +135 -0
  106. package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
  107. package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
  108. package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
  109. package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
  110. package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
  111. package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
  112. package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
  113. package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
  114. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
  115. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
  116. package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
  117. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
  118. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
  119. package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
  120. package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
  121. package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
  122. package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
  123. package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
  124. package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
  125. package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
  126. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
  127. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
  128. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
  129. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
  130. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
  131. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
  132. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
  133. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
  134. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
  135. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
  136. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
  137. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
  138. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
  139. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
  140. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
  141. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
  142. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
  143. package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
  144. package/contracts/migration-sidecar-bundle.v0.json +149 -0
  145. package/contracts/orientation-limits.v1.json +41 -0
  146. package/contracts/orientation-reason-codes.v1.json +196 -0
  147. package/contracts/private-template-sources.json +8 -0
  148. package/contracts/release-ledger.json +4598 -0
  149. package/contracts/reserved-skill-names.json +13 -0
  150. package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
  151. package/contracts/supported-surface.json +180 -0
  152. package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
  153. package/contracts/template-brand-contract.apollo.v0.json +27 -0
  154. package/contracts/template-brand-contract.demeter.v0.json +27 -0
  155. package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
  156. package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
  157. package/contracts/template-brand-contract.olympus.v0.json +27 -0
  158. package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
  159. package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
  160. package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
  161. package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
  162. package/contracts/template-slot-manifest.apollo.v0.json +12 -0
  163. package/contracts/template-slot-manifest.demeter.v0.json +30 -0
  164. package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
  165. package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
  166. package/contracts/template-slot-manifest.olympus.v0.json +12 -0
  167. package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
  168. package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
  169. package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
  170. package/docs/brand-theme-bridge.md +159 -0
  171. package/docs/build-packet.md +1300 -0
  172. package/docs/campaign-build-brief.md +145 -0
  173. package/docs/campaign-standardization-report.md +329 -0
  174. package/docs/campaigns-os-build-flow.md +117 -0
  175. package/docs/design-source-package.md +784 -0
  176. package/docs/legacy-migration.md +58 -0
  177. package/docs/migration-sidecar-bundle.md +139 -0
  178. package/docs/orientation-contract-reference.md +1220 -0
  179. package/docs/polish-evidence.md +502 -0
  180. package/docs/qa-and-test-orders.md +1691 -0
  181. package/docs/release-ledger-authoring-guide.md +274 -0
  182. package/docs/runtime-readiness.md +211 -0
  183. package/docs/supported-surface.md +83 -0
  184. package/docs/versioning.md +55 -0
  185. package/docs/workflow-findings-sidecar.md +588 -0
  186. package/package.json +135 -0
  187. package/prompts/first-build.md +27 -0
  188. package/prompts/friction-log.md +30 -0
  189. package/schemas/campaign-build-brief.v1.schema.json +149 -0
  190. package/schemas/campaign-design-source-package.v0.schema.json +697 -0
  191. package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
  192. package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
  193. package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
  194. package/schemas/campaign-spec.v4.schema.json +582 -0
  195. package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
  196. package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
  197. package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
  198. package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
  199. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
  200. package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
  201. package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
  202. package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
  203. package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
  204. package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
  205. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
  206. package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
  207. package/schemas/source-html-manifest.v0.schema.json +252 -0
  208. package/skills/next-campaigns-build/SKILL.md +73 -0
  209. package/skills/next-campaigns-os/SKILL.md +101 -0
  210. package/skills/next-campaigns-os/references/session-intake.md +160 -0
  211. package/skills/next-campaigns-os-setup/SKILL.md +22 -0
  212. package/skills/next-campaigns-polish/SKILL.md +145 -0
  213. package/skills/next-campaigns-qa/SKILL.md +92 -0
  214. package/skills.json +56 -0
  215. package/skills.sh +64 -0
  216. package/src/adapter-decision-contract.mjs +333 -0
  217. package/src/brand-theme.mjs +1151 -0
  218. package/src/browser-launch.mjs +79 -0
  219. package/src/build-brief.mjs +781 -0
  220. package/src/built-site-scope.mjs +312 -0
  221. package/src/campaign-ecosystem.mjs +734 -0
  222. package/src/campaign-identity.mjs +405 -0
  223. package/src/campaign-workspace.mjs +127 -0
  224. package/src/checkpoint-waiver.mjs +302 -0
  225. package/src/cli.mjs +13442 -0
  226. package/src/commercial-journey.mjs +1119 -0
  227. package/src/commercial-parity.mjs +965 -0
  228. package/src/consent.mjs +347 -0
  229. package/src/content-residue.mjs +322 -0
  230. package/src/deadline.mjs +81 -0
  231. package/src/design-source-package.mjs +2604 -0
  232. package/src/deviation.mjs +107 -0
  233. package/src/doctor-check-registry.mjs +49 -0
  234. package/src/doctor-sidecar.mjs +106 -0
  235. package/src/finding-cause.mjs +557 -0
  236. package/src/findings.mjs +326 -0
  237. package/src/fs-identity.mjs +68 -0
  238. package/src/gate-actions.mjs +105 -0
  239. package/src/html-scan.mjs +53 -0
  240. package/src/install-mode.mjs +272 -0
  241. package/src/legacy-migration.d.ts +128 -0
  242. package/src/legacy-migration.mjs +510 -0
  243. package/src/lifecycle.mjs +338 -0
  244. package/src/local-proof.mjs +401 -0
  245. package/src/map-pin-writeback.mjs +210 -0
  246. package/src/orchestration-stage-contract.mjs +81 -0
  247. package/src/package-install-fixture.mjs +33 -0
  248. package/src/page-kit-build-summary.mjs +175 -0
  249. package/src/page-kit-campaign-config.mjs +57 -0
  250. package/src/page-kit-sdk-version.mjs +392 -0
  251. package/src/page-kit-store-profile.mjs +369 -0
  252. package/src/page-kit-sync.mjs +162 -0
  253. package/src/polish-browser.mjs +867 -0
  254. package/src/polish-capture.mjs +1094 -0
  255. package/src/polish-deadline.mjs +51 -0
  256. package/src/polish-gate.mjs +739 -0
  257. package/src/polish-node.mjs +639 -0
  258. package/src/polish-page-load.mjs +1100 -0
  259. package/src/private-template-source.mjs +237 -0
  260. package/src/proof-policy.mjs +82 -0
  261. package/src/qa-analytics-correctness.mjs +307 -0
  262. package/src/qa-analytics-errors.mjs +38 -0
  263. package/src/qa-analytics-parity.mjs +699 -0
  264. package/src/qa-binding-evidence.mjs +140 -0
  265. package/src/qa-browser.mjs +6608 -0
  266. package/src/qa-cart-entry.mjs +406 -0
  267. package/src/qa-commercial-parity.mjs +641 -0
  268. package/src/qa-node.mjs +3620 -0
  269. package/src/qa-order-bump.mjs +381 -0
  270. package/src/qa-parity-capture.mjs +428 -0
  271. package/src/qa-parity-fixture.mjs +359 -0
  272. package/src/qa-publish.mjs +362 -0
  273. package/src/qa-purchase-data-layer.mjs +263 -0
  274. package/src/qa-route-probe.mjs +272 -0
  275. package/src/qa-sidecar.mjs +188 -0
  276. package/src/qa-test-order-topology.mjs +207 -0
  277. package/src/qa-url-privacy.mjs +13 -0
  278. package/src/qa-verdict-discovery.mjs +192 -0
  279. package/src/qa-verdict-publish.mjs +105 -0
  280. package/src/qa-verdict.mjs +287 -0
  281. package/src/remit.mjs +388 -0
  282. package/src/repo-scan.mjs +83 -0
  283. package/src/route-identity.mjs +133 -0
  284. package/src/run-record-closeout.mjs +229 -0
  285. package/src/run-record.mjs +839 -0
  286. package/src/run-session.mjs +226 -0
  287. package/src/runtime-state-ignore.mjs +113 -0
  288. package/src/sdk-attribute-index.mjs +212 -0
  289. package/src/sdk-markup.mjs +358 -0
  290. package/src/sdk-meta-tags.mjs +52 -0
  291. package/src/shell-token.mjs +7 -0
  292. package/src/sidecar-bundle.mjs +399 -0
  293. package/src/source-asset-crawl.mjs +469 -0
  294. package/src/source-html-intake.mjs +627 -0
  295. package/src/source-html-manifest.mjs +276 -0
  296. package/src/source-prep.mjs +284 -0
  297. package/src/spec-derive-store.mjs +431 -0
  298. package/src/spec-derive.mjs +514 -0
  299. package/src/spec-fetch.mjs +66 -0
  300. package/src/spec-hash.mjs +48 -0
  301. package/src/spec-identity.mjs +27 -0
  302. package/src/stage-ledger.mjs +509 -0
  303. package/src/standardization-report.mjs +1297 -0
  304. package/src/template-brand-contract.mjs +473 -0
  305. package/src/template-freshness.mjs +196 -0
  306. package/src/template-reference.mjs +81 -0
  307. package/src/template-slot-manifest.mjs +150 -0
  308. package/src/text-safety.mjs +62 -0
  309. package/src/theme-gate.mjs +185 -0
  310. package/src/upsell-selector-scope.mjs +299 -0
@@ -0,0 +1,502 @@
1
+ # Polish evidence schema (`stages.polish.evidence`)
2
+
3
+ This is the authoritative, docs-side description of what the polish gate
4
+ (`evaluatePolishGate` in `src/polish-gate.mjs`) accepts **today**. The gate is
5
+ evaluated by `campaigns-os next polish|deploy|qa`, by `qa run`, and by doctor;
6
+ a blocked gate surfaces as a `polish.*` error and stops QA handoff. Everything
7
+ below documents existing behavior — if this document and the code disagree, the
8
+ code wins and this file has drifted (a test in `src/polish-gate.test.mjs`
9
+ pins the required-field list and blocker codes to this file).
10
+
11
+ Three layers must all be satisfied:
12
+
13
+ 1. **The stage record** — `stages.polish` on the Assembly Report
14
+ (`.campaign-runtime/assembly-report.json`) with the freshness/identity
15
+ fields below.
16
+ 2. **The evidence block** — `stages.polish.evidence` (legacy fallback:
17
+ `report.polish.evidence`) with the seven required categories, three of which
18
+ also get semantic content checks.
19
+ 3. **Package-owned page-load evidence** —
20
+ `stages.polish.evidence.visual_review.page_load`, produced only by
21
+ `campaigns-os polish capture` from the current packet, report, served build,
22
+ mapped routes, and fixed desktop/mobile viewports.
23
+
24
+ ## 1. Stage-record requirements
25
+
26
+ The gate only applies once assembly is complete
27
+ (`stages.assembly.status` starts with `completed`); before that it returns
28
+ `polish.not_applicable`.
29
+
30
+ | Field | Accepted locations | Requirement |
31
+ |---|---|---|
32
+ | `status` | `stages.polish.status` | Must start with `completed`. `blocked` → `polish.blocked`; anything else → `polish.evidence_missing`. |
33
+ | `performed_by` | `stages.polish.performed_by`, `stages.polish.command_identity`, `evidence.performed_by` | Must be exactly `next-campaigns-polish`. Anything else (including missing) → `polish.self_certified`. |
34
+ | `commands` | `stages.polish.commands` and/or `evidence.commands` | Must NOT mention a build command anywhere — a polish record that ran build is self-certification → `polish.self_certified`. The matcher is exactly the strings `next-campaigns-build` and `campaigns-os next build` (case-insensitive); other build tooling (e.g. page-kit's `campaign-build`) is not matched. |
35
+ | `source_build_fingerprint` | `stages.polish.source_build_fingerprint` or `evidence.source_build_fingerprint` | Required, and must equal the current build fingerprint (`stages.assembly.build_fingerprint`, falling back to `stages.assembly.artifact_fingerprint`, `report.build_fingerprint`, `report.artifact_fingerprint`). Missing → `polish.source_build_fingerprint_missing`; different → `polish.stale`. Doctor, QA, and `polish capture` also recompute the fingerprint from the built output under `_site/<slug>/` (algorithm in `docs/build-packet.md`); when the output no longer matches the recorded value the gate is `polish.output_drift` with `current_output_fingerprint` and a `rerun_build` action, whatever the recorded string says. |
36
+ | `source_package_material_fingerprint` | `stages.polish.source_package_material_fingerprint` or `evidence.source_package_material_fingerprint` | Only enforced when the report carries a current Design Source Package material fingerprint (`design_source_package.material_fingerprint` or equivalents). Missing → `polish.source_package_material_fingerprint_missing`; different → `polish.source_package_stale`. |
37
+ | `completed_at` | `stages.polish.completed_at` or `evidence.completed_at` | Required non-empty timestamp string → else `polish.completed_at_missing`. |
38
+
39
+ Assembly itself must also be tied to the current Design Source Package when one
40
+ is fingerprinted: `stages.assembly.source_package_material_fingerprint` missing
41
+ → `polish.assembly_source_package_fingerprint_missing`; different →
42
+ `polish.assembly_source_package_stale`. Both can be waived (see §5).
43
+
44
+ ## 2. Required evidence categories
45
+
46
+ `stages.polish.evidence` must be an object containing **all seven** fields.
47
+ A missing or shape-invalid field produces `polish.evidence_incomplete` with a
48
+ per-field problem line.
49
+
50
+ | Field | Accepted shape (presence check) |
51
+ |---|---|
52
+ | `visual_review` | **Object** with a `screenshots` array (accepted aliases for the array key: `screenshot_paths`, `paths`, `urls`) containing at least one non-empty string, plus package-generated `page_load`. A bare string, an object without a screenshot array, or an empty array all fail. The gate's shape check accepts a single screenshot entry; the `next-campaigns-polish` responsibility bar is desktop **and** mobile captures of the key commerce anchors — record both. Never hand-author `page_load`. |
53
+ | `brand_review` | Non-empty object (semantic checks in §3 apply: `favicon`, `brand_bleed`). |
54
+ | `checkout_review` | Non-empty object (semantic checks in §3 apply: `field_labels`, `bump_compare_price_rule`). |
55
+ | `template_residue_review` | Non-empty object/array/string (semantic check on `starter_favicon` in §3). |
56
+ | `commerce_flow_review` | Non-empty string, non-empty array, or non-empty object. |
57
+ | `issues` | **Must be an array.** An empty array is the canonical "no issues found". A missing field, string, or object fails. |
58
+ | `commands` | Non-empty array with at least one string or object entry — the commands the polish pass actually ran. Must not include build commands (§1). |
59
+
60
+ For fields without a stricter rule above, "non-empty" means: array with ≥1
61
+ entry, object with ≥1 key, or non-empty string.
62
+
63
+ ### 2.1 Package-owned page-load evidence
64
+
65
+ Install the package-owned Chromium runtime, serve the current build, then run
66
+ the producer before marking `stages.polish` complete, deploying, or starting
67
+ QA:
68
+
69
+ ```bash
70
+ npm run qa:install-browser
71
+ campaigns-os polish capture \
72
+ --packet campaign-runtime.build.json \
73
+ --base-url http://127.0.0.1:4173
74
+ ```
75
+
76
+ The command derives every mapped, non-skipped Page Kit route from the packet
77
+ and captures each at desktop `1440x1200` and mobile `390x844`. It re-reads the
78
+ packet and Assembly Report after the browser pass, refuses attachment if the
79
+ governing build, campaign, route plan, report identity, or existing `page_load`
80
+ changed before that final read, and otherwise merges only
81
+ `stages.polish.evidence.visual_review.page_load` onto the report it just read.
82
+ The temp-file-plus-rename write prevents readers from seeing torn JSON. This is
83
+ optimistic two-read conflict detection, not a lock, compare-and-swap, or true
84
+ no-clobber write: another writer can still change the report in the narrow
85
+ interval between the final read and rename. Keep one Assembly Report writer at
86
+ a time and retry from the newest report after a conflict. Screenshots and every
87
+ unrelated report field from the final read are preserved.
88
+
89
+ `--base-url` is the operator-provided location of the served current build. The
90
+ producer binds its evidence to the packet/report build fingerprint, campaign,
91
+ and deterministic route plan, but it does not cryptographically attest that the
92
+ bytes served at that URL came from that build. Point it at the current output;
93
+ do not reuse an older preview merely because its routes match. Incomplete
94
+ evidence is still persisted for diagnosis, returns a nonzero status, and never
95
+ marks Polish complete.
96
+
97
+ The optional real-browser smoke is separate from the workflow producer:
98
+
99
+ ```bash
100
+ npm run smoke:polish-capture
101
+ ```
102
+
103
+ Run it only after `npm run qa:install-browser` in an environment that permits a
104
+ loopback HTTP listener and Chromium. It is deliberately opt-in and is not part
105
+ of `npm run check` or CI.
106
+
107
+ ### Collector response records (the producer's wire form)
108
+
109
+ Inside the producer, the browser adapter's CDP collector emits one
110
+ `responses[]` list per route/viewport cell and the pure aggregator in
111
+ `src/polish-capture.mjs` turns it into the `page_load` projection below. The
112
+ list is never persisted and never hand-authored; it is one record shape with
113
+ one producer, built and read through the constructors `src/polish-capture.mjs`
114
+ exports (`singleResponseRecord`, `redirectChainRecord`, `captureProblemRecord`,
115
+ `responseRecordResponses`, `responseRecordFromCache`). The aggregator trusts
116
+ what the constructors build — it does not re-check hop numbering, request
117
+ identity or sentinel spelling on the way in — so a test that hand-builds a
118
+ record uses the same constructors.
119
+
120
+ | Record | Shape |
121
+ |---|---|
122
+ | Single response | `{ request_id, ...response }` — one CDP request that produced one response. |
123
+ | Redirect chain | `{ request_id, redirect_chain: [{ ...response, redirect_hop: 0 }, { ...response, redirect_hop: 1 }, …] }` — one request whose hops are numbered from zero in transfer order; hops never carry `request_id`. Every hop is one observed response and every hop URL joins the chain's ledger matches. |
124
+ | Problem sentinel | `{ capture_problem: code }` with `code` one of `response_record_overflow` (the collector dropped responses past `MAX_PAGE_LOAD_RESPONSE_RECORDS`, 4,096) or `document_context_changed` (the main frame or loader changed under the capture). A sentinel is counted as that problem and is not a response. |
125
+
126
+ A response carries `url` and `resource_type` (the CDP type; an `OPTIONS`
127
+ request reported as `Other` is classified `Preflight` at the source),
128
+ `status`, `mime_type` when the browser reported one, `encoded_data_length` (the
129
+ retained transfer size — see the accounting note below) or, for a canceled
130
+ load, `canceled: true` with `declared_data_length`, `source_urls[]` (the
131
+ request URL before the response URL), the three cache flags
132
+ `from_disk_cache`, `from_prefetch_cache` and `request_served_from_cache` (a
133
+ response is cache-served when any of them is true; no other spelling is read),
134
+ `from_service_worker`, and `failed`. The final main document additionally
135
+ carries `is_final_main_document: true` and `document_context_fingerprint`.
136
+ Frame and loader identifiers never leave the collector. Alongside the list the
137
+ collector reports `responseCollectionStatus` (`complete` or `failed`), which the
138
+ aggregator combines with the attributed ledger to decide
139
+ `response_collection.status`.
140
+
141
+ ### Durable `page_load` field map
142
+
143
+ The `page_load` object is generated by the package and must not be hand-authored,
144
+ copied between builds, or repaired in place. Its stable projection is:
145
+
146
+ | Path | Meaning |
147
+ |---|---|
148
+ | `schema_version`, `performed_by`, `threshold_bytes` | Page-load format, package producer identity, and the `1,048,576`-byte finding threshold. |
149
+ | `subject.build_fingerprint`, `subject.campaign_slug` | Build and campaign authority copied from the current packet/report pair. |
150
+ | `subject.route_scope`, `subject.routes[]`, `subject.viewports[]` | Deterministic mapped-route scope (`all` or `selected`), normalized routes, and fixed `desktop` / `mobile` viewport keys. |
151
+ | `measurement.status` | `complete` only when the subject is valid and every expected route/viewport has exactly one complete capture. |
152
+ | `measurement.expected_capture_count`, `measurement.captured_count` | Planned and recorded capture totals. |
153
+ | `measurement.missing[]`, `measurement.duplicate[]`, `measurement.unexpected[]`, `measurement.incomplete[]` | Route/viewport coverage defects. Each incomplete entry carries its sorted `problem_codes[]`; an entry whose codes include `capture_shape_invalid` also carries `shape_violation`, the name of the first shape rule the capture broke (see below). |
154
+ | `measurement.warnings[]` | Complete captures that still carry warning-class problems (today only `cross_origin_request_failed`). Each entry carries the route, viewport, the warning `problem_codes[]`, the sorted unique `resource_types[]` the demotion applied to (the beacon allowlist, so at most five values), the bounded sorted `failed_origins[]` (at most 32) and the full `failed_origin_count`. Warnings never change `measurement.status`; they are evidence for the operator and the merchant. |
155
+ | `captures[]` | One deterministic package projection per route and viewport; see the per-capture map below. |
156
+ | `findings[]` | Observed hidden eager-media findings. Each records `code`, route, viewport, tag and element index, bounded `sources[]` / `resource_ids[]` with their full counts, a fingerprint over the complete resource-identity set, transferred and threshold bytes, preload state, and `hidden_by[]`. |
157
+
158
+ Each `captures[]` entry has this shape:
159
+
160
+ | Path | Meaning |
161
+ |---|---|
162
+ | `schema_version`, `performed_by` | Route-capture format and package producer identity. |
163
+ | `subject.build_fingerprint`, `subject.campaign_slug`, `subject.requested_route`, `subject.final_document_route`, `subject.viewport` | Exact authority and navigation binding for this observation. A final-route mismatch is incomplete evidence. |
164
+ | `measurement_status`, `producer_status` | Overall completeness and whether the browser producer itself completed. |
165
+ | `response_collection.status`, `observed_response_count`, `unattributed_response_count` | CDP response-collection outcome and bounded counts. `unattributed_response_count` is the number of observed responses that have no resource-ledger entry. A response with a non-http(s) URL (`data:`, `blob:`, `about:` — video controls, inline icons and authored `data:` images produce these on ordinary pages) is counted here and is not a problem: nothing was transferred and there is nothing to repair. Only a malformed or over-long URL, or a non-http(s) load that failed, raises `resource_url_unresolvable`. |
166
+ | `document_response.status`, `document_response.url`, `document_response.resource_id`, `document_response.http_status`, `document_response.mime_type` | Safe final main-document projection. Completeness requires exactly one root final-document response with HTTP `200` and HTML or XHTML MIME. |
167
+ | `document_response.context_fingerprint`, `document_response.capture_origin`, `document_response.final_origin`, `document_response.origin_matches_capture` | Hashed browser document context and same-origin redirect binding. Raw frame/loader IDs are never persisted. |
168
+ | `metrics.total_transferred_bytes`, `metrics.request_count`, `metrics.largest_resource` | Totals over retained ledger entries; `largest_resource` carries only resource ID, redacted URL, type, bytes, and request count. |
169
+ | `metrics.cross_origin_request_count`, `metrics.cache_request_count`, `metrics.service_worker_request_count` | Counts used to expose cross-origin traffic and completeness-invalidating cache/service-worker observations. |
170
+ | `networkidle.status`, `networkidle.duration_ms` | `settled`, `timeout`, or `invalid`. Measured duration starts immediately before navigation and ends when network-idle settles or times out; synthetic producer failures use `invalid` / `null`, not a fabricated duration. It is evidence timing, not a performance SLA. |
171
+ | `media_collection.status` and count fields | `observed_element_count`, `failed_element_count`, `omitted_element_count`, `source_overflow_element_count`, and `ancestor_overflow_element_count` explain complete, partial, or failed DOM measurement. |
172
+ | `media[]` source fields | `tag_name`, `element_index`, `current_src`, `src_attribute`, `source_src_attributes[]`, `observed_source_urls[]`, and normalized `source_references[]` retain the initial and post-network-idle source history needed for resource attribution. |
173
+ | `media[]` state and transfer fields | `preload_attribute`, `preload_defers_fetch`, `hidden_at_load`, `hidden_by[]`, `zero_size_at_load`, `fetched_bytes`, `declared_bytes`, `fetched_request_count`, and bounded `fetched_resources[]`. Zero-size geometry is evidence only, not hidden-state proof. |
174
+ | `resource_ledger.limit`, `total_resource_count`, `omitted_resource_count`, `omitted_request_count` | Ledger bound and explicit overflow totals. Any omission makes the capture incomplete. |
175
+ | `resource_ledger.entries[]` | Safe URL/resource identity, type and type status, transferred/declared bytes, request/canceled/declared/unmeasured/failed/partial/cross-origin/cache/service-worker counts, HTTP statuses, and match-resource IDs. Queries, fragments, credentials, headers, cookies, bodies, and raw protocol records are excluded. |
176
+ | `problems[]` | Sorted `{ code, count }` capture problems. Any entry outside the warning class forces `measurement_status: "incomplete"`; a warning-class entry (`cross_origin_request_failed`) is recorded without making the capture incomplete. |
177
+ | `integrity.schema_version`, `algorithm`, `association_fingerprint`, `projection_fingerprint` | Versioned SHA-256 tamper-evidence for the deterministic projection and media/resource joins. These checks detect accidental or partial mutation; they are not a keyed signature. |
178
+
179
+ Transfer accounting retains the greater of the terminal CDP encoded length and
180
+ the cumulative `Network.dataReceived` encoded-byte count. A slow, failed, or
181
+ unfinished transfer can therefore contribute an observed lower bound even when
182
+ the terminal measurement is unavailable. Browser-canceled loads and requests
183
+ still in flight when the bounded capture window closes remain complete when
184
+ they have a response and an observed or declared size; they are recorded as
185
+ canceled rather than failed.
186
+
187
+ A genuine failure (`Network.loadingFailed` that is not a cancellation) is
188
+ attributed before it is judged, by the failing resource's origin relative to
189
+ the final document and by its role:
190
+
191
+ - `cross_origin_request_failed` — a cross-origin request in a beacon-class
192
+ role. The beacon class is an explicit allowlist: `ping`, `fetch`, `xhr`,
193
+ `other`, `preflight`. A stale analytics pixel in a merchant tag container
194
+ is the common case. It says nothing about hidden media, so the capture stays
195
+ complete and the checkpoint is evaluated on its merits. The failure is still
196
+ recorded on the ledger entry (`failed_request_count`, with
197
+ `cross_origin_request_count` naming the origin relation) and surfaced in
198
+ `measurement.warnings[]` with the failing origin and with the beacon roles
199
+ the demotion applied to in `resource_types[]`. The roles are named because
200
+ the demotion is a trade-off rather than a fact about the page: an operator
201
+ reading the warning can see whether a failed `ping` was forgiven or a failed
202
+ `fetch` that the page may have depended on, without opening the resource
203
+ ledger. `campaigns-os polish` prints the same list as `Resource types:` in
204
+ its `Capture warnings (not blocking)` block.
205
+ - `dependency_request_failed` — everything else: the document response, any
206
+ first-party resource of any role, and any cross-origin resource outside the
207
+ beacon allowlist — `document`, `script`, `stylesheet`, `image`, `font`,
208
+ `media`, and also `texttrack`, `manifest`, `eventsource`,
209
+ `cspviolationreport`, `prefetch`, `signedexchange`, `websocket`, and an
210
+ unknown or ambiguous type. A failed caption track or CSP report endpoint
211
+ is not a beacon even though nothing renders from it. The failure voids the
212
+ collection: `response_collection.status` becomes `failed`,
213
+ `response_collection_failed` is added, and the capture is incomplete and
214
+ nonwaivable, exactly as before. Widening the beacon allowlist is an
215
+ operator-visible trade-off, not a tidy-up.
216
+
217
+ A failed request has no transfer size by definition, so it is never also
218
+ counted as `transfer_size_unavailable` or in the entry's
219
+ `unmeasured_request_count`. Both attributed counts are recomputed from the
220
+ resource ledger at evaluation time; a capture whose problems disagree with its
221
+ ledger, or that declares its collection complete over a ledger-recorded
222
+ dependency failure, is `capture_shape_invalid` and blocks.
223
+
224
+ That recomputation is one of an ordered table of shape rules. Each rule names
225
+ one statement a capture makes about itself (its metrics, its media totals, its
226
+ collection statuses, the problem counts its ledger implies) and recomputes it
227
+ with the producer's own derivation, exported from `polish-capture.mjs`, so the
228
+ producer and the validator cannot drift apart. The rule names are exported as
229
+ `POLISH_CAPTURE_SHAPE_RULES` from `polish-page-load.mjs`, and
230
+ `captureShapeViolation(capture)` returns the first rule a capture breaks (or
231
+ `null`). A `measurement.incomplete[]` entry whose codes include
232
+ `capture_shape_invalid` carries that name as `shape_violation`; the name is a
233
+ fixed token from the table and never capture content. Rules run in order and
234
+ the first failure is the one reported, so `integrity` reports for any capture
235
+ whose fields fall outside the projected vocabulary, and the later rules only
236
+ ever name a contradiction between values the capture could have produced.
237
+
238
+ For canceled responses, the collector also retains the declared body size from
239
+ `Content-Range`'s total when available, falling back to `Content-Length`.
240
+ Observed transferred bytes remain unchanged; when no transfer bytes were
241
+ observed, the response omits that measurement and the resource ledger retains a
242
+ zero-byte lower bound alongside its non-zero declaration. This accounted
243
+ declaration does not become an unavailable-transfer problem. The resource ledger
244
+ keeps the largest declared total across repeated requests for one URL, avoiding
245
+ range-request double counting; a media element sums those totals only across its
246
+ distinct matched resources. The hidden-eager-media checkpoint
247
+ compares the larger of observed and declared bytes, so an early-aborted range
248
+ load cannot make a large hidden video look small. Declared sizes do not add a
249
+ measurement problem and are ignored for visible media and exact `preload="none"`
250
+ or `preload="metadata"` exemptions.
251
+
252
+ Producer waits are owned and bounded. The built-in browser bounds launch and
253
+ per-cell work at 45 seconds and cleanup at 5 seconds, beneath the orchestration
254
+ startup/cell bound of 55 seconds and final-close bound of 10 seconds. A stuck
255
+ launch, DOM/CDP wait, teardown, or close records the fixed `producer_timeout`
256
+ problem, retires that adapter generation, records remaining matrix cells as
257
+ incomplete without overlapping the late operation, and requires a fresh
258
+ `polish capture`. Raw browser errors and operation details are not persisted.
259
+
260
+ Bounds are part of the evidence semantics: at most 128 packet route mappings,
261
+ 4,096 response records, 2,048 resource-ledger entries, 512 media elements, 32
262
+ child-source attributes and 32 observed source-history URLs per element, 64
263
+ ancestor styles per element, and 8,192 characters per captured URL. A
264
+ route-plan overflow aborts before capture. Other overflow is recorded through
265
+ counts/sentinels and a problem code, then blocks as incomplete rather than
266
+ silently truncating into a pass.
267
+
268
+ The owned checkpoint is `polish.hidden_eager_media`. A finding requires one
269
+ computed-hidden `video` or `audio` element whose aggregate assessed bytes are
270
+ strictly greater than `1,048,576`; assessed bytes are the larger of observed
271
+ transfer and canceled-response declared size. `display:none`, `visibility:hidden`, or
272
+ `visibility:collapse` on the element or an ancestor counts as hidden. Zero-size
273
+ geometry is evidence only.
274
+ Exact ASCII-case-insensitive `preload="none"` and `preload="metadata"` defer the
275
+ finding; surrounding whitespace does not. Visible media and media exactly at
276
+ the threshold pass this checkpoint.
277
+
278
+ Measurement completeness is nonwaivable. Missing/malformed evidence, a stale
279
+ build/campaign/route/viewport binding, final-document route mismatch, integrity
280
+ mismatch, unfinished transfers, dependency request failures, cache/service-worker
281
+ observations, unjoinable media sources, and resource-ledger contradictions all
282
+ block until a fresh capture succeeds. A failed cross-origin beacon-class request
283
+ is the one recorded problem that does not: it is a warning, not a completeness
284
+ defect. URLs in persisted resources and findings drop query,
285
+ fragment, credentials, headers, cookies, bodies, and raw CDP/DOM records.
286
+
287
+ Only a complete real finding is waivable. The decision binds the current build,
288
+ slug, route scope, routes, fixed viewports, and stable finding state:
289
+
290
+ ```bash
291
+ campaigns-os checkpoint waive \
292
+ --packet campaign-runtime.build.json \
293
+ --gate polish.hidden_eager_media \
294
+ --reason "<why this exact finding is accepted>" \
295
+ --waived-by "<named human>" \
296
+ --review-condition "<specific re-evaluation trigger>"
297
+ ```
298
+
299
+ Changing the finding, build, slug, routes, or viewports makes the decision
300
+ inert. An active decision stays visible as `waived` / `ready_with_waivers`; it
301
+ never becomes a clean pass.
302
+
303
+ ## 3. Semantic content checks
304
+
305
+ Three categories are read, not just presence-checked. The gate flattens every
306
+ string/number/boolean inside the value and pattern-matches the joined text —
307
+ so **affirm the cleared outcome; do not echo the residue tokens you removed**
308
+ (describing deleted residue reads as residue).
309
+
310
+ Everywhere free text is scanned, an explicit negative — `not found`,
311
+ `none found`, `not present`, `no starter …` / `no template …` — reads as
312
+ clean.
313
+
314
+ ### 3.1 `brand_review.favicon` — the authoritative certification shape
315
+
316
+ A **structured certification record is authoritative** and skips the free-text
317
+ leak scan entirely:
318
+
319
+ ```jsonc
320
+ "favicon": { "byte_match": true, "status": "matched_source" }
321
+ ```
322
+
323
+ Accepted certification: `byte_match: true`, **or** `status` (alias `result`)
324
+ equal to one of:
325
+
326
+ - `matched_source` — built favicon byte-matches a prepared-source favicon
327
+ - `promoted_source` — a source favicon was promoted into the build
328
+ - `confirmed_non_template` — verified not the starter/template favicon
329
+ - `no_source_candidate` — the prepared source ships no favicon candidate
330
+ (documented outcome, not a pass-by-omission)
331
+
332
+ Escape semantics: without a certifying record, the favicon value's free text is
333
+ scanned for starter-favicon leakage (`starter/template favicon
334
+ found|present|matched|leaked|retained|kept|remaining`, or the literal starter
335
+ path `images/favicon.png`). A certified record may safely *mention* the starter
336
+ path (e.g. "replaced assets/images/favicon.png"); free-text-only evidence may
337
+ not.
338
+
339
+ When the Build Brief sets `template_residue_policy.block_template_favicon:
340
+ true`, the favicon evidence must certify (structured record above, or free text
341
+ that affirms a source/brand match, a promoted source, a confirmed
342
+ non-template favicon, or "no source candidate"). In that mode
343
+ `byte_match: false` is an explicit block **even when an accepted `status` is
344
+ also present** — the two checks are independent, so when byte comparison is
345
+ inapplicable (e.g. `no_source_candidate`), omit `byte_match` rather than
346
+ recording `false`. The policy flag lives at
347
+ `report.build_brief.artifact.template_residue_policy.block_template_favicon`;
348
+ when it is absent or false, only the leak-text scan applies.
349
+
350
+ ### 3.2 Payment-chrome assets: remove or rename, never edit in place
351
+
352
+ The assets listed under `default_residue.payment_chrome.assets` in the family's
353
+ brand contract are keyed by QA on the **referenced basename**. Editing one in
354
+ place — stripping the PayPal or Klarna marks from inside a shared strip such as
355
+ `upsell-payment-logos.svg` — leaves the basename referenced, so the page still
356
+ reads as carrying chrome it no longer has.
357
+
358
+ Delete the asset, or author a replacement under a new name and repoint the
359
+ reference. Recording the edit in polish evidence does not change how QA keys it.
360
+
361
+ QA fetches a referenced `.svg` and hashes the served bytes against the shipped
362
+ starter hash recorded in the shared-commerce contract
363
+ (`payment_chrome.asset_sha256`, pinned to a starter-templates commit by
364
+ `asset_pin.sha`). The shipped bytes are the untouched starter strip and block
365
+ as residue, whatever the markup says — the starter's `upsell-payment-logos.svg`
366
+ draws its PayPal wordmark as path data and names no method. Bytes that differ
367
+ and no longer mention the method are an edit in place, and that assertion is
368
+ downgraded from a blocker to `manual_review` — the verdict lands on
369
+ `ready_with_exceptions` and no autonomous repair is dispatched. That is a
370
+ safety net for a mistake already made, not a supported workflow: the
371
+ downgrade only applies to assets QA can fetch and read, so a raster, an
372
+ unreachable URL, or an edited file that still names the method still blocks.
373
+
374
+ ### 3.3 `template_residue_review.starter_favicon`
375
+
376
+ Same certification shape and same leak scan as §3.1: a certifying record
377
+ (`byte_match: true` / accepted `status`) is authoritative; otherwise the text
378
+ must not indicate starter-favicon leakage.
379
+
380
+ ### 3.3 `checkout_review` structured fields
381
+
382
+ Two entries are required inside `checkout_review`:
383
+
384
+ - **`field_labels`** (aliases: `initial_field_hints`, `visible_labels`) —
385
+ confirm the initial checkout field labels/placeholders/hints are legible.
386
+ Missing → blocked. Text indicating `missing`, `absent`, `blank`,
387
+ `unlabeled`, `placeholder-stripped`, or `not legible` → blocked.
388
+ - **`bump_compare_price_rule`** (alias: `bump_compare_price`) — confirm no
389
+ order-bump renders an equal / no-discount compare (strike-through) price.
390
+ Missing → blocked. `equal_compare_price_found: true` or
391
+ `same_price_compare_rendered: true` → blocked, as does free text reporting an
392
+ equal/duplicate/no-discount compare price. Negations like "no equal compare
393
+ price found" read as clean.
394
+
395
+ ### 3.4 `brand_review.brand_bleed` (cloned-source de-brand pass)
396
+
397
+ Field precedence: `brand_bleed`, then aliases `brand_bleed_review`, `debrand`.
398
+ The field must exist; the canonical cleared form is an object with
399
+ `cleared: true` (accepted directly, no further text scan). `cleared: false`,
400
+ `bleed_found: true`, or `residual_found: true` block. Free-text evidence is
401
+ scanned per residue kind (promo/sale code or copy, prior-campaign favicon,
402
+ scaffold/non-design fonts, hardcoded non-token colors) — see the
403
+ `next-campaigns-polish` skill for the full recording guidance and pitfalls.
404
+
405
+ ## 4. Blocker-code ladder (evaluation order)
406
+
407
+ The gate returns the **first** failing code; fix in this order.
408
+
409
+ | Code | Meaning / fix |
410
+ |---|---|
411
+ | `polish.report_missing` | No assembly report where one is required. Run the pipeline stages first. |
412
+ | `polish.not_applicable` | Assembly not complete yet (not a block). |
413
+ | `polish.build_fingerprint_missing` | Build never recorded `stages.assembly.build_fingerprint`. Re-run build. |
414
+ | `polish.assembly_source_package_fingerprint_missing` | Assembly not tied to the current Design Source Package. Re-run build (or waive, §5). |
415
+ | `polish.assembly_source_package_stale` | Source package changed after build. Re-run build (or waive, §5). |
416
+ | `polish.waiver_expires_at_invalid` | A source freshness waiver has an unparseable `expires_at`. Fix or remove the waiver record (§5). |
417
+ | `polish.evidence_missing` | No `stages.polish` stage, or status neither completed nor blocked. Run next-campaigns-polish. |
418
+ | `polish.blocked` | Polish itself recorded `status: blocked`. Resolve its blockers. |
419
+ | `polish.self_certified` | `performed_by` is not `next-campaigns-polish`, or the record mentions build commands. |
420
+ | `polish.source_build_fingerprint_missing` | Evidence not tied to any build fingerprint. |
421
+ | `polish.stale` | Evidence tied to an older build fingerprint. Re-run polish. |
422
+ | `polish.output_drift` | The built output under `_site/<slug>/` no longer matches `stages.assembly.build_fingerprint` (doctor and QA recompute it; `current_output_fingerprint` carries the value). Re-run build, record the fingerprint, then re-run polish. |
423
+ | `polish.source_package_material_fingerprint_missing` | Evidence not tied to the current source package fingerprint. |
424
+ | `polish.source_package_stale` | Source package changed after polish. Re-run polish. |
425
+ | `polish.completed_at_missing` | No completion timestamp on stage or evidence. |
426
+ | `polish.evidence_incomplete` | One or more §2/§3 problems; the `problems` array names each. |
427
+ | `polish.hidden_eager_media.capture_malformed` | Package capture is missing/malformed, or packet/report authority is inconsistent. Repair authority when named, then capture again. |
428
+ | `polish.hidden_eager_media.capture_stale` | Page-load evidence is bound to a different build, campaign, route scope, route set, or viewport set. Recapture. |
429
+ | `polish.hidden_eager_media.capture_incomplete` | One or more required route/viewport measurements failed completeness. Repair the named capture problem and recapture; this is not waivable. The blocked checkpoint carries `measurement` (the recomputed `status`, counts, and the `missing[]`, `duplicate[]`, `unexpected[]` and `incomplete[]` cells with their `problem_codes[]`), and the QA verdict's `polish.hidden_eager_media` assertion projects it as `evidence.measurement` (one 256-cell budget across the four lists, the full 128-route, two-viewport matrix; anything past it, and any record outside the closed route/viewport vocabularies, is counted in `omitted_cell_count` and per list in `omitted_cell_count_by_list`; counts are integers), so the failing route, viewport and problem code are readable from the verdict alone. `dependency_request_failed` names a document, first-party, or script/stylesheet/media failure; a `cross_origin_request_failed` warning alone never produces this code. When the capture origin is `http:` and a failed dependency is a cross-origin `http:` resource — a protocol-relative vendor loader (`//host/...`) from a production build served over plain HTTP — the first required action is `polish.hidden_eager_media.local_proof_rebuild`: rebuild in the development environment (local proof mode, `deploy.target: local-serve`) and recapture. Never edit the generated include that emits the loader. |
430
+ | `polish.hidden_eager_media` | Complete evidence contains hidden eager media strictly above the threshold. Repair and recapture, or record an exact named-human checkpoint waiver. |
431
+ | `polish.evidence_current` (pass) / `polish.assembly_source_package_waived` (waived) | Gate satisfied. |
432
+
433
+ ## 5. Source-package freshness waiver
434
+
435
+ The two assembly-freshness blocks (§1) accept a recorded waiver on the report
436
+ (`report.waivers[]`, `report.assembly_source_package_freshness_waiver`, or
437
+ `report.source_package_freshness_waiver`). A waiver is only honored when it has
438
+ ALL of: a non-empty `reason`; a matching `scope`
439
+ (`assembly_source_package_freshness`, `source_package_after_build`,
440
+ `source_package_stale_after_build`) **or** an `applies_to[]` entry naming one of
441
+ the fingerprint fields/blocker codes; attribution (`waived_by` or `owner`); a
442
+ timestamp (`waived_at` or `created_at`); and a bound (`expires_at` or
443
+ `review_condition`). When `expires_at` is present it must be a parseable
444
+ timestamp (ISO 8601): an unparseable value blocks the gate with
445
+ `polish.waiver_expires_at_invalid`, and an expiry at or before the evaluation
446
+ instant means the waiver is **not** honored (the boundary is inclusive — do
447
+ not record the current run timestamp as `expires_at`; give the waiver real
448
+ headroom). An expired waiver means the freshness block fires as if no waiver
449
+ were recorded
450
+ (the verdict names the expired waiver so a fresh one can be recorded
451
+ deliberately). Waived runs pass with status `waived`, never silently.
452
+
453
+ ## 6. Complete passing example
454
+
455
+ The hand-authored portion below is completed first. Run `campaigns-os polish
456
+ capture` to attach the versioned `visual_review.page_load` object; that package
457
+ artifact is intentionally not reproduced as editable JSON here.
458
+
459
+ ```jsonc
460
+ "stages": {
461
+ "assembly": { "status": "completed", "build_fingerprint": "sha256:1f0a…33ee" },
462
+ "polish": {
463
+ "stage": "polish",
464
+ "status": "completed",
465
+ "performed_by": "next-campaigns-polish",
466
+ "source_build_fingerprint": "sha256:1f0a…33ee",
467
+ "completed_at": "2026-08-02T17:40:00Z",
468
+ "evidence": {
469
+ "visual_review": {
470
+ "screenshots": [
471
+ ".campaign-runtime/polish/landing-desktop.png",
472
+ ".campaign-runtime/polish/checkout-mobile.png"
473
+ ],
474
+ "notes": "desktop + mobile commerce anchors compared against prepared source"
475
+ },
476
+ "brand_review": {
477
+ "favicon": { "byte_match": true, "status": "matched_source" },
478
+ "brand_bleed": { "cleared": true, "promo_codes": "none", "fonts": "design fonts only", "colors": "tokenized" }
479
+ },
480
+ "checkout_review": {
481
+ "field_labels": "initial card/email/address hints legible in native-looking controls",
482
+ "bump_compare_price_rule": { "equal_compare_price_found": false, "note": "bump shows discounted vs compare price" }
483
+ },
484
+ "template_residue_review": {
485
+ "starter_favicon": { "byte_match": true, "status": "matched_source" },
486
+ "copy": "starter headings replaced from prepared source; placeholder scan clean"
487
+ },
488
+ "commerce_flow_review": "bundle selector single-select verified; express wallet mount present; upsell wiring untouched",
489
+ "issues": [],
490
+ "commands": ["campaigns-os next polish --packet campaign-runtime.build.json"]
491
+ }
492
+ }
493
+ }
494
+ ```
495
+
496
+ If a Design Source Package fingerprint exists on the report, also record
497
+ `source_package_material_fingerprint` on the stage (or evidence) with the
498
+ current value.
499
+
500
+ Polish evidence certifies the polish pass only — it is not QA and does not
501
+ certify launch readiness (`docs/qa-and-test-orders.md` owns the QA proof
502
+ stack).