@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,739 @@
|
|
|
1
|
+
import { HIDDEN_EAGER_MEDIA_SCOPE } from "./polish-page-load.mjs";
|
|
2
|
+
import { HIDDEN_EAGER_MEDIA_ACTIONS } from "./gate-actions.mjs";
|
|
3
|
+
|
|
4
|
+
export const POLISH_GATE_REQUIRED_EVIDENCE = Object.freeze([
|
|
5
|
+
"visual_review",
|
|
6
|
+
"brand_review",
|
|
7
|
+
"checkout_review",
|
|
8
|
+
"template_residue_review",
|
|
9
|
+
"commerce_flow_review",
|
|
10
|
+
"issues",
|
|
11
|
+
"commands",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const POLISH_PRODUCER = "next-campaigns-polish";
|
|
15
|
+
|
|
16
|
+
const SUCCESS_STATUS_PREFIXES = Object.freeze(["completed"]);
|
|
17
|
+
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nonEmptyString(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeString(value) {
|
|
27
|
+
return nonEmptyString(value) ? value.trim() : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeStringArray(value) {
|
|
31
|
+
return Array.isArray(value) ? value.map((entry) => normalizeString(entry)).filter(Boolean) : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stageStatus(stage) {
|
|
35
|
+
return String(stage?.status || "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stageSucceeded(stage) {
|
|
39
|
+
const status = stageStatus(stage);
|
|
40
|
+
return SUCCESS_STATUS_PREFIXES.some((prefix) => status.startsWith(prefix));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function terminalAssembly(report) {
|
|
44
|
+
return String(report?.stages?.assembly?.status || "").startsWith("completed");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function currentBuildFingerprint(report) {
|
|
48
|
+
return normalizeString(report?.stages?.assembly?.build_fingerprint)
|
|
49
|
+
|| normalizeString(report?.stages?.assembly?.artifact_fingerprint)
|
|
50
|
+
|| normalizeString(report?.build_fingerprint)
|
|
51
|
+
|| normalizeString(report?.artifact_fingerprint)
|
|
52
|
+
|| null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function currentSourcePackageMaterialFingerprint(report) {
|
|
56
|
+
return normalizeString(report?.design_source_package?.material_fingerprint)
|
|
57
|
+
|| normalizeString(report?.inputs?.design_source_package?.material_fingerprint)
|
|
58
|
+
|| normalizeString(report?.source_package?.material_fingerprint)
|
|
59
|
+
|| normalizeString(report?.source_package_material_fingerprint)
|
|
60
|
+
|| null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assemblySourcePackageMaterialFingerprint(report) {
|
|
64
|
+
const assembly = report?.stages?.assembly;
|
|
65
|
+
return normalizeString(assembly?.source_package_material_fingerprint)
|
|
66
|
+
|| normalizeString(assembly?.evidence?.source_package_material_fingerprint)
|
|
67
|
+
|| normalizeString(report?.assembly?.source_package_material_fingerprint)
|
|
68
|
+
|| null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// One predicate for "this report claims a Design Source Package but Build never
|
|
72
|
+
// tied itself to it". The polish gate, `doctor`/`next` (through the gate) and
|
|
73
|
+
// the standalone `validate-assembly-report` all answer that question from here,
|
|
74
|
+
// so the three paths cannot drift: before this existed the gate blocked and the
|
|
75
|
+
// validator stayed silent on the same report.
|
|
76
|
+
//
|
|
77
|
+
// It carries the gate's own preconditions, not just the fingerprint
|
|
78
|
+
// comparison. A report still pending Assembly (the shape `prepare-build` and
|
|
79
|
+
// `start` emit, which records the package fingerprint before any build has
|
|
80
|
+
// consumed it) and a report with no build fingerprint yet are both outside
|
|
81
|
+
// this finding: the gate answers not_applicable or blocks on
|
|
82
|
+
// polish.build_fingerprint_missing there, and so the validator must not fail
|
|
83
|
+
// them on freshness.
|
|
84
|
+
//
|
|
85
|
+
// Callers that have already assessed the freshness waivers pass that
|
|
86
|
+
// assessment in rather than paying for a second scan of the same records; the
|
|
87
|
+
// gate does. A malformed waiver is a separate finding — the gate's
|
|
88
|
+
// polish.waiver_expires_at_invalid, mirrored by the validator — and both
|
|
89
|
+
// callers report it before asking this question, so a report whose only
|
|
90
|
+
// waiver record is malformed fails on that finding alone. Asked directly, this
|
|
91
|
+
// predicate treats a malformed record as no active waiver.
|
|
92
|
+
export function assemblySourcePackageFingerprintMissing(report, now = Date.now(), waiverAssessment = null) {
|
|
93
|
+
if (!terminalAssembly(report)) return false;
|
|
94
|
+
if (!currentBuildFingerprint(report)) return false;
|
|
95
|
+
if (!currentSourcePackageMaterialFingerprint(report)) return false;
|
|
96
|
+
if (assemblySourcePackageMaterialFingerprint(report)) return false;
|
|
97
|
+
const assessment = waiverAssessment || assessAssemblySourcePackageFreshnessWaivers(report, now);
|
|
98
|
+
return !assessment.active;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function assessAssemblySourcePackageFreshnessWaivers(report, now = Date.now()) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
...(Array.isArray(report?.waivers) ? report.waivers : []),
|
|
104
|
+
report?.assembly_source_package_freshness_waiver,
|
|
105
|
+
report?.source_package_freshness_waiver,
|
|
106
|
+
].filter(Boolean);
|
|
107
|
+
const assessment = { active: null, expired: [], invalid: [] };
|
|
108
|
+
for (const waiver of candidates) {
|
|
109
|
+
if (!isObject(waiver) || !normalizeString(waiver.reason)) continue;
|
|
110
|
+
const scope = normalizeString(waiver.scope);
|
|
111
|
+
const appliesTo = normalizeStringArray(waiver.applies_to);
|
|
112
|
+
const scopeMatches = [
|
|
113
|
+
"assembly_source_package_freshness",
|
|
114
|
+
"source_package_after_build",
|
|
115
|
+
"source_package_stale_after_build",
|
|
116
|
+
].includes(scope);
|
|
117
|
+
const appliesToMatches = appliesTo.some((entry) => [
|
|
118
|
+
"stages.assembly.source_package_material_fingerprint",
|
|
119
|
+
"design_source_package.material_fingerprint",
|
|
120
|
+
"polish.assembly_source_package_fingerprint_missing",
|
|
121
|
+
"polish.assembly_source_package_stale",
|
|
122
|
+
].includes(entry));
|
|
123
|
+
const attributed = Boolean(normalizeString(waiver.waived_by) || normalizeString(waiver.owner));
|
|
124
|
+
const timestamped = Boolean(normalizeString(waiver.waived_at) || normalizeString(waiver.created_at));
|
|
125
|
+
if (!(scopeMatches || appliesToMatches) || !attributed || !timestamped) continue;
|
|
126
|
+
const expiresAt = normalizeString(waiver.expires_at);
|
|
127
|
+
if (expiresAt !== null) {
|
|
128
|
+
// expires_at must parse as a real timestamp: an unparseable value is a
|
|
129
|
+
// malformed record the gate refuses to honor, and an explicit expiry
|
|
130
|
+
// that has passed dominates any review_condition on the same record.
|
|
131
|
+
// The boundary is inclusive — expires_at exactly equal to now is
|
|
132
|
+
// expired. Selection is first-valid-wins: expired and invalid records
|
|
133
|
+
// are never selected regardless of position, so order only matters
|
|
134
|
+
// between multiple currently-valid waivers.
|
|
135
|
+
const parsed = Date.parse(expiresAt);
|
|
136
|
+
if (Number.isNaN(parsed)) {
|
|
137
|
+
assessment.invalid.push(waiver);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (parsed <= now) {
|
|
141
|
+
assessment.expired.push(waiver);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!assessment.active) assessment.active = waiver;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (normalizeString(waiver.review_condition) && !assessment.active) assessment.active = waiver;
|
|
148
|
+
}
|
|
149
|
+
return assessment;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function polishEvidence(stage, report) {
|
|
153
|
+
if (isObject(stage?.evidence)) return stage.evidence;
|
|
154
|
+
if (isObject(report?.polish?.evidence)) return report.polish.evidence;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function fieldHasEvidence(value, field) {
|
|
159
|
+
if (field === "issues") return Array.isArray(value);
|
|
160
|
+
if (field === "commands") {
|
|
161
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
162
|
+
return value.some((entry) => nonEmptyString(entry) || isObject(entry));
|
|
163
|
+
}
|
|
164
|
+
if (field === "visual_review") {
|
|
165
|
+
if (!isObject(value)) return false;
|
|
166
|
+
const screenshots = value.screenshots || value.screenshot_paths || value.paths || value.urls;
|
|
167
|
+
return Array.isArray(screenshots) && screenshots.some(nonEmptyString);
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
170
|
+
if (isObject(value)) return Object.keys(value).length > 0;
|
|
171
|
+
return nonEmptyString(value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function textFragments(value) {
|
|
175
|
+
if (Array.isArray(value)) return value.flatMap(textFragments);
|
|
176
|
+
if (isObject(value)) return Object.values(value).flatMap(textFragments);
|
|
177
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return [String(value)];
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function reviewText(value) {
|
|
182
|
+
return textFragments(value).join(" ");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function hasNegativeEvidence(value, pattern) {
|
|
186
|
+
const text = reviewText(value);
|
|
187
|
+
if (/\bnot\s+found\b|\bnone\s+found\b|\bnot\s+present\b|\bno\s+(?:starter|template)\b/i.test(text)) return false;
|
|
188
|
+
return pattern.test(text);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildBriefBlocksTemplateFavicon(report) {
|
|
192
|
+
// Canonical Build Brief location after prepare-build normalization.
|
|
193
|
+
const policy = report?.build_brief?.artifact?.template_residue_policy || null;
|
|
194
|
+
return policy?.block_template_favicon === true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const STARTER_FAVICON_LEAK_RE = /\b(?:starter|template)\s+favicon\s+(?:found|present|matched|leaked|retained|kept|remaining)|images\/favicon\.png\b/i;
|
|
198
|
+
|
|
199
|
+
function faviconRecordCertifiesOk(record) {
|
|
200
|
+
return isObject(record) && (
|
|
201
|
+
record.byte_match === true
|
|
202
|
+
|| ["matched_source", "promoted_source", "confirmed_non_template", "no_source_candidate"].includes(String(record.status || record.result || ""))
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function faviconTextConfirmsSourceMatch(text) {
|
|
207
|
+
return /(?:byte[-_\s]?match|matched|matches|matching).{0,40}(?:source|brand|candidate)|(?:source|brand|candidate).{0,40}(?:matched|matches|matching|byte[-_\s]?match)|promoted.{0,40}source|confirmed.{0,40}(?:non[-_\s]?template|not[-_\s]?template)|\b(?:no|none)\s+source\s+candidate\b/i.test(text);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hasNegativeBumpCompareEvidence(value) {
|
|
211
|
+
const text = reviewText(value);
|
|
212
|
+
if (/\b(?:no|none)\s+(?:equal|same|duplicate|doubled|no[-_\s]?discount).{0,24}(?:compare|strike|original|price)\s*(?:found|present|rendered|shown|visible)?\b/i.test(text)) return false;
|
|
213
|
+
return hasNegativeEvidence(value, /\b(?:equal|same)\s+(?:compare|strike|original)\s+price\s+(?:found|present|rendered|shown|visible)\b|\b(?:equal|same)\s+price\s+(?:compare|strike|original)\s+(?:found|present|rendered|shown|visible)\b|\b(?:compare|strike|original)\s+price\s+(?:equals|===|same\s+as)\s+(?:list|full|retail|original)\b|\b(?:doubled|duplicate)\s+(?:compare|strike|original|price)(?:\s+(?:row|price))?\s+(?:found|present|rendered|shown|visible)\b|\bno[-_\s]?discount\s+(?:compare|strike|original)\s+(?:rendered|shown|visible|present|found)\b/i);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function semanticEvidenceProblems(evidence, report) {
|
|
217
|
+
const problems = [];
|
|
218
|
+
const brandReview = evidence?.brand_review;
|
|
219
|
+
const checkoutReview = evidence?.checkout_review;
|
|
220
|
+
const residueReview = evidence?.template_residue_review;
|
|
221
|
+
|
|
222
|
+
const favicon = isObject(brandReview) ? brandReview.favicon : null;
|
|
223
|
+
const faviconCertifiedOk = faviconRecordCertifiesOk(favicon);
|
|
224
|
+
if (buildBriefBlocksTemplateFavicon(report)) {
|
|
225
|
+
const faviconText = reviewText(favicon);
|
|
226
|
+
const faviconTextOk = faviconTextConfirmsSourceMatch(faviconText);
|
|
227
|
+
if (!faviconCertifiedOk && !faviconTextOk) {
|
|
228
|
+
problems.push("stages.polish.evidence.brand_review.favicon must confirm source/brand favicon matching or a documented no-source-candidate outcome when block_template_favicon is true.");
|
|
229
|
+
}
|
|
230
|
+
if (isObject(favicon) && favicon.byte_match === false) {
|
|
231
|
+
problems.push("stages.polish.evidence.brand_review.favicon records byte_match=false while block_template_favicon is true.");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// A structured record that explicitly certifies the favicon (byte_match /
|
|
235
|
+
// accepted status) is authoritative — do not let a note that merely mentions
|
|
236
|
+
// the starter path (e.g. "copied over assets/images/favicon.png") read as
|
|
237
|
+
// leakage. Free-text-only evidence still gets the negative scan.
|
|
238
|
+
if (!faviconCertifiedOk && hasNegativeEvidence(favicon, STARTER_FAVICON_LEAK_RE)) {
|
|
239
|
+
problems.push("stages.polish.evidence.brand_review.favicon still indicates starter-template favicon leakage.");
|
|
240
|
+
}
|
|
241
|
+
if (!faviconRecordCertifiesOk(residueReview?.starter_favicon) && hasNegativeEvidence(residueReview?.starter_favicon, STARTER_FAVICON_LEAK_RE)) {
|
|
242
|
+
problems.push("stages.polish.evidence.template_residue_review.starter_favicon still indicates starter-template favicon leakage.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const fieldEvidence = checkoutReview?.field_labels ?? checkoutReview?.initial_field_hints ?? checkoutReview?.visible_labels;
|
|
246
|
+
if (!fieldEvidence) {
|
|
247
|
+
problems.push("stages.polish.evidence.checkout_review must confirm initial checkout field labels/placeholders/hints.");
|
|
248
|
+
} else if (hasNegativeEvidence(fieldEvidence, /\b(?:missing|absent|blank|unlabeled|unlabelled|placeholder[-_\s]?stripped|not\s+legible)\b/i)) {
|
|
249
|
+
problems.push("stages.polish.evidence.checkout_review.field_labels must confirm a legible initial field hint/label state.");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const bumpEvidence = checkoutReview?.bump_compare_price_rule ?? checkoutReview?.bump_compare_price;
|
|
253
|
+
if (!bumpEvidence) {
|
|
254
|
+
problems.push("stages.polish.evidence.checkout_review must confirm the order-bump compare-price rule.");
|
|
255
|
+
} else if ((isObject(bumpEvidence) && (bumpEvidence.equal_compare_price_found === true || bumpEvidence.same_price_compare_rendered === true)) || hasNegativeBumpCompareEvidence(bumpEvidence)) {
|
|
256
|
+
problems.push("stages.polish.evidence.checkout_review.bump_compare_price_rule must confirm no equal/no-discount compare price renders.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Brand bleed: when a campaign is cloned from a proven sibling, the sibling's
|
|
260
|
+
// brand defaults ride along — a default promo/sale banner with a fake code,
|
|
261
|
+
// hardcoded non-token colors (e.g. next-core's #C670FE pill), scaffold fonts
|
|
262
|
+
// (Plus Jakarta), and the prior favicon. Polish must affirmatively certify
|
|
263
|
+
// the de-brand pass cleared them. See the Shield build learnings (A3).
|
|
264
|
+
// Field precedence: `brand_bleed` is canonical; `brand_bleed_review` and
|
|
265
|
+
// `debrand` are accepted aliases (in that order) for forward compat.
|
|
266
|
+
const bleedEvidence = brandReview?.brand_bleed ?? brandReview?.brand_bleed_review ?? brandReview?.debrand;
|
|
267
|
+
if (bleedEvidence === undefined || bleedEvidence === null) {
|
|
268
|
+
problems.push("stages.polish.evidence.brand_review.brand_bleed must confirm the cloned-source de-brand pass: no residual promo/sale code or copy, no prior-campaign favicon, no scaffold/non-design fonts, no hardcoded non-token colors.");
|
|
269
|
+
} else {
|
|
270
|
+
const kinds = brandBleedResidualKinds(bleedEvidence);
|
|
271
|
+
if (kinds.length) {
|
|
272
|
+
problems.push(`stages.polish.evidence.brand_review.brand_bleed still indicates cloned-source brand bleed (${kinds.join(", ")}).`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return problems;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Per-kind brand-bleed residue detection. Returns the kinds that still leaked,
|
|
280
|
+
// so the gate message names the actual residue. Each kind has its own phrase
|
|
281
|
+
// vocabulary rather than one monolithic regex.
|
|
282
|
+
const BRAND_BLEED_RESIDUE_PATTERNS = [
|
|
283
|
+
["residual promo/sale code or copy", /\b(?:promo|sale|discount|coupon)\b[^.;]{0,40}\b(?:code|copy|banner|sale)\b[^.;]{0,40}\b(?:found|present|remain(?:s|ing)?|leaked|retained|kept|still)\b|\b(?:found|present|remain(?:s|ing)?|leaked|retained|kept|still)\b[^.;]{0,40}\b(?:promo|sale|discount|coupon)\b[^.;]{0,40}\b(?:code|copy|banner)\b|\bfake\s+(?:code|sale|coupon)\b/i],
|
|
284
|
+
["prior-campaign favicon", /\bprior[-_\s]?campaign\b|\bsibling[-_\s]?(?:brand|campaign|source)\b/i],
|
|
285
|
+
["scaffold/non-design fonts", /\bscaffold\s+fonts?\b|\bplus\s+jakarta\b|\bnon[-_\s]?design\s+fonts?\b/i],
|
|
286
|
+
["hardcoded non-token color", /(?:^|[^\w])#?c670fe(?:[^\w]|$)|\bhardcoded\s+(?:non[-_\s]?token\s+)?(?:colou?r|hex|purple)\b|\bnon[-_\s]?token\s+colou?r\s+(?:found|present|remain(?:s|ing)?)\b/i],
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
function brandBleedResidualKinds(value) {
|
|
290
|
+
if (isObject(value)) {
|
|
291
|
+
if (value.cleared === false || value.bleed_found === true || value.residual_found === true) return ["de-brand pass not cleared"];
|
|
292
|
+
if (value.cleared === true || value.bleed_found === false || value.residual_found === false) return [];
|
|
293
|
+
}
|
|
294
|
+
const text = reviewText(value);
|
|
295
|
+
// Explicit "not cleared" wording blocks first — a free-form "cleared: false"
|
|
296
|
+
// must not slip through just because no residue *kind* phrase happens to match.
|
|
297
|
+
// Tightened to avoid over-blocking benign clearances: the boolean form
|
|
298
|
+
// requires a literal "false" (not "no", which collides with "cleared: no
|
|
299
|
+
// bleed"), and bleed/residual "found" is excluded when preceded by "no" (so
|
|
300
|
+
// "no bleed found" reads as cleared, not as residue).
|
|
301
|
+
if (/\bnot\s+cleared\b|\bcleared\s*[:=-]\s*false\b|\b(?<!no\s)(?:bleed|residual)\s+(?:found|present|detected|remain(?:s|ing)?)\b|\bnot\s+de[-_\s]?branded\b/i.test(text)) {
|
|
302
|
+
return ["de-brand pass not cleared"];
|
|
303
|
+
}
|
|
304
|
+
// Otherwise an explicit cleared/none statement short-circuits the phrase scan.
|
|
305
|
+
if (/\bcleared\b|\bno\s+(?:bleed|residue|residual)\b|\bde[-_\s]?brand(?:ed|\s+pass)\b/i.test(text)) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
const kinds = [];
|
|
309
|
+
for (const [label, pattern] of BRAND_BLEED_RESIDUE_PATTERNS) {
|
|
310
|
+
if (hasNegativeEvidence(value, pattern)) kinds.push(label);
|
|
311
|
+
}
|
|
312
|
+
return kinds;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function evidenceProblems(evidence, report) {
|
|
316
|
+
if (!isObject(evidence)) {
|
|
317
|
+
return ["stages.polish.evidence must be an object with the required polish evidence categories."];
|
|
318
|
+
}
|
|
319
|
+
const problems = [];
|
|
320
|
+
for (const field of POLISH_GATE_REQUIRED_EVIDENCE) {
|
|
321
|
+
if (!fieldHasEvidence(evidence[field], field)) {
|
|
322
|
+
problems.push(`stages.polish.evidence.${field} is missing or incomplete.`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
problems.push(...semanticEvidenceProblems(evidence, report));
|
|
326
|
+
return problems;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function performedBy(stage, evidence) {
|
|
330
|
+
return normalizeString(stage?.performed_by)
|
|
331
|
+
|| normalizeString(stage?.command_identity)
|
|
332
|
+
|| normalizeString(evidence?.performed_by)
|
|
333
|
+
|| null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function sourceBuildFingerprint(stage, evidence) {
|
|
337
|
+
return normalizeString(stage?.source_build_fingerprint)
|
|
338
|
+
|| normalizeString(evidence?.source_build_fingerprint)
|
|
339
|
+
|| null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function sourcePackageMaterialFingerprint(stage, evidence) {
|
|
343
|
+
return normalizeString(stage?.source_package_material_fingerprint)
|
|
344
|
+
|| normalizeString(evidence?.source_package_material_fingerprint)
|
|
345
|
+
|| null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function completedAt(stage, evidence) {
|
|
349
|
+
return normalizeString(stage?.completed_at)
|
|
350
|
+
|| normalizeString(evidence?.completed_at)
|
|
351
|
+
|| null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function commandMentionsBuild(stage, evidence) {
|
|
355
|
+
const commands = [
|
|
356
|
+
...(Array.isArray(stage?.commands) ? stage.commands : []),
|
|
357
|
+
...(Array.isArray(evidence?.commands) ? evidence.commands : []),
|
|
358
|
+
];
|
|
359
|
+
return commands.some((entry) => /next-campaigns-build|campaigns-os\s+next\s+build/i.test(String(isObject(entry) ? entry.command || entry.name || "" : entry)));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function evaluateStructuredPolishGate({ report, required = false, now = Date.now(), currentOutputFingerprint = null } = {}) {
|
|
363
|
+
if (!isObject(report)) {
|
|
364
|
+
return required
|
|
365
|
+
? {
|
|
366
|
+
status: "blocked",
|
|
367
|
+
code: "polish.report_missing",
|
|
368
|
+
reason: "Polish evidence missing for current build. Run next-campaigns-polish before QA.",
|
|
369
|
+
required_actions: [{ id: "run_polish", kind: "skill", command: "next-campaigns-polish", description: "Run the distinct Polish stage and record structured evidence on stages.polish." }],
|
|
370
|
+
}
|
|
371
|
+
: { status: "not_applicable", code: "polish.not_applicable", reason: "No assembly report is available to evaluate polish evidence." };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!terminalAssembly(report)) {
|
|
375
|
+
return {
|
|
376
|
+
status: "not_applicable",
|
|
377
|
+
code: "polish.not_applicable",
|
|
378
|
+
reason: "Assembly is not completed yet; polish evidence is required after build completion.",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const buildFingerprint = currentBuildFingerprint(report);
|
|
383
|
+
const currentSourcePackageFingerprint = currentSourcePackageMaterialFingerprint(report);
|
|
384
|
+
const assemblySourcePackageFingerprint = assemblySourcePackageMaterialFingerprint(report);
|
|
385
|
+
const waiverAssessment = assessAssemblySourcePackageFreshnessWaivers(report, now);
|
|
386
|
+
const sourcePackageFreshnessWaiver = waiverAssessment.active;
|
|
387
|
+
const expiredWaiver = waiverAssessment.expired[0] || null;
|
|
388
|
+
const stage = report.stages?.polish;
|
|
389
|
+
const evidence = polishEvidence(stage, report);
|
|
390
|
+
const buildRequiredActions = [
|
|
391
|
+
{
|
|
392
|
+
id: "rerun_build",
|
|
393
|
+
kind: "skill",
|
|
394
|
+
command: "next-campaigns-build",
|
|
395
|
+
description: "Re-run Build/Assembly against the current Design Source Package and record stages.assembly.source_package_material_fingerprint before Polish.",
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
const requiredActions = [
|
|
399
|
+
{
|
|
400
|
+
id: "run_polish",
|
|
401
|
+
kind: "skill",
|
|
402
|
+
command: "next-campaigns-polish",
|
|
403
|
+
description: "Run the distinct Polish stage and record stages.polish.performed_by, source_build_fingerprint, source_package_material_fingerprint when available, completed_at, and structured evidence.",
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
if (waiverAssessment.invalid.length) {
|
|
408
|
+
const invalid = waiverAssessment.invalid[0];
|
|
409
|
+
return {
|
|
410
|
+
status: "blocked",
|
|
411
|
+
code: "polish.waiver_expires_at_invalid",
|
|
412
|
+
reason: `Source freshness waiver has an unparseable expires_at (${JSON.stringify(invalid.expires_at)}). Record a valid ISO 8601 timestamp, or remove the malformed waiver, before Polish/QA.`,
|
|
413
|
+
build_fingerprint: buildFingerprint,
|
|
414
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
415
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
416
|
+
waiver: invalid,
|
|
417
|
+
required_actions: [
|
|
418
|
+
{
|
|
419
|
+
id: "repair_waiver",
|
|
420
|
+
kind: "manual",
|
|
421
|
+
command: null,
|
|
422
|
+
description: "Correct the waiver's expires_at to a valid ISO 8601 timestamp (or remove the malformed waiver record) on the assembly report.",
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!buildFingerprint) {
|
|
429
|
+
return {
|
|
430
|
+
status: "blocked",
|
|
431
|
+
code: "polish.build_fingerprint_missing",
|
|
432
|
+
reason: "Current build artifact fingerprint is missing. Build must record stages.assembly.build_fingerprint before Polish/QA handoff.",
|
|
433
|
+
required_actions: requiredActions,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (assemblySourcePackageFingerprintMissing(report, now, waiverAssessment)) {
|
|
438
|
+
return {
|
|
439
|
+
status: "blocked",
|
|
440
|
+
code: "polish.assembly_source_package_fingerprint_missing",
|
|
441
|
+
reason: expiredWaiver
|
|
442
|
+
? `Assembly is not tied to the current Design Source Package material fingerprint, and the recorded source freshness waiver expired at ${expiredWaiver.expires_at}. Re-run Build before Polish (or record a new waiver).`
|
|
443
|
+
: "Assembly is not tied to the current Design Source Package material fingerprint. Re-run Build before Polish.",
|
|
444
|
+
build_fingerprint: buildFingerprint,
|
|
445
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
446
|
+
...(expiredWaiver ? { expired_waiver: expiredWaiver } : {}),
|
|
447
|
+
required_actions: buildRequiredActions,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (currentSourcePackageFingerprint && assemblySourcePackageFingerprint !== currentSourcePackageFingerprint && !sourcePackageFreshnessWaiver) {
|
|
451
|
+
return {
|
|
452
|
+
status: "blocked",
|
|
453
|
+
code: "polish.assembly_source_package_stale",
|
|
454
|
+
reason: expiredWaiver
|
|
455
|
+
? `The Design Source Package changed after Build, and the recorded source freshness waiver expired at ${expiredWaiver.expires_at}. Re-run Build against the current source package before Polish (or record a new waiver).`
|
|
456
|
+
: "The Design Source Package changed after Build. Re-run Build against the current source package before Polish.",
|
|
457
|
+
build_fingerprint: buildFingerprint,
|
|
458
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
459
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
460
|
+
...(expiredWaiver ? { expired_waiver: expiredWaiver } : {}),
|
|
461
|
+
required_actions: buildRequiredActions,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!isObject(stage)) {
|
|
466
|
+
return {
|
|
467
|
+
status: "blocked",
|
|
468
|
+
code: "polish.evidence_missing",
|
|
469
|
+
reason: "Polish evidence missing for current build. Run next-campaigns-polish before QA.",
|
|
470
|
+
build_fingerprint: buildFingerprint,
|
|
471
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
472
|
+
required_actions: requiredActions,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const status = stageStatus(stage);
|
|
477
|
+
if (status === "blocked") {
|
|
478
|
+
return {
|
|
479
|
+
status: "blocked",
|
|
480
|
+
code: "polish.blocked",
|
|
481
|
+
reason: "Polish is recorded as blocked. Resolve the Polish blockers before QA.",
|
|
482
|
+
build_fingerprint: buildFingerprint,
|
|
483
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
484
|
+
required_actions: requiredActions,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (!stageSucceeded(stage)) {
|
|
488
|
+
return {
|
|
489
|
+
status: "blocked",
|
|
490
|
+
code: "polish.evidence_missing",
|
|
491
|
+
reason: "Polish evidence missing for current build. Run next-campaigns-polish before QA.",
|
|
492
|
+
build_fingerprint: buildFingerprint,
|
|
493
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
494
|
+
required_actions: requiredActions,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const producer = performedBy(stage, evidence);
|
|
499
|
+
if (producer !== POLISH_PRODUCER || commandMentionsBuild(stage, evidence)) {
|
|
500
|
+
return {
|
|
501
|
+
status: "blocked",
|
|
502
|
+
code: "polish.self_certified",
|
|
503
|
+
reason: `Polish success must be produced by ${POLISH_PRODUCER}, not ${producer || "an unspecified or build-owned producer"}. Run a distinct Polish stage before QA.`,
|
|
504
|
+
build_fingerprint: buildFingerprint,
|
|
505
|
+
performed_by: producer,
|
|
506
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
507
|
+
required_actions: requiredActions,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const sourceFingerprint = sourceBuildFingerprint(stage, evidence);
|
|
512
|
+
if (!sourceFingerprint) {
|
|
513
|
+
return {
|
|
514
|
+
status: "blocked",
|
|
515
|
+
code: "polish.source_build_fingerprint_missing",
|
|
516
|
+
reason: "Polish evidence is not tied to the current build artifact fingerprint. Re-run next-campaigns-polish against the current build before QA.",
|
|
517
|
+
build_fingerprint: buildFingerprint,
|
|
518
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
519
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
520
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
521
|
+
required_actions: requiredActions,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// The value the evidence must match is the fingerprint of the output on
|
|
525
|
+
// disk when the caller could recompute it (doctor, QA), not the string build
|
|
526
|
+
// recorded: evidence that matches a recorded value the output has since
|
|
527
|
+
// drifted from is stale in exactly the same way.
|
|
528
|
+
const outputFingerprint = normalizeString(currentOutputFingerprint);
|
|
529
|
+
if (outputFingerprint && outputFingerprint !== buildFingerprint) {
|
|
530
|
+
return {
|
|
531
|
+
status: "blocked",
|
|
532
|
+
code: "polish.output_drift",
|
|
533
|
+
reason: `Built output no longer matches stages.assembly.build_fingerprint (recorded ${buildFingerprint}, current ${outputFingerprint}); polish evidence is bound to a build that no longer exists. Re-run build, then next-campaigns-polish before QA.`,
|
|
534
|
+
build_fingerprint: buildFingerprint,
|
|
535
|
+
current_output_fingerprint: outputFingerprint,
|
|
536
|
+
source_build_fingerprint: sourceFingerprint,
|
|
537
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
538
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
539
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
540
|
+
required_actions: mergeRequiredActions([
|
|
541
|
+
{
|
|
542
|
+
id: "rerun_build",
|
|
543
|
+
kind: "skill",
|
|
544
|
+
command: "next-campaigns-build",
|
|
545
|
+
description: "Re-run Build (page-kit build) and record the current output fingerprint (doctor --json derived.build_output_fingerprint.value) on stages.assembly.build_fingerprint before Polish.",
|
|
546
|
+
},
|
|
547
|
+
], requiredActions),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
if (sourceFingerprint !== buildFingerprint) {
|
|
551
|
+
return {
|
|
552
|
+
status: "blocked",
|
|
553
|
+
code: "polish.stale",
|
|
554
|
+
reason: "Polish evidence is stale for the current build artifact fingerprint. Re-run next-campaigns-polish before QA.",
|
|
555
|
+
build_fingerprint: buildFingerprint,
|
|
556
|
+
source_build_fingerprint: sourceFingerprint,
|
|
557
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
558
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
559
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
560
|
+
required_actions: requiredActions,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const polishSourcePackageFingerprint = sourcePackageMaterialFingerprint(stage, evidence);
|
|
565
|
+
if (currentSourcePackageFingerprint && !polishSourcePackageFingerprint) {
|
|
566
|
+
return {
|
|
567
|
+
status: "blocked",
|
|
568
|
+
code: "polish.source_package_material_fingerprint_missing",
|
|
569
|
+
reason: "Polish evidence is not tied to the current Design Source Package material fingerprint. Re-run next-campaigns-polish against the current source package before QA.",
|
|
570
|
+
build_fingerprint: buildFingerprint,
|
|
571
|
+
source_build_fingerprint: sourceFingerprint,
|
|
572
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
573
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
574
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
575
|
+
required_actions: requiredActions,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (currentSourcePackageFingerprint && polishSourcePackageFingerprint !== currentSourcePackageFingerprint) {
|
|
579
|
+
return {
|
|
580
|
+
status: "blocked",
|
|
581
|
+
code: "polish.source_package_stale",
|
|
582
|
+
reason: "Polish evidence is stale for the current Design Source Package material fingerprint. Re-run next-campaigns-polish before QA.",
|
|
583
|
+
build_fingerprint: buildFingerprint,
|
|
584
|
+
source_build_fingerprint: sourceFingerprint,
|
|
585
|
+
source_package_material_fingerprint: polishSourcePackageFingerprint,
|
|
586
|
+
current_source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
587
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
588
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
589
|
+
required_actions: requiredActions,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!completedAt(stage, evidence)) {
|
|
594
|
+
return {
|
|
595
|
+
status: "blocked",
|
|
596
|
+
code: "polish.completed_at_missing",
|
|
597
|
+
reason: "Polish evidence is missing completed_at. Re-run or repair the Polish stage record before QA.",
|
|
598
|
+
build_fingerprint: buildFingerprint,
|
|
599
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
600
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
601
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
602
|
+
required_actions: requiredActions,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const problems = evidenceProblems(evidence, report);
|
|
607
|
+
if (problems.length) {
|
|
608
|
+
return {
|
|
609
|
+
status: "blocked",
|
|
610
|
+
code: "polish.evidence_incomplete",
|
|
611
|
+
reason: "Polish evidence is incomplete for the current build. Run next-campaigns-polish before QA.",
|
|
612
|
+
build_fingerprint: buildFingerprint,
|
|
613
|
+
source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
614
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
615
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
616
|
+
problems,
|
|
617
|
+
required_actions: requiredActions,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const warnings = [];
|
|
622
|
+
if (!currentSourcePackageFingerprint) {
|
|
623
|
+
warnings.push({
|
|
624
|
+
code: "polish.source_package_material_fingerprint_unavailable",
|
|
625
|
+
message: "No current Design Source Package material fingerprint is available; Polish freshness is using legacy build-fingerprint-only behavior.",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const waivedAssemblyFreshness = Boolean(sourcePackageFreshnessWaiver);
|
|
630
|
+
return {
|
|
631
|
+
status: waivedAssemblyFreshness ? "waived" : "pass",
|
|
632
|
+
code: waivedAssemblyFreshness ? "polish.assembly_source_package_waived" : "polish.evidence_current",
|
|
633
|
+
reason: waivedAssemblyFreshness
|
|
634
|
+
? `Polish evidence is current, structured, and produced by next-campaigns-polish under source freshness waiver: ${sourcePackageFreshnessWaiver.reason}`
|
|
635
|
+
: "Polish evidence is current, structured, and produced by next-campaigns-polish.",
|
|
636
|
+
build_fingerprint: buildFingerprint,
|
|
637
|
+
source_build_fingerprint: sourceFingerprint,
|
|
638
|
+
source_package_material_fingerprint: polishSourcePackageFingerprint || null,
|
|
639
|
+
current_source_package_material_fingerprint: currentSourcePackageFingerprint,
|
|
640
|
+
assembly_source_package_material_fingerprint: assemblySourcePackageFingerprint,
|
|
641
|
+
performed_by: producer,
|
|
642
|
+
waiver: sourcePackageFreshnessWaiver,
|
|
643
|
+
warnings,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function mergeRequiredActions(...groups) {
|
|
648
|
+
const actions = [];
|
|
649
|
+
const seen = new Set();
|
|
650
|
+
for (const action of groups.flat()) {
|
|
651
|
+
if (!isObject(action) || !nonEmptyString(action.id) || seen.has(action.id)) continue;
|
|
652
|
+
seen.add(action.id);
|
|
653
|
+
actions.push(action);
|
|
654
|
+
}
|
|
655
|
+
return actions;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function ownedCheckpointBlock(hiddenEagerMediaGate) {
|
|
659
|
+
if (isObject(hiddenEagerMediaGate)
|
|
660
|
+
&& hiddenEagerMediaGate.id === HIDDEN_EAGER_MEDIA_SCOPE
|
|
661
|
+
&& hiddenEagerMediaGate.status === "blocked"
|
|
662
|
+
&& nonEmptyString(hiddenEagerMediaGate.code)
|
|
663
|
+
&& nonEmptyString(hiddenEagerMediaGate.reason)) {
|
|
664
|
+
return {
|
|
665
|
+
code: hiddenEagerMediaGate.code,
|
|
666
|
+
reason: hiddenEagerMediaGate.reason,
|
|
667
|
+
required_actions: mergeRequiredActions(hiddenEagerMediaGate.required_actions || []),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
code: "polish.hidden_eager_media.capture_malformed",
|
|
672
|
+
reason: "Package-owned page-load evidence is missing or malformed; run polish capture before completing Polish.",
|
|
673
|
+
required_actions: [HIDDEN_EAGER_MEDIA_ACTIONS.capture],
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// The hidden eager-media checkpoint is a package-owned category of Polish,
|
|
678
|
+
// but its authority depends on the current packet route plan. Callers compute
|
|
679
|
+
// that checkpoint from their one packet/report snapshot and inject it here;
|
|
680
|
+
// this evaluator never self-reads a second, potentially inconsistent snapshot.
|
|
681
|
+
export function evaluatePolishGate({
|
|
682
|
+
report,
|
|
683
|
+
required = false,
|
|
684
|
+
now = Date.now(),
|
|
685
|
+
hiddenEagerMediaGate = undefined,
|
|
686
|
+
currentOutputFingerprint = null,
|
|
687
|
+
} = {}) {
|
|
688
|
+
const base = evaluateStructuredPolishGate({ report, required, now, currentOutputFingerprint });
|
|
689
|
+
if (!terminalAssembly(report)) return base;
|
|
690
|
+
if (hiddenEagerMediaGate === undefined && !required) return base;
|
|
691
|
+
|
|
692
|
+
const ownedId = HIDDEN_EAGER_MEDIA_SCOPE;
|
|
693
|
+
const hiddenStatus = hiddenEagerMediaGate?.id === ownedId
|
|
694
|
+
? hiddenEagerMediaGate.status
|
|
695
|
+
: null;
|
|
696
|
+
if (hiddenStatus !== "pass" && hiddenStatus !== "waived") {
|
|
697
|
+
const owned = ownedCheckpointBlock(hiddenEagerMediaGate);
|
|
698
|
+
if (base.status === "blocked") {
|
|
699
|
+
return {
|
|
700
|
+
...base,
|
|
701
|
+
required_actions: mergeRequiredActions(base.required_actions || [], owned.required_actions),
|
|
702
|
+
owned_checkpoint_id: ownedId,
|
|
703
|
+
owned_checkpoint_status: "blocked",
|
|
704
|
+
owned_checkpoint_only: false,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
...base,
|
|
709
|
+
status: "blocked",
|
|
710
|
+
code: owned.code,
|
|
711
|
+
reason: owned.reason,
|
|
712
|
+
required_actions: owned.required_actions,
|
|
713
|
+
owned_checkpoint_id: ownedId,
|
|
714
|
+
owned_checkpoint_status: "blocked",
|
|
715
|
+
owned_checkpoint_only: true,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (hiddenStatus === "waived" && base.status !== "blocked") {
|
|
720
|
+
const hiddenOnly = base.status === "pass";
|
|
721
|
+
return {
|
|
722
|
+
...base,
|
|
723
|
+
status: "waived",
|
|
724
|
+
code: hiddenOnly ? hiddenEagerMediaGate.code : base.code,
|
|
725
|
+
reason: `${base.reason} ${hiddenEagerMediaGate.reason}`,
|
|
726
|
+
...(hiddenOnly && hiddenEagerMediaGate.waiver ? { waiver: hiddenEagerMediaGate.waiver } : {}),
|
|
727
|
+
owned_checkpoint_id: ownedId,
|
|
728
|
+
owned_checkpoint_status: "waived",
|
|
729
|
+
owned_checkpoint_only: hiddenOnly,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
...base,
|
|
735
|
+
owned_checkpoint_id: ownedId,
|
|
736
|
+
owned_checkpoint_status: hiddenStatus,
|
|
737
|
+
owned_checkpoint_only: false,
|
|
738
|
+
};
|
|
739
|
+
}
|