@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.
- package/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- 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).
|