@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,639 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { HIDDEN_EAGER_MEDIA_ACTIONS } from "./gate-actions.mjs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { computeBuildFingerprint } from "./built-site-scope.mjs";
|
|
5
|
+
import {
|
|
6
|
+
buildPageLoadCapture,
|
|
7
|
+
MAX_POLISH_CAPTURE_URL_LENGTH,
|
|
8
|
+
normalizePageLoadRoute,
|
|
9
|
+
plainHttpDependencyFailures,
|
|
10
|
+
} from "./polish-capture.mjs";
|
|
11
|
+
import {
|
|
12
|
+
buildPolishPageLoadEvidence,
|
|
13
|
+
evaluateHiddenEagerMediaCheckpoint,
|
|
14
|
+
HIDDEN_EAGER_MEDIA_SCOPE,
|
|
15
|
+
POLISH_PAGE_LOAD_PRODUCER,
|
|
16
|
+
POLISH_PAGE_LOAD_SCHEMA_VERSION,
|
|
17
|
+
} from "./polish-page-load.mjs";
|
|
18
|
+
import {
|
|
19
|
+
boundedPolishDeadline,
|
|
20
|
+
POLISH_BROWSER_UNAVAILABLE_ERROR_CODE,
|
|
21
|
+
POLISH_CAPTURE_CELL_DEADLINE_MS,
|
|
22
|
+
POLISH_CAPTURE_CLOSE_DEADLINE_MS,
|
|
23
|
+
POLISH_CAPTURE_STARTUP_DEADLINE_MS,
|
|
24
|
+
POLISH_PRODUCER_CLEANUP_ERROR_CODE,
|
|
25
|
+
POLISH_PRODUCER_TIMEOUT_ERROR_CODE,
|
|
26
|
+
polishProducerCleanupError,
|
|
27
|
+
polishProducerTimeoutError,
|
|
28
|
+
runWithPolishProducerDeadline,
|
|
29
|
+
} from "./polish-deadline.mjs";
|
|
30
|
+
import {
|
|
31
|
+
assemblySourcePackageMaterialFingerprint,
|
|
32
|
+
currentSourcePackageMaterialFingerprint,
|
|
33
|
+
} from "./polish-gate.mjs";
|
|
34
|
+
|
|
35
|
+
export const POLISH_CAPTURE_VIEWPORTS = Object.freeze([
|
|
36
|
+
Object.freeze({ key: "desktop", width: 1_440, height: 1_200 }),
|
|
37
|
+
Object.freeze({ key: "mobile", width: 390, height: 844 }),
|
|
38
|
+
]);
|
|
39
|
+
export const MAX_POLISH_CAPTURE_ROUTES = 128;
|
|
40
|
+
|
|
41
|
+
function isPlainObject(value) {
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
43
|
+
const prototype = Object.getPrototypeOf(value);
|
|
44
|
+
return prototype === Object.prototype || prototype === null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nonemptyString(value) {
|
|
48
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function captureBaseUrl(value) {
|
|
52
|
+
if (typeof value !== "string" || value.length > MAX_POLISH_CAPTURE_URL_LENGTH) {
|
|
53
|
+
throw new Error("polish capture requires a bounded resolvable HTTP(S) --base-url.");
|
|
54
|
+
}
|
|
55
|
+
let url;
|
|
56
|
+
try {
|
|
57
|
+
url = new URL(String(value));
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error("polish capture requires a resolvable HTTP(S) --base-url.");
|
|
60
|
+
}
|
|
61
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
62
|
+
throw new Error("polish capture requires a resolvable HTTP(S) --base-url.");
|
|
63
|
+
}
|
|
64
|
+
return url;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mappedPublicRoute(value, pageId) {
|
|
68
|
+
if (typeof value !== "string" || value.length > MAX_POLISH_CAPTURE_URL_LENGTH) {
|
|
69
|
+
throw new Error(`Polish capture page "${pageId}" has an unresolvable page_kit.public_route.`);
|
|
70
|
+
}
|
|
71
|
+
const raw = nonemptyString(value);
|
|
72
|
+
if (!raw || !raw.startsWith("/") || raw.startsWith("//") || /[?#\\]/.test(raw)) {
|
|
73
|
+
throw new Error(`Polish capture page "${pageId}" has an unresolvable page_kit.public_route.`);
|
|
74
|
+
}
|
|
75
|
+
const normalized = normalizePageLoadRoute(raw);
|
|
76
|
+
if (!normalized) throw new Error(`Polish capture page "${pageId}" has an unresolvable page_kit.public_route.`);
|
|
77
|
+
const route = normalized === "/" ? "/" : `${normalized.replace(/\/+$/, "")}/`;
|
|
78
|
+
return route;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mappedSpecRoute(value, pageId) {
|
|
82
|
+
if (typeof value !== "string" || value.length > MAX_POLISH_CAPTURE_URL_LENGTH) {
|
|
83
|
+
throw new Error(`Polish capture page "${pageId}" is missing page_kit.spec_route.`);
|
|
84
|
+
}
|
|
85
|
+
const raw = value.trim();
|
|
86
|
+
if (/[?#\\]/.test(raw) || /^[a-z][a-z0-9+.-]*:/i.test(raw)) {
|
|
87
|
+
throw new Error(`Polish capture page "${pageId}" has an unresolvable page_kit.spec_route.`);
|
|
88
|
+
}
|
|
89
|
+
const normalized = raw === "" ? "/" : normalizePageLoadRoute(raw);
|
|
90
|
+
if (!normalized) throw new Error(`Polish capture page "${pageId}" has an unresolvable page_kit.spec_route.`);
|
|
91
|
+
const route = normalized === "/" ? "/" : `${normalized.replace(/\/+$/, "")}/`;
|
|
92
|
+
return route;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function planPolishCapture({ packet, baseUrl } = {}) {
|
|
96
|
+
if (!isPlainObject(packet) || !Array.isArray(packet?.source_html?.pages) || packet.source_html.pages.length === 0) {
|
|
97
|
+
throw new Error("polish capture requires packet.source_html.pages mappings.");
|
|
98
|
+
}
|
|
99
|
+
if (packet.source_html.pages.length > MAX_POLISH_CAPTURE_ROUTES) {
|
|
100
|
+
throw new Error(`polish capture supports at most ${MAX_POLISH_CAPTURE_ROUTES} packet route mappings per run.`);
|
|
101
|
+
}
|
|
102
|
+
const base = captureBaseUrl(baseUrl);
|
|
103
|
+
const routes = [];
|
|
104
|
+
let skipped = false;
|
|
105
|
+
|
|
106
|
+
for (const mapping of packet.source_html.pages) {
|
|
107
|
+
const pageId = typeof mapping?.page_id === "string" && mapping.page_id.length <= 128
|
|
108
|
+
? nonemptyString(mapping.page_id)
|
|
109
|
+
: null;
|
|
110
|
+
if (!pageId) throw new Error("Every polish capture page mapping needs page_id.");
|
|
111
|
+
if (nonemptyString(mapping?.skip_reason)) {
|
|
112
|
+
skipped = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!nonemptyString(mapping?.path)) {
|
|
116
|
+
throw new Error(`Polish capture page "${pageId}" is neither mapped nor explicitly skipped.`);
|
|
117
|
+
}
|
|
118
|
+
if (!isPlainObject(mapping?.page_kit)) {
|
|
119
|
+
throw new Error(`Polish capture page "${pageId}" is missing its page_kit route mapping.`);
|
|
120
|
+
}
|
|
121
|
+
const requestedRoute = mappedPublicRoute(mapping.page_kit.public_route, pageId);
|
|
122
|
+
const specRoute = mappedSpecRoute(mapping.page_kit.spec_route, pageId);
|
|
123
|
+
routes.push({
|
|
124
|
+
page_id: pageId,
|
|
125
|
+
requested_route: requestedRoute,
|
|
126
|
+
spec_route: specRoute,
|
|
127
|
+
url: new URL(requestedRoute, base).href,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (routes.length === 0) throw new Error("polish capture has no mapped non-skipped routes to capture.");
|
|
132
|
+
routes.sort((a, b) => a.requested_route.localeCompare(b.requested_route) || a.page_id.localeCompare(b.page_id));
|
|
133
|
+
for (let index = 1; index < routes.length; index += 1) {
|
|
134
|
+
if (routes[index - 1].requested_route === routes[index].requested_route) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Polish capture routes are duplicated at ${routes[index].requested_route} `
|
|
137
|
+
+ `(${routes[index - 1].page_id}, ${routes[index].page_id}).`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
route_scope: skipped ? "selected" : "all",
|
|
144
|
+
routes,
|
|
145
|
+
viewports: POLISH_CAPTURE_VIEWPORTS,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The recorded actions live in gate-actions.mjs so polish-gate, which this
|
|
150
|
+
// module imports, can publish the same capture action without a copy.
|
|
151
|
+
function recordedCheckpointActions(gate, { authorityMalformed = false, browserUnavailable = false, plainHttpProductionBuild = false } = {}) {
|
|
152
|
+
const { capture, install_browser, waive, repair, repair_authority, local_proof_rebuild } = HIDDEN_EAGER_MEDIA_ACTIONS;
|
|
153
|
+
if (gate?.status !== "blocked") return [];
|
|
154
|
+
if (authorityMalformed) return [repair_authority, capture];
|
|
155
|
+
if (browserUnavailable) return [install_browser, capture];
|
|
156
|
+
// A production build captured over plain HTTP fails on its protocol-relative
|
|
157
|
+
// vendor loaders; the repair is the environment, never the include, so the
|
|
158
|
+
// rebuild action is named ahead of the recapture.
|
|
159
|
+
if (plainHttpProductionBuild) return [local_proof_rebuild, capture];
|
|
160
|
+
return gate.waivable ? [repair, capture, waive] : [capture];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function recordedCaptureHasProblem(pageLoad, code) {
|
|
164
|
+
return Array.isArray(pageLoad?.captures) && pageLoad.captures.some((capture) => Array.isArray(capture?.problems)
|
|
165
|
+
&& capture.problems.some((problem) => problem?.code === code));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function recordedCaptureIsPlainHttpProductionBuild(pageLoad) {
|
|
169
|
+
return Array.isArray(pageLoad?.captures)
|
|
170
|
+
&& pageLoad.captures.some((capture) => plainHttpDependencyFailures(capture).length > 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function recordedAuthorityBlock({ packet, report, plan = null, now } = {}) {
|
|
174
|
+
const malformed = evaluateHiddenEagerMediaCheckpoint({
|
|
175
|
+
pageLoad: null,
|
|
176
|
+
buildFingerprint: report?.stages?.assembly?.build_fingerprint,
|
|
177
|
+
slug: packet?.campaign?.public_route_slug,
|
|
178
|
+
routeScope: plan?.route_scope || "all",
|
|
179
|
+
routes: plan?.routes?.map((route) => route.requested_route) || [],
|
|
180
|
+
viewports: POLISH_CAPTURE_VIEWPORTS.map((viewport) => viewport.key),
|
|
181
|
+
waivers: Array.isArray(report?.waivers) ? report.waivers : [],
|
|
182
|
+
...(now === undefined ? {} : { now }),
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
...malformed,
|
|
186
|
+
reason: "The packet or Assembly Report authority is missing, malformed, or inconsistent; repair it before package capture.",
|
|
187
|
+
required_actions: recordedCheckpointActions(malformed, { authorityMalformed: true }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function recordedCheckpointNotApplicable() {
|
|
192
|
+
return {
|
|
193
|
+
id: HIDDEN_EAGER_MEDIA_SCOPE,
|
|
194
|
+
scope: HIDDEN_EAGER_MEDIA_SCOPE,
|
|
195
|
+
status: "not_applicable",
|
|
196
|
+
checkpoint_status: "not_applicable",
|
|
197
|
+
severity: "info",
|
|
198
|
+
code: "polish.hidden_eager_media.not_applicable",
|
|
199
|
+
reason: "Hidden eager-media page-load evidence is required only after assembly is completed.",
|
|
200
|
+
waivable: false,
|
|
201
|
+
subject: null,
|
|
202
|
+
state: null,
|
|
203
|
+
state_fingerprint: null,
|
|
204
|
+
findings: [],
|
|
205
|
+
waiver: null,
|
|
206
|
+
waiver_assessment: {
|
|
207
|
+
active: null,
|
|
208
|
+
inert_counts: { stale: 0, foreign: 0, malformed: 0, expired: 0 },
|
|
209
|
+
},
|
|
210
|
+
required_actions: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report, now } = {}) {
|
|
215
|
+
const assemblyStatus = nonemptyString(report?.stages?.assembly?.status);
|
|
216
|
+
if (!assemblyStatus?.startsWith("completed")) return recordedCheckpointNotApplicable();
|
|
217
|
+
|
|
218
|
+
let plan;
|
|
219
|
+
try {
|
|
220
|
+
plan = planPolishCapture({ packet, baseUrl: "https://polish-capture.invalid" });
|
|
221
|
+
} catch {
|
|
222
|
+
return recordedAuthorityBlock({ packet, report, now });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const packetSlug = nonemptyString(packet?.campaign?.public_route_slug);
|
|
226
|
+
const reportSlug = nonemptyString(report?.identity?.public_route_slug);
|
|
227
|
+
if (!packetSlug || packetSlug !== reportSlug || !currentBuildFingerprint(report)) {
|
|
228
|
+
return recordedAuthorityBlock({ packet, report, plan, now });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const gate = evaluateHiddenEagerMediaCheckpoint({
|
|
232
|
+
pageLoad: report?.stages?.polish?.evidence?.visual_review?.page_load,
|
|
233
|
+
buildFingerprint: report?.stages?.assembly?.build_fingerprint,
|
|
234
|
+
slug: packetSlug,
|
|
235
|
+
routeScope: plan.route_scope,
|
|
236
|
+
routes: plan.routes.map((route) => route.requested_route),
|
|
237
|
+
viewports: plan.viewports.map((viewport) => viewport.key),
|
|
238
|
+
waivers: Array.isArray(report?.waivers) ? report.waivers : [],
|
|
239
|
+
...(now === undefined ? {} : { now }),
|
|
240
|
+
});
|
|
241
|
+
const pageLoad = report?.stages?.polish?.evidence?.visual_review?.page_load;
|
|
242
|
+
return {
|
|
243
|
+
...gate,
|
|
244
|
+
required_actions: recordedCheckpointActions(gate, {
|
|
245
|
+
browserUnavailable: recordedCaptureHasProblem(pageLoad, "browser_unavailable"),
|
|
246
|
+
plainHttpProductionBuild: recordedCaptureIsPlainHttpProductionBuild(pageLoad),
|
|
247
|
+
}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function canonicalize(value) {
|
|
252
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
253
|
+
if (isPlainObject(value)) {
|
|
254
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function canonicalJson(value) {
|
|
260
|
+
return JSON.stringify(canonicalize(value));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function conflictToken(visualReview) {
|
|
264
|
+
if (!Object.hasOwn(visualReview, "page_load")) return "absent";
|
|
265
|
+
return `sha256:${createHash("sha256").update(canonicalJson({ value: visualReview.page_load })).digest("hex")}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function captureReportAncestors(report) {
|
|
269
|
+
if (!isPlainObject(report)
|
|
270
|
+
|| !isPlainObject(report.stages)
|
|
271
|
+
|| !isPlainObject(report.stages.assembly)
|
|
272
|
+
|| !isPlainObject(report.stages.polish)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"polish capture requires plain-object stages.polish.evidence.visual_review on the Assembly Report.",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const evidence = report.stages.polish.evidence;
|
|
278
|
+
if ((evidence !== undefined && !isPlainObject(evidence))
|
|
279
|
+
|| (isPlainObject(evidence)
|
|
280
|
+
&& evidence.visual_review !== undefined
|
|
281
|
+
&& !isPlainObject(evidence.visual_review))) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
"polish capture requires plain-object stages.polish.evidence.visual_review on the Assembly Report.",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const assemblyStatus = nonemptyString(report.stages.assembly.status);
|
|
287
|
+
if (!assemblyStatus?.startsWith("completed")) {
|
|
288
|
+
throw new Error("polish capture requires completed assembly before browser evidence collection.");
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
assembly: report.stages.assembly,
|
|
292
|
+
visualReview: evidence?.visual_review || {},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function bindingPlanProjection(plan) {
|
|
297
|
+
if (!isPlainObject(plan) || !["all", "selected"].includes(plan.route_scope)
|
|
298
|
+
|| !Array.isArray(plan.routes) || !Array.isArray(plan.viewports)) {
|
|
299
|
+
throw new Error("polish capture binding requires a valid deterministic route plan.");
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
route_scope: plan.route_scope,
|
|
303
|
+
routes: plan.routes.map((route) => ({
|
|
304
|
+
page_id: route.page_id,
|
|
305
|
+
requested_route: route.requested_route,
|
|
306
|
+
spec_route: route.spec_route,
|
|
307
|
+
url: route.url,
|
|
308
|
+
})),
|
|
309
|
+
viewports: plan.viewports.map((viewport) => ({
|
|
310
|
+
key: viewport.key,
|
|
311
|
+
width: viewport.width,
|
|
312
|
+
height: viewport.height,
|
|
313
|
+
})),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function createPolishCaptureBinding({ packet, report, plan, packetPath, targetRepo } = {}) {
|
|
318
|
+
if (!isPlainObject(packet) || packet.schema_version !== "campaign-runtime-build-packet/v0") {
|
|
319
|
+
throw new Error("polish capture requires a current campaign-runtime-build-packet/v0 packet.");
|
|
320
|
+
}
|
|
321
|
+
if (!isPlainObject(report) || report.schema_version !== "campaign-runtime-assembly-report/v0") {
|
|
322
|
+
throw new Error("polish capture requires a current campaign-runtime-assembly-report/v0 report.");
|
|
323
|
+
}
|
|
324
|
+
const boundPacketPath = nonemptyString(packetPath);
|
|
325
|
+
const boundTargetRepo = nonemptyString(targetRepo);
|
|
326
|
+
if (!boundPacketPath || !boundTargetRepo) {
|
|
327
|
+
throw new Error("polish capture binding requires the resolved packet path and target repo.");
|
|
328
|
+
}
|
|
329
|
+
const { assembly, visualReview } = captureReportAncestors(report);
|
|
330
|
+
const slug = captureCampaignSlug(packet, report);
|
|
331
|
+
const packetMapId = nonemptyString(packet?.spec?.map_id);
|
|
332
|
+
const reportMapId = nonemptyString(report?.identity?.map_id);
|
|
333
|
+
if (!packetMapId || packetMapId !== reportMapId) {
|
|
334
|
+
throw new Error("polish capture requires matching packet and Assembly Report map identities.");
|
|
335
|
+
}
|
|
336
|
+
const buildFingerprint = currentBuildFingerprint(report);
|
|
337
|
+
if (!buildFingerprint) throw new Error("polish capture requires a strict current Assembly Report build fingerprint.");
|
|
338
|
+
// Bind to the output on disk, not only to the recorded string: a capture of
|
|
339
|
+
// an output that has drifted from what build recorded would be evidence
|
|
340
|
+
// about a build that no longer exists, and an output that changes during the
|
|
341
|
+
// browser pass fails the unchanged-binding assertion after it.
|
|
342
|
+
const outputFingerprint = boundOutputFingerprint(join(resolve(targetRepo), "_site", slug), slug, buildFingerprint);
|
|
343
|
+
const runId = nonemptyString(report.run_id);
|
|
344
|
+
const reportPacketPath = nonemptyString(report?.inputs?.packet_path);
|
|
345
|
+
if (!runId || !reportPacketPath) {
|
|
346
|
+
throw new Error("polish capture requires the Assembly Report run_id and inputs.packet_path identity.");
|
|
347
|
+
}
|
|
348
|
+
const resolvedPacketPath = resolve(boundPacketPath);
|
|
349
|
+
const resolvedTargetRepo = resolve(boundTargetRepo);
|
|
350
|
+
if (resolve(resolvedTargetRepo, reportPacketPath) !== resolvedPacketPath) {
|
|
351
|
+
throw new Error("polish capture requires the Assembly Report packet path identity to match --packet.");
|
|
352
|
+
}
|
|
353
|
+
const packetTargetRepo = nonemptyString(packet?.assembly?.target_repo);
|
|
354
|
+
if (!packetTargetRepo || resolve(dirname(resolvedPacketPath), packetTargetRepo) !== resolvedTargetRepo) {
|
|
355
|
+
throw new Error("polish capture requires the packet target repo identity to match its resolved target.");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return canonicalize({
|
|
359
|
+
packet: {
|
|
360
|
+
resolved_path: resolvedPacketPath,
|
|
361
|
+
resolved_target_repo: resolvedTargetRepo,
|
|
362
|
+
map_id: packetMapId,
|
|
363
|
+
campaign_slug: slug,
|
|
364
|
+
route_root: nonemptyString(packet?.campaign?.route_root),
|
|
365
|
+
target_repo: nonemptyString(packet?.assembly?.target_repo),
|
|
366
|
+
},
|
|
367
|
+
report: {
|
|
368
|
+
run_id: runId,
|
|
369
|
+
identity: {
|
|
370
|
+
map_id: reportMapId,
|
|
371
|
+
public_route_slug: nonemptyString(report?.identity?.public_route_slug),
|
|
372
|
+
spec_hash: nonemptyString(report?.identity?.spec_hash),
|
|
373
|
+
},
|
|
374
|
+
inputs_packet_path: reportPacketPath,
|
|
375
|
+
assembly: {
|
|
376
|
+
status: nonemptyString(assembly.status),
|
|
377
|
+
build_fingerprint: buildFingerprint,
|
|
378
|
+
output_fingerprint: outputFingerprint,
|
|
379
|
+
source_package_material_fingerprint: assemblySourcePackageMaterialFingerprint(report),
|
|
380
|
+
},
|
|
381
|
+
current_source_package_material_fingerprint: currentSourcePackageMaterialFingerprint(report),
|
|
382
|
+
page_load_conflict_token: conflictToken(visualReview),
|
|
383
|
+
},
|
|
384
|
+
plan: bindingPlanProjection(plan),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function assertPolishCaptureBindingUnchanged(initial, current) {
|
|
389
|
+
if (canonicalJson(initial) !== canonicalJson(current)) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
"Polish capture attachment refused because governing packet/report state or page_load changed during capture.",
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function mergePolishPageLoadEvidence(report, pageLoad) {
|
|
398
|
+
const { visualReview } = captureReportAncestors(report);
|
|
399
|
+
if (!isPlainObject(pageLoad)
|
|
400
|
+
|| pageLoad.schema_version !== POLISH_PAGE_LOAD_SCHEMA_VERSION
|
|
401
|
+
|| pageLoad.performed_by !== POLISH_PAGE_LOAD_PRODUCER) {
|
|
402
|
+
throw new Error("polish capture can attach only package-produced page_load evidence.");
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
...report,
|
|
406
|
+
stages: {
|
|
407
|
+
...report.stages,
|
|
408
|
+
polish: {
|
|
409
|
+
...report.stages.polish,
|
|
410
|
+
evidence: {
|
|
411
|
+
...report.stages.polish.evidence,
|
|
412
|
+
visual_review: {
|
|
413
|
+
...visualReview,
|
|
414
|
+
page_load: pageLoad,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// The fingerprint of the built output the capture binds to. Every way of not
|
|
423
|
+
// having one is a named refusal, parallel to the missing recorded value: no
|
|
424
|
+
// built route root (build has not run here), an output the walk cannot read
|
|
425
|
+
// (permissions, a file vanishing mid-walk), or an output that no longer
|
|
426
|
+
// matches what build recorded. None of them may surface as an uncaught
|
|
427
|
+
// filesystem error, and none may bind as a null.
|
|
428
|
+
function boundOutputFingerprint(outputRoot, slug, buildFingerprint) {
|
|
429
|
+
let current;
|
|
430
|
+
try {
|
|
431
|
+
current = computeBuildFingerprint(outputRoot);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`polish capture refuses: built output under _site/${slug}/ could not be read to fingerprint it `
|
|
435
|
+
+ `(${error?.code || error?.name || "error"}: ${error?.message || error}). Re-run build so the output is readable, then capture.`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (!current.ok) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`polish capture refuses: built output root _site/${slug}/ is missing under the target repo, so there is no build to bind the capture to. `
|
|
441
|
+
+ "Run page-kit build and record stages.assembly.build_fingerprint first.",
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (current.fingerprint !== buildFingerprint) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`polish capture refuses: built output under _site/${slug}/ no longer matches stages.assembly.build_fingerprint `
|
|
447
|
+
+ `(recorded ${buildFingerprint}, current ${current.fingerprint}). Re-run build and record the current fingerprint first.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return current.fingerprint;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function currentBuildFingerprint(report) {
|
|
454
|
+
const fingerprint = report?.stages?.assembly?.build_fingerprint;
|
|
455
|
+
return typeof fingerprint === "string" && /^sha256:[a-f0-9]{64}$/.test(fingerprint)
|
|
456
|
+
? fingerprint
|
|
457
|
+
: null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function captureCampaignSlug(packet, report) {
|
|
461
|
+
const packetSlug = nonemptyString(packet?.campaign?.public_route_slug);
|
|
462
|
+
const reportSlug = nonemptyString(report?.identity?.public_route_slug);
|
|
463
|
+
if (!packetSlug || packetSlug !== reportSlug) {
|
|
464
|
+
throw new Error("polish capture requires matching packet and Assembly Report campaign slugs.");
|
|
465
|
+
}
|
|
466
|
+
return packetSlug;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function capturePolishPageLoad({
|
|
470
|
+
packet,
|
|
471
|
+
report,
|
|
472
|
+
baseUrl,
|
|
473
|
+
headed = false,
|
|
474
|
+
authCookie = null,
|
|
475
|
+
createBrowserAdapter,
|
|
476
|
+
adapterStartupDeadlineMs,
|
|
477
|
+
captureCellDeadlineMs,
|
|
478
|
+
adapterCloseDeadlineMs,
|
|
479
|
+
} = {}) {
|
|
480
|
+
const plan = planPolishCapture({ packet, baseUrl });
|
|
481
|
+
const slug = captureCampaignSlug(packet, report);
|
|
482
|
+
const buildFingerprint = currentBuildFingerprint(report);
|
|
483
|
+
if (!buildFingerprint) throw new Error("polish capture requires a current Assembly Report build fingerprint.");
|
|
484
|
+
if (typeof createBrowserAdapter !== "function") {
|
|
485
|
+
throw new Error("polish capture requires a browser adapter factory.");
|
|
486
|
+
}
|
|
487
|
+
const startupDeadlineMs = boundedPolishDeadline(
|
|
488
|
+
adapterStartupDeadlineMs,
|
|
489
|
+
POLISH_CAPTURE_STARTUP_DEADLINE_MS,
|
|
490
|
+
);
|
|
491
|
+
const cellDeadlineMs = boundedPolishDeadline(captureCellDeadlineMs, POLISH_CAPTURE_CELL_DEADLINE_MS);
|
|
492
|
+
const closeDeadlineMs = boundedPolishDeadline(
|
|
493
|
+
adapterCloseDeadlineMs,
|
|
494
|
+
POLISH_CAPTURE_CLOSE_DEADLINE_MS,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
let adapter = null;
|
|
498
|
+
let adapterStartupError = null;
|
|
499
|
+
let startupTimedOut = false;
|
|
500
|
+
const adapterPromise = Promise.resolve().then(() => createBrowserAdapter({
|
|
501
|
+
headed: headed === true,
|
|
502
|
+
authCookie: nonemptyString(authCookie),
|
|
503
|
+
}));
|
|
504
|
+
void adapterPromise.then((lateAdapter) => {
|
|
505
|
+
if (!startupTimedOut || typeof lateAdapter?.close !== "function") return;
|
|
506
|
+
void runWithPolishProducerDeadline(() => lateAdapter.close(), {
|
|
507
|
+
timeoutMs: closeDeadlineMs,
|
|
508
|
+
unrefTimer: true,
|
|
509
|
+
}).catch(() => {});
|
|
510
|
+
}, () => {});
|
|
511
|
+
try {
|
|
512
|
+
adapter = await runWithPolishProducerDeadline(() => adapterPromise, {
|
|
513
|
+
timeoutMs: startupDeadlineMs,
|
|
514
|
+
onTimeout() { startupTimedOut = true; },
|
|
515
|
+
});
|
|
516
|
+
if (!adapter || typeof adapter.captureRoute !== "function" || typeof adapter.close !== "function") {
|
|
517
|
+
throw new Error("polish capture browser adapter must provide captureRoute() and close().");
|
|
518
|
+
}
|
|
519
|
+
} catch (error) {
|
|
520
|
+
adapterStartupError = error;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const captures = [];
|
|
524
|
+
let adapterCloseError = null;
|
|
525
|
+
let adapterPoisonProblem = null;
|
|
526
|
+
let observedProducerTimeout = false;
|
|
527
|
+
let adapterClosePromise = null;
|
|
528
|
+
const closeAdapter = async () => {
|
|
529
|
+
if (!adapter || typeof adapter.close !== "function") return;
|
|
530
|
+
if (!adapterClosePromise) {
|
|
531
|
+
adapterClosePromise = runWithPolishProducerDeadline(() => adapter.close(), {
|
|
532
|
+
timeoutMs: closeDeadlineMs,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return adapterClosePromise;
|
|
536
|
+
};
|
|
537
|
+
try {
|
|
538
|
+
for (const route of plan.routes) {
|
|
539
|
+
for (const viewport of plan.viewports) {
|
|
540
|
+
let observation;
|
|
541
|
+
try {
|
|
542
|
+
if (adapterStartupError) throw adapterStartupError;
|
|
543
|
+
if (adapterPoisonProblem) {
|
|
544
|
+
throw adapterPoisonProblem === "producer_timeout"
|
|
545
|
+
? polishProducerTimeoutError()
|
|
546
|
+
: polishProducerCleanupError();
|
|
547
|
+
}
|
|
548
|
+
const abortController = new AbortController();
|
|
549
|
+
observation = await runWithPolishProducerDeadline(
|
|
550
|
+
() => adapter.captureRoute({ url: route.url, viewport, signal: abortController.signal }),
|
|
551
|
+
{
|
|
552
|
+
timeoutMs: cellDeadlineMs,
|
|
553
|
+
onTimeout() { abortController.abort(); },
|
|
554
|
+
},
|
|
555
|
+
);
|
|
556
|
+
if (!isPlainObject(observation)) throw new Error("Browser adapter returned no route observation.");
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const browserUnavailable = error?.code === POLISH_BROWSER_UNAVAILABLE_ERROR_CODE;
|
|
559
|
+
const producerTimedOut = error?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
560
|
+
const producerCleanupFailed = error?.code === POLISH_PRODUCER_CLEANUP_ERROR_CODE;
|
|
561
|
+
if (producerTimedOut) observedProducerTimeout = true;
|
|
562
|
+
if ((producerTimedOut || producerCleanupFailed) && adapter && !adapterPoisonProblem) {
|
|
563
|
+
adapterPoisonProblem = producerTimedOut ? "producer_timeout" : "producer_failed";
|
|
564
|
+
try {
|
|
565
|
+
await closeAdapter();
|
|
566
|
+
} catch (closeError) {
|
|
567
|
+
adapterCloseError = closeError;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
observation = {
|
|
571
|
+
finalDocumentUrl: route.url,
|
|
572
|
+
responseCollectionStatus: "failed",
|
|
573
|
+
networkidle: { status: "invalid", duration_ms: null },
|
|
574
|
+
producerProblem: browserUnavailable
|
|
575
|
+
? "browser_unavailable"
|
|
576
|
+
: producerTimedOut ? "producer_timeout" : "producer_failed",
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
captures.push(buildPageLoadCapture({
|
|
580
|
+
buildFingerprint,
|
|
581
|
+
slug,
|
|
582
|
+
requestedRoute: route.requested_route,
|
|
583
|
+
viewport: viewport.key,
|
|
584
|
+
requestedDocumentUrl: route.url,
|
|
585
|
+
finalDocumentUrl: observation.finalDocumentUrl,
|
|
586
|
+
responseCollectionStatus: observation.responseCollectionStatus,
|
|
587
|
+
networkidle: observation.networkidle,
|
|
588
|
+
mediaElements: observation.mediaElements,
|
|
589
|
+
responses: observation.responses,
|
|
590
|
+
producerError: observation.producerError,
|
|
591
|
+
producerProblem: observation.producerProblem,
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} finally {
|
|
596
|
+
if (adapter && !adapterCloseError) {
|
|
597
|
+
try {
|
|
598
|
+
await closeAdapter();
|
|
599
|
+
} catch (error) {
|
|
600
|
+
adapterCloseError = error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (adapterCloseError) {
|
|
606
|
+
const producerProblem = observedProducerTimeout
|
|
607
|
+
|| adapterCloseError?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE
|
|
608
|
+
? "producer_timeout"
|
|
609
|
+
: "producer_failed";
|
|
610
|
+
captures.length = 0;
|
|
611
|
+
for (const route of plan.routes) {
|
|
612
|
+
for (const viewport of plan.viewports) {
|
|
613
|
+
captures.push(buildPageLoadCapture({
|
|
614
|
+
buildFingerprint,
|
|
615
|
+
slug,
|
|
616
|
+
requestedRoute: route.requested_route,
|
|
617
|
+
viewport: viewport.key,
|
|
618
|
+
requestedDocumentUrl: route.url,
|
|
619
|
+
finalDocumentUrl: route.url,
|
|
620
|
+
responseCollectionStatus: "failed",
|
|
621
|
+
networkidle: { status: "invalid", duration_ms: null },
|
|
622
|
+
producerProblem,
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const routes = plan.routes.map((route) => route.requested_route);
|
|
629
|
+
const viewports = plan.viewports.map((viewport) => viewport.key);
|
|
630
|
+
const pageLoad = buildPolishPageLoadEvidence({
|
|
631
|
+
buildFingerprint,
|
|
632
|
+
slug,
|
|
633
|
+
routeScope: plan.route_scope,
|
|
634
|
+
routes,
|
|
635
|
+
viewports,
|
|
636
|
+
captures,
|
|
637
|
+
});
|
|
638
|
+
return { plan, page_load: pageLoad };
|
|
639
|
+
}
|