@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,312 @@
|
|
|
1
|
+
// Non-packet runnability (learnings L7).
|
|
2
|
+
//
|
|
3
|
+
// A page-kit `campaign-build` campaign produces a built `_site/` but no full
|
|
4
|
+
// campaigns-os Build Packet, so doctor/qa historically had nothing to run
|
|
5
|
+
// against — only the static contract `check` gate was reachable. This module
|
|
6
|
+
// resolves QA scope directly from the built `_site/` (enumerating pages and
|
|
7
|
+
// inferring their funnel type) and synthesizes a minimal Build Packet, so the
|
|
8
|
+
// existing built-output gates (literal residue, placeholder text, demo-asset
|
|
9
|
+
// fidelity, brand contract) can run without a hand-authored packet.
|
|
10
|
+
//
|
|
11
|
+
// Pure with respect to doctor/qa state: it only reads the filesystem and
|
|
12
|
+
// returns plain data. Callers turn that data into doctor issues / QA
|
|
13
|
+
// topologies / a packet.
|
|
14
|
+
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
17
|
+
import { basename, join, relative, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
const HTML_EXT = ".html";
|
|
20
|
+
|
|
21
|
+
// Funnel page-type inference from a built route or filename. Order matters:
|
|
22
|
+
// downsell is tested before upsell, and the broad fallbacks (landing/page) run
|
|
23
|
+
// last. Returns one of the page types QA understands; "page" for generic
|
|
24
|
+
// content pages with no funnel role.
|
|
25
|
+
export function inferPageType(routeOrName) {
|
|
26
|
+
const value = String(routeOrName || "").toLowerCase().trim();
|
|
27
|
+
if (value === "" || value === "/" || value === "index") return "landing";
|
|
28
|
+
if (/down[\s_/-]*sell/.test(value)) return "downsell";
|
|
29
|
+
if (/up[\s_/-]*sell|(^|[\s_/-])oto([\s_/-]|\d|$)|one[\s_/-]*time[\s_/-]*offer/.test(value)) return "upsell";
|
|
30
|
+
if (/thank|receipt|confirm(ation)?|order[\s_/-]*complete/.test(value)) return "receipt";
|
|
31
|
+
if (/checkout|\bcart\b|\border\b/.test(value)) return "checkout";
|
|
32
|
+
// The two-step bundle-selection step. Deliberately narrow, and anchored on
|
|
33
|
+
// BOTH ends: the route must *be* about choosing a bundle, not merely contain
|
|
34
|
+
// the words. An editorial "/our-choose-bundle-guide/" stays generic rather
|
|
35
|
+
// than being pulled into commerce residue checking. Checkout/cart/order win
|
|
36
|
+
// above, so "select-checkout" stays checkout.
|
|
37
|
+
// Matched against the FINAL route segment, whole: the segment must *be* the
|
|
38
|
+
// selector step, not merely contain the words. "/our-choose-bundle-guide/"
|
|
39
|
+
// and "selected-items" stay generic.
|
|
40
|
+
const lastSegment = value.split("/").filter(Boolean).pop() || value;
|
|
41
|
+
if (/^(?:select|bundle[\s_-]*select|select[\s_-]*bundle|choose[\s_-]*(?:your[\s_-]*)?(?:bundle|package))$/.test(lastSegment)) return "select";
|
|
42
|
+
if (/presell|advertorial|listicle|review/.test(value)) return "presell";
|
|
43
|
+
if (/landing|^home$|index/.test(value)) return "landing";
|
|
44
|
+
return "page";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function listHtmlFiles(root) {
|
|
48
|
+
const files = [];
|
|
49
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) return files;
|
|
50
|
+
const walk = (dir) => {
|
|
51
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
52
|
+
// Skip page-kit/Jekyll internals and VCS/deps. Built includes/layouts are
|
|
53
|
+
// template scaffolding, not rendered pages, so they never define scope.
|
|
54
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
55
|
+
const full = join(dir, entry.name);
|
|
56
|
+
if (entry.isDirectory()) walk(full);
|
|
57
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(HTML_EXT)) files.push(full);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walk(root);
|
|
61
|
+
return files.sort();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function routeForFile(campaignDir, file) {
|
|
65
|
+
const rel = relative(campaignDir, file).split(sep).join("/");
|
|
66
|
+
if (rel === "index.html") return "";
|
|
67
|
+
if (rel.endsWith("/index.html")) return rel.slice(0, -"/index.html".length);
|
|
68
|
+
return rel.replace(/\.html$/i, "");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pageIdForRoute(route) {
|
|
72
|
+
if (!route) return "index";
|
|
73
|
+
return route.replace(/\//g, "-").replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "index";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find the built `_site/` for a target repo and the campaign directory within
|
|
77
|
+
// it. Accepts a page-kit target repo (contains `_site/`), a `_site/` directory,
|
|
78
|
+
// or a campaign directory directly.
|
|
79
|
+
function resolveSiteRoot(targetRepo) {
|
|
80
|
+
const candidate = join(targetRepo, "_site");
|
|
81
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
82
|
+
return { siteRoot: candidate, slugDiscovery: true };
|
|
83
|
+
}
|
|
84
|
+
return { siteRoot: targetRepo, slugDiscovery: basename(targetRepo) === "_site" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve QA/doctor scope from a built page-kit campaign directory.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} targetRepo Absolute path to the page-kit target repo (or a
|
|
91
|
+
* `_site/` directory, or a campaign directory).
|
|
92
|
+
* @param {{ slug?: string|null }} [options]
|
|
93
|
+
* @returns {{
|
|
94
|
+
* ok: boolean,
|
|
95
|
+
* error?: string,
|
|
96
|
+
* target_repo: string,
|
|
97
|
+
* site_root: string,
|
|
98
|
+
* slug: string,
|
|
99
|
+
* campaign_dir: string,
|
|
100
|
+
* pages: Array<{ page_id: string, page_type: string, route: string, built_path: string }>,
|
|
101
|
+
* html_count: number,
|
|
102
|
+
* slug_candidates?: string[],
|
|
103
|
+
* }}
|
|
104
|
+
*/
|
|
105
|
+
export function resolveBuiltSiteScope(targetRepo, { slug = null } = {}) {
|
|
106
|
+
const base = { ok: false, target_repo: targetRepo, site_root: null, slug: "", campaign_dir: null, pages: [], html_count: 0 };
|
|
107
|
+
if (!targetRepo || !existsSync(targetRepo) || !statSync(targetRepo).isDirectory()) {
|
|
108
|
+
return { ...base, error: `Built campaign directory does not exist: ${targetRepo}` };
|
|
109
|
+
}
|
|
110
|
+
const { siteRoot, slugDiscovery } = resolveSiteRoot(targetRepo);
|
|
111
|
+
|
|
112
|
+
// Slug discovery: prefer an explicit slug; otherwise the campaign is either a
|
|
113
|
+
// single subdirectory under `_site/` or rendered at the site root.
|
|
114
|
+
let resolvedSlug = typeof slug === "string" && slug.trim() ? slug.trim() : null;
|
|
115
|
+
if (!resolvedSlug) {
|
|
116
|
+
if (!slugDiscovery) {
|
|
117
|
+
resolvedSlug = "";
|
|
118
|
+
} else {
|
|
119
|
+
const entries = readdirSync(siteRoot, { withFileTypes: true });
|
|
120
|
+
const subdirs = entries
|
|
121
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
122
|
+
.map((entry) => entry.name)
|
|
123
|
+
.filter((name) => listHtmlFiles(join(siteRoot, name)).length > 0);
|
|
124
|
+
const rootHtml = entries.some((e) => e.isFile() && e.name.toLowerCase().endsWith(HTML_EXT));
|
|
125
|
+
if (subdirs.length === 1 && !rootHtml) {
|
|
126
|
+
resolvedSlug = subdirs[0];
|
|
127
|
+
} else if (rootHtml || subdirs.length === 0) {
|
|
128
|
+
resolvedSlug = "";
|
|
129
|
+
} else {
|
|
130
|
+
return { ...base, site_root: siteRoot, error: `Multiple campaign slugs under ${siteRoot}; pass --slug to choose one.`, slug_candidates: subdirs };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const campaignDir = resolvedSlug ? join(siteRoot, resolvedSlug) : siteRoot;
|
|
136
|
+
if (!existsSync(campaignDir) || !statSync(campaignDir).isDirectory()) {
|
|
137
|
+
return { ...base, site_root: siteRoot, slug: resolvedSlug, error: `Campaign directory does not exist: ${campaignDir}` };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const pages = listHtmlFiles(campaignDir).map((file) => {
|
|
141
|
+
const route = routeForFile(campaignDir, file);
|
|
142
|
+
return {
|
|
143
|
+
page_id: pageIdForRoute(route),
|
|
144
|
+
page_type: inferPageType(route),
|
|
145
|
+
route,
|
|
146
|
+
built_path: file,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!pages.length) {
|
|
151
|
+
return { ...base, site_root: siteRoot, slug: resolvedSlug, campaign_dir: campaignDir, error: `No built HTML pages found under ${campaignDir}.` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
target_repo: targetRepo,
|
|
157
|
+
site_root: siteRoot,
|
|
158
|
+
slug: resolvedSlug,
|
|
159
|
+
campaign_dir: campaignDir,
|
|
160
|
+
pages,
|
|
161
|
+
html_count: pages.length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function trimTrailingSlash(value) {
|
|
166
|
+
return String(value || "").replace(/\/+$/, "");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Build QA topologies (the spec-shaped { topology_id, pages: [{page_id,
|
|
170
|
+
// page_type, url}] } structure runQa walks) from a built-site scope plus a
|
|
171
|
+
// served base URL. base URL points at the campaign root the pages are served
|
|
172
|
+
// under; a page's route is appended to form its URL. A base URL is required —
|
|
173
|
+
// the whole point of a topology is fetchable page URLs, so fail fast rather
|
|
174
|
+
// than hand a direct caller (tests, future tools) a topology with null URLs.
|
|
175
|
+
export function topologiesFromBuiltSiteScope(scope, baseUrl) {
|
|
176
|
+
const base = trimTrailingSlash(baseUrl);
|
|
177
|
+
if (!base) {
|
|
178
|
+
throw new Error("topologiesFromBuiltSiteScope requires a non-empty base URL so built pages have fetchable URLs.");
|
|
179
|
+
}
|
|
180
|
+
const pages = (scope?.pages || []).map((page) => ({
|
|
181
|
+
page_id: page.page_id,
|
|
182
|
+
page_type: page.page_type,
|
|
183
|
+
url: page.route ? `${base}/${page.route}/` : `${base}/`,
|
|
184
|
+
route: page.route,
|
|
185
|
+
}));
|
|
186
|
+
return [{ topology_id: scope?.slug || "campaign", pages }];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Synthesize a minimal Build Packet from a built-site scope so the existing
|
|
191
|
+
* packet-driven surfaces can consume a `campaign-build`'d page-kit campaign.
|
|
192
|
+
* Deliberately marked `_synthesized` and missing the source_html/full assembly
|
|
193
|
+
* fields a real packet carries — it is enough to point doctor/qa at the built
|
|
194
|
+
* output and the chosen family, not a substitute for a real build packet.
|
|
195
|
+
*/
|
|
196
|
+
export function synthesizeMinimalBuildPacket({
|
|
197
|
+
schemaVersion,
|
|
198
|
+
targetRepo,
|
|
199
|
+
scope,
|
|
200
|
+
family = null,
|
|
201
|
+
mapId = null,
|
|
202
|
+
baseUrl = null,
|
|
203
|
+
deployTarget = "unknown",
|
|
204
|
+
}) {
|
|
205
|
+
const slug = scope?.slug || "";
|
|
206
|
+
const outputDir = scope?.campaign_dir && targetRepo
|
|
207
|
+
? relative(targetRepo, scope.campaign_dir).split(sep).join("/") || "."
|
|
208
|
+
: ".";
|
|
209
|
+
return {
|
|
210
|
+
schema_version: schemaVersion,
|
|
211
|
+
_synthesized: {
|
|
212
|
+
from: "built_site",
|
|
213
|
+
note: "Minimal packet auto-emitted from a built _site/. Not a full Build Packet: source_html and assembly provenance are absent.",
|
|
214
|
+
site_root: scope?.site_root || null,
|
|
215
|
+
campaign_dir: scope?.campaign_dir || null,
|
|
216
|
+
html_count: scope?.html_count || 0,
|
|
217
|
+
},
|
|
218
|
+
campaign: {
|
|
219
|
+
public_route_slug: slug || (mapId || "local-campaign"),
|
|
220
|
+
allowed_domains_confirmed: false,
|
|
221
|
+
},
|
|
222
|
+
spec: {
|
|
223
|
+
map_id: mapId || slug || "local-campaign",
|
|
224
|
+
},
|
|
225
|
+
assembly: {
|
|
226
|
+
target_repo: targetRepo,
|
|
227
|
+
output_dir: outputDir,
|
|
228
|
+
template_family: family || "undecided",
|
|
229
|
+
},
|
|
230
|
+
deploy: {
|
|
231
|
+
target: deployTarget || "unknown",
|
|
232
|
+
preview_url: baseUrl || null,
|
|
233
|
+
},
|
|
234
|
+
qa: {},
|
|
235
|
+
pages: (scope?.pages || []).map((page) => ({ page_id: page.page_id, type: page.page_type, route: page.route })),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Build output fingerprint. `stages.assembly.build_fingerprint` is the value
|
|
240
|
+
// every later stage (polish capture, the polish gate, QA) binds its evidence
|
|
241
|
+
// to, so it must change exactly when the built output changes — and only
|
|
242
|
+
// then. It therefore hashes the OUTPUT, never the inputs: a toolkit or
|
|
243
|
+
// template upgrade that renders different bytes from identical source is a
|
|
244
|
+
// different build, and a rebuild from identical source on another machine is
|
|
245
|
+
// the same build. The manifest is `<path>\n<sha256>\n` per file, paths
|
|
246
|
+
// relative to the output root with `/` separators and sorted by code point,
|
|
247
|
+
// so directory walk order and the absolute location never leak into the
|
|
248
|
+
// value. Page Kit (0.2.0) writes only rendered HTML and copied assets into
|
|
249
|
+
// _site/, nothing it timestamps, so nothing is excluded by default; `exclude`
|
|
250
|
+
// takes root-relative paths for a consumer whose build does stamp a file.
|
|
251
|
+
// Symbolic links are never build output and are skipped, not followed.
|
|
252
|
+
export const BUILD_FINGERPRINT_ALGORITHM = "sha256-manifest/v1";
|
|
253
|
+
|
|
254
|
+
function listFilesRelative(root) {
|
|
255
|
+
const files = [];
|
|
256
|
+
const walk = (dir) => {
|
|
257
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
258
|
+
// Symlinks are not build output: page-kit writes files and copies
|
|
259
|
+
// assets, never links. A link is skipped rather than followed, so a
|
|
260
|
+
// link into the source tree (or a loop) can neither leak input bytes
|
|
261
|
+
// into the value nor hang the walk.
|
|
262
|
+
if (entry.isSymbolicLink()) continue;
|
|
263
|
+
const full = join(dir, entry.name);
|
|
264
|
+
if (entry.isDirectory()) walk(full);
|
|
265
|
+
else if (entry.isFile()) files.push(relative(root, full).split(sep).join("/"));
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
walk(root);
|
|
269
|
+
return files.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function sha256Hex(buffer) {
|
|
273
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Fingerprint a built output tree. Returns
|
|
278
|
+
* { ok, algorithm, root, fingerprint, file_count, excluded, manifest, error }
|
|
279
|
+
* where `fingerprint` is `sha256:<hex>` over the canonical manifest and
|
|
280
|
+
* `manifest` is that manifest text, so an operator can recompute the value by
|
|
281
|
+
* hand (`sha256sum` over each file, sort by path, hash the lines). `ok: false`
|
|
282
|
+
* with `error` when the root is not a directory; an empty tree still hashes
|
|
283
|
+
* (the empty manifest) so a wiped _site/ reads as a changed build, not a
|
|
284
|
+
* missing one.
|
|
285
|
+
*/
|
|
286
|
+
export function computeBuildFingerprint(outputDir, { exclude = [] } = {}) {
|
|
287
|
+
const root = String(outputDir || "");
|
|
288
|
+
const base = { ok: false, algorithm: BUILD_FINGERPRINT_ALGORITHM, root, fingerprint: null, file_count: 0, excluded: [], manifest: null };
|
|
289
|
+
if (!root || !existsSync(root) || !statSync(root).isDirectory()) {
|
|
290
|
+
return { ...base, error: `Build output directory does not exist: ${root || "(empty)"}` };
|
|
291
|
+
}
|
|
292
|
+
const excludeSet = new Set((Array.isArray(exclude) ? exclude : []).map((path) => String(path).split(sep).join("/")));
|
|
293
|
+
const excluded = [];
|
|
294
|
+
const lines = [];
|
|
295
|
+
for (const path of listFilesRelative(root)) {
|
|
296
|
+
if (excludeSet.has(path)) {
|
|
297
|
+
excluded.push(path);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
lines.push(`${path}\n${sha256Hex(readFileSync(join(root, path)))}\n`);
|
|
301
|
+
}
|
|
302
|
+
const manifest = lines.join("");
|
|
303
|
+
return {
|
|
304
|
+
...base,
|
|
305
|
+
ok: true,
|
|
306
|
+
fingerprint: `sha256:${sha256Hex(Buffer.from(manifest, "utf8"))}`,
|
|
307
|
+
file_count: lines.length,
|
|
308
|
+
excluded,
|
|
309
|
+
manifest,
|
|
310
|
+
error: null,
|
|
311
|
+
};
|
|
312
|
+
}
|