@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,781 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
|
|
5
|
+
import { escapeRegExp } from "./repo-scan.mjs";
|
|
6
|
+
|
|
7
|
+
export const BUILD_BRIEF_SCHEMA = "campaigns-os-build-brief/v1";
|
|
8
|
+
export const BUILD_BRIEF_NORMALIZED_REL_PATH = ".campaign-runtime/input/campaign-build-brief.normalized.json";
|
|
9
|
+
export const BUILD_BRIEF_CANDIDATE_FILENAMES = Object.freeze([
|
|
10
|
+
"campaign-build-brief.yaml",
|
|
11
|
+
"campaign-build-brief.yml",
|
|
12
|
+
"campaign-build-brief.json",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const REQUIRED_HIGH_IMPACT_FIELDS = Object.freeze([
|
|
16
|
+
{
|
|
17
|
+
id: "page_design_authority",
|
|
18
|
+
priority: 1,
|
|
19
|
+
field: "design_authority",
|
|
20
|
+
question: "Which source controls each campaign page: the provided design export, the selected template, or a template adapted to another page?",
|
|
21
|
+
reason: "Page-by-page authority prevents checkout, OTO, and receipt pages from drifting into unrelated starter-template composition.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "brand_palette_cta",
|
|
25
|
+
priority: 2,
|
|
26
|
+
field: "brand",
|
|
27
|
+
question: "Which palette and CTA style should commerce pages use?",
|
|
28
|
+
reason: "Commerce pages need a business-approved brand layer instead of silent template defaults.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "variant_media_rules",
|
|
32
|
+
priority: 3,
|
|
33
|
+
field: "media",
|
|
34
|
+
question: "Which product variants or colors are actually sold, and may media show other variants?",
|
|
35
|
+
reason: "Variant ambiguity often creates wrong-color carousels and unavailable-product claims.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "bundle_pricing_presentation",
|
|
39
|
+
priority: 4,
|
|
40
|
+
field: "offer_presentation.bundle_cards",
|
|
41
|
+
question: "How should bundle cards present pricing: simple unit price, savings-led, or full accounting?",
|
|
42
|
+
reason: "CampaignSpec owns prices; the brief owns which shopper-facing price story is appropriate.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "promo_urgency_copy",
|
|
46
|
+
priority: 5,
|
|
47
|
+
field: "promo_urgency",
|
|
48
|
+
question: "Which promo, savings, and urgency language is approved for timers, banners, and exit-pop surfaces?",
|
|
49
|
+
reason: "Promo placeholders and unsupported scarcity claims are business decisions, not implementation details.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "payment_methods_trust",
|
|
53
|
+
priority: 6,
|
|
54
|
+
field: "commerce_surfaces.payment_methods_allowed",
|
|
55
|
+
question: "Which payment methods and trust badges may appear?",
|
|
56
|
+
reason: "Templates often carry demo wallets or badges that must not survive without approval.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "canonical_display_names",
|
|
60
|
+
priority: 7,
|
|
61
|
+
field: "canonical_display.product_name_source",
|
|
62
|
+
question: "Should CampaignSpec display names win, or may runtime/catalog names override them?",
|
|
63
|
+
reason: "Name drift across source, spec, and runtime data is hard to spot after assembly.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "regulated_claims",
|
|
67
|
+
priority: 8,
|
|
68
|
+
field: "campaign_intent.compliance",
|
|
69
|
+
question: "Are there regulated claims, forbidden phrases, or approved benefit statements the build must follow?",
|
|
70
|
+
reason: "Health, financial, and other regulated offers need explicit copy boundaries.",
|
|
71
|
+
conditional: "regulated",
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const PAYMENT_METHOD_ALIASES = new Map([
|
|
76
|
+
["credit_card", "card"],
|
|
77
|
+
["credit card", "card"],
|
|
78
|
+
["card", "card"],
|
|
79
|
+
["cards", "card"],
|
|
80
|
+
["paypal", "paypal"],
|
|
81
|
+
["pay_pal", "paypal"],
|
|
82
|
+
["applepay", "apple_pay"],
|
|
83
|
+
["apple_pay", "apple_pay"],
|
|
84
|
+
["apple pay", "apple_pay"],
|
|
85
|
+
["googlepay", "google_pay"],
|
|
86
|
+
["google_pay", "google_pay"],
|
|
87
|
+
["google pay", "google_pay"],
|
|
88
|
+
["klarna", "klarna"],
|
|
89
|
+
["afterpay", "afterpay"],
|
|
90
|
+
["sezzle", "sezzle"],
|
|
91
|
+
["shop_pay", "shop_pay"],
|
|
92
|
+
["shop pay", "shop_pay"],
|
|
93
|
+
["amazonpay", "amazon_pay"],
|
|
94
|
+
["amazon_pay", "amazon_pay"],
|
|
95
|
+
["amazon pay", "amazon_pay"],
|
|
96
|
+
["venmo", "venmo"],
|
|
97
|
+
["cashapp", "cash_app"],
|
|
98
|
+
["cash_app", "cash_app"],
|
|
99
|
+
["cash app", "cash_app"],
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const ORDER_BUMP_KEYS = new Set(["order_bump", "order_bumps", "prepurchase", "pre_purchase"]);
|
|
103
|
+
const ORDER_BUMP_VALUE_KEYS = new Set(["role", "type", "kind", "surface", "placement", "slot", "page_type"]);
|
|
104
|
+
const ORDER_BUMP_VALUES = new Set(["order_bump", "order_bumps", "prepurchase", "pre_purchase"]);
|
|
105
|
+
const DISABLED_SIGNAL_VALUES = new Set(["false", "none", "no", "0", "off", "disabled", "disable", "inactive"]);
|
|
106
|
+
|
|
107
|
+
const COLOR_WORDS = Object.freeze([
|
|
108
|
+
"black",
|
|
109
|
+
"white",
|
|
110
|
+
"gray",
|
|
111
|
+
"grey",
|
|
112
|
+
"navy",
|
|
113
|
+
"blue",
|
|
114
|
+
"red",
|
|
115
|
+
"green",
|
|
116
|
+
"yellow",
|
|
117
|
+
"pink",
|
|
118
|
+
"purple",
|
|
119
|
+
"orange",
|
|
120
|
+
"brown",
|
|
121
|
+
"cream",
|
|
122
|
+
"beige",
|
|
123
|
+
"silver",
|
|
124
|
+
"gold",
|
|
125
|
+
"matte",
|
|
126
|
+
"rose",
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const QA_POLICY_ENFORCEMENT = Object.freeze({
|
|
130
|
+
status: "documented_expectation",
|
|
131
|
+
enforced_by: "qa.proof_policy and report.proof_policy",
|
|
132
|
+
note: "Build Brief qa_policy records business QA expectations; doctor and QA enforce the Build Packet qa.proof_policy and Assembly Report report.proof_policy contract.",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export function inferBuildBriefPath({ explicitPath = null, sourceRoot = null, targetRepo = null } = {}) {
|
|
136
|
+
if (isNonEmptyString(explicitPath)) return { path: resolve(explicitPath), source: "operator_flag" };
|
|
137
|
+
const roots = [...new Set([sourceRoot, targetRepo].filter(isNonEmptyString).map((path) => resolve(path)))];
|
|
138
|
+
for (const root of roots) {
|
|
139
|
+
for (const filename of BUILD_BRIEF_CANDIDATE_FILENAMES) {
|
|
140
|
+
const candidate = join(root, filename);
|
|
141
|
+
if (safeIsFile(candidate)) return { path: candidate, source: root === resolve(sourceRoot || "") ? "source_root" : "target_repo" };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function loadCampaignBuildBriefFile(path) {
|
|
148
|
+
const resolvedPath = resolve(path);
|
|
149
|
+
const text = readFileSync(resolvedPath, "utf8");
|
|
150
|
+
const format = buildBriefFormatForPath(resolvedPath);
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = format === "json" ? JSON.parse(text) : parseYaml(text);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`Campaign Build Brief ${basename(resolvedPath)} could not be parsed as ${format.toUpperCase()}: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
return { path: resolvedPath, format, value: parsed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createCampaignBuildBriefArtifact({
|
|
161
|
+
inputPath = null,
|
|
162
|
+
inputSource = null,
|
|
163
|
+
spec = null,
|
|
164
|
+
activePages = [],
|
|
165
|
+
pageMappings = [],
|
|
166
|
+
templateFamily = null,
|
|
167
|
+
sourceAssetCrawl = null,
|
|
168
|
+
commerceZoneFindings = [],
|
|
169
|
+
} = {}) {
|
|
170
|
+
const loaded = inputPath ? loadCampaignBuildBriefFile(inputPath) : null;
|
|
171
|
+
const mode = loaded ? "prepared" : "guided_draft";
|
|
172
|
+
const baseBrief = loaded
|
|
173
|
+
? cloneJson(loaded.value)
|
|
174
|
+
: draftCampaignBuildBrief({ spec, activePages, pageMappings, templateFamily, sourceAssetCrawl });
|
|
175
|
+
|
|
176
|
+
const errors = [];
|
|
177
|
+
const normalized = normalizeCampaignBuildBrief(baseBrief, {
|
|
178
|
+
mode,
|
|
179
|
+
inputPath: loaded?.path || null,
|
|
180
|
+
inputFormat: loaded?.format || "generated",
|
|
181
|
+
inputSource,
|
|
182
|
+
spec,
|
|
183
|
+
activePages,
|
|
184
|
+
pageMappings,
|
|
185
|
+
sourceAssetCrawl,
|
|
186
|
+
}, errors);
|
|
187
|
+
const evaluation = evaluateCampaignBuildBrief(normalized, {
|
|
188
|
+
spec,
|
|
189
|
+
activePages,
|
|
190
|
+
pageMappings,
|
|
191
|
+
sourceAssetCrawl,
|
|
192
|
+
commerceZoneFindings,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const gates = [
|
|
196
|
+
...errors.map((error) => ({
|
|
197
|
+
code: error.code,
|
|
198
|
+
severity: "blocker",
|
|
199
|
+
field: error.field || null,
|
|
200
|
+
message: error.message,
|
|
201
|
+
})),
|
|
202
|
+
...evaluation.gates,
|
|
203
|
+
];
|
|
204
|
+
const blockerGates = gates.filter((gate) => gate.severity === "blocker");
|
|
205
|
+
normalized.status = errors.length
|
|
206
|
+
? "invalid"
|
|
207
|
+
: evaluation.questions.length || blockerGates.length
|
|
208
|
+
? "needs_answers"
|
|
209
|
+
: "complete";
|
|
210
|
+
normalized.questions = evaluation.questions;
|
|
211
|
+
normalized.gates = gates;
|
|
212
|
+
|
|
213
|
+
const blocking = mode === "prepared"
|
|
214
|
+
? blockerGates
|
|
215
|
+
: [];
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
mode,
|
|
219
|
+
inputPath: loaded?.path || null,
|
|
220
|
+
inputFormat: loaded?.format || "generated",
|
|
221
|
+
artifact: normalized,
|
|
222
|
+
errors,
|
|
223
|
+
questions: evaluation.questions,
|
|
224
|
+
gates: normalized.gates,
|
|
225
|
+
blockers: blocking,
|
|
226
|
+
warnings: normalized.gates.filter((gate) => gate.severity !== "blocker"),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function validateCampaignBuildBriefArtifact(brief, { spec = null } = {}) {
|
|
231
|
+
const errors = [];
|
|
232
|
+
const warnings = [];
|
|
233
|
+
const ready = [];
|
|
234
|
+
|
|
235
|
+
if (!isObject(brief)) {
|
|
236
|
+
errors.push({ code: "build_brief.type", message: "Campaign Build Brief artifact must be an object." });
|
|
237
|
+
return { errors, warnings, ready };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (brief.schema_version !== BUILD_BRIEF_SCHEMA) {
|
|
241
|
+
errors.push({ code: "build_brief.schema_version", message: `Campaign Build Brief schema_version must be ${BUILD_BRIEF_SCHEMA}.` });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const mode = brief._meta?.mode || "unknown";
|
|
245
|
+
const questions = Array.isArray(brief.questions) ? brief.questions : [];
|
|
246
|
+
const gates = Array.isArray(brief.gates) ? brief.gates : [];
|
|
247
|
+
const blockerGates = gates.filter((gate) => gate?.severity === "blocker");
|
|
248
|
+
|
|
249
|
+
if (!errors.length && brief.status === "complete" && questions.length === 0 && blockerGates.length === 0) {
|
|
250
|
+
ready.push("Campaign Build Brief is complete: merchandising/design presentation truth is available.");
|
|
251
|
+
} else if (mode === "prepared") {
|
|
252
|
+
for (const gate of blockerGates) {
|
|
253
|
+
errors.push({ code: gate.code || "build_brief.blocker", message: gate.message || "Prepared Campaign Build Brief has an unresolved blocker." });
|
|
254
|
+
}
|
|
255
|
+
if (questions.length) {
|
|
256
|
+
errors.push({
|
|
257
|
+
code: "build_brief.questions_unanswered",
|
|
258
|
+
message: `Prepared Campaign Build Brief has ${questions.length} unresolved business question(s): ${questions.map((question) => question.id).join(", ")}.`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
if (questions.length) {
|
|
263
|
+
warnings.push({
|
|
264
|
+
code: "build_brief.guided_questions",
|
|
265
|
+
message: `Generated Campaign Build Brief draft has ${questions.length} high-impact business question(s) to confirm: ${questions.map((question) => question.id).join(", ")}.`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
for (const gate of blockerGates) {
|
|
269
|
+
warnings.push({ code: gate.code || "build_brief.blocker", message: gate.message || "Generated Campaign Build Brief draft has an unresolved blocker." });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const allowedPayments = normalizePaymentList(brief.commerce_surfaces?.payment_methods_allowed);
|
|
274
|
+
const hiddenPayments = normalizePaymentList(brief.commerce_surfaces?.hidden_payment_methods);
|
|
275
|
+
const overlap = allowedPayments.filter((method) => hiddenPayments.includes(method));
|
|
276
|
+
if (overlap.length) {
|
|
277
|
+
errors.push({
|
|
278
|
+
code: "build_brief.payment_methods_conflict",
|
|
279
|
+
message: `Campaign Build Brief lists payment method(s) as both allowed and hidden: ${overlap.join(", ")}.`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const specPayments = collectSpecPaymentMethods(spec);
|
|
284
|
+
const unsupportedAllowed = specPayments.length ? allowedPayments.filter((method) => !specPayments.includes(method)) : [];
|
|
285
|
+
if (unsupportedAllowed.length) {
|
|
286
|
+
warnings.push({
|
|
287
|
+
code: "build_brief.payment_methods_spec_drift",
|
|
288
|
+
message: `Campaign Build Brief allows payment method(s) not found in CampaignSpec data: ${unsupportedAllowed.join(", ")}. Confirm runtime/catalog support before rendering them.`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (brief.promo_urgency?.forbid_placeholders !== true) {
|
|
293
|
+
warnings.push({
|
|
294
|
+
code: "build_brief.promo_placeholders",
|
|
295
|
+
message: "Campaign Build Brief should set promo_urgency.forbid_placeholders: true so XXCODE/SPEC_* promo residue is treated as a gate.",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (brief.template_residue_policy?.block_placeholders !== true) {
|
|
299
|
+
warnings.push({
|
|
300
|
+
code: "build_brief.template_residue_policy",
|
|
301
|
+
message: "Campaign Build Brief should set template_residue_policy.block_placeholders: true so starter placeholders cannot survive silently.",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { errors, warnings, ready };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function normalizeCampaignBuildBrief(value, meta, errors) {
|
|
309
|
+
const brief = isObject(value) ? cloneJson(value) : {};
|
|
310
|
+
if (!isObject(value)) {
|
|
311
|
+
errors.push({ code: "build_brief.type", field: null, message: "Campaign Build Brief must be a YAML/JSON object." });
|
|
312
|
+
}
|
|
313
|
+
if (brief.schema_version == null) brief.schema_version = BUILD_BRIEF_SCHEMA;
|
|
314
|
+
if (brief.schema_version !== BUILD_BRIEF_SCHEMA) {
|
|
315
|
+
errors.push({
|
|
316
|
+
code: "build_brief.schema_version",
|
|
317
|
+
field: "schema_version",
|
|
318
|
+
message: `Campaign Build Brief schema_version must be ${BUILD_BRIEF_SCHEMA}.`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
brief._meta = {
|
|
323
|
+
generated_at: new Date().toISOString(),
|
|
324
|
+
mode: meta.mode,
|
|
325
|
+
input_path: meta.inputPath || null,
|
|
326
|
+
input_format: meta.inputFormat || null,
|
|
327
|
+
input_source: meta.inputSource || null,
|
|
328
|
+
normalized_by: "campaigns-os prepare-build",
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
brief.campaign_intent = objectOrEmpty(brief.campaign_intent);
|
|
332
|
+
brief.design_authority = objectOrEmpty(brief.design_authority);
|
|
333
|
+
brief.brand = objectOrEmpty(brief.brand);
|
|
334
|
+
brief.media = normalizeMedia(brief.media);
|
|
335
|
+
brief.offer_presentation = normalizeOfferPresentation(brief.offer_presentation);
|
|
336
|
+
brief.promo_urgency = normalizePromoUrgency(brief.promo_urgency);
|
|
337
|
+
brief.commerce_surfaces = normalizeCommerceSurfaces(brief.commerce_surfaces);
|
|
338
|
+
brief.canonical_display = objectOrEmpty(brief.canonical_display);
|
|
339
|
+
brief.template_residue_policy = normalizeResiduePolicy(brief.template_residue_policy);
|
|
340
|
+
brief.qa_policy = normalizeQaPolicy(brief.qa_policy);
|
|
341
|
+
brief.confidence = objectOrEmpty(brief.confidence);
|
|
342
|
+
|
|
343
|
+
return brief;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function draftCampaignBuildBrief({ spec, activePages, pageMappings, templateFamily, sourceAssetCrawl }) {
|
|
347
|
+
const pageMap = new Map((pageMappings || []).map((page) => [page.page_id, page]));
|
|
348
|
+
const designAuthority = {};
|
|
349
|
+
for (const page of activePages || []) {
|
|
350
|
+
const mapped = pageMap.get(page.id);
|
|
351
|
+
designAuthority[page.id] = {
|
|
352
|
+
source: mapped?.path ? "provided_design_export" : "template",
|
|
353
|
+
reference: mapped?.path || mapped?.skip_reason || `${page.type || "page"} from selected template`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const variantSignals = collectVariantSignals(spec, sourceAssetCrawl);
|
|
358
|
+
const paymentMethods = collectSpecPaymentMethods(spec);
|
|
359
|
+
const hasExitPop = activePages?.some((page) => page?.type === "checkout" && page?.exit_intent?.enabled === true) === true;
|
|
360
|
+
const hasOrderBump = hasOrderBumpSignals(spec);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
schema_version: BUILD_BRIEF_SCHEMA,
|
|
364
|
+
campaign_intent: {
|
|
365
|
+
audience: null,
|
|
366
|
+
conversion_goal: inferConversionGoal(activePages),
|
|
367
|
+
tone: "clear, practical, benefit-led",
|
|
368
|
+
},
|
|
369
|
+
design_authority: designAuthority,
|
|
370
|
+
brand: {
|
|
371
|
+
commerce_palette_source: designAuthority.landing ? "landing" : "provided_design_export",
|
|
372
|
+
primary_accent: null,
|
|
373
|
+
cta_style: null,
|
|
374
|
+
avoid: ["template-default brand colors"],
|
|
375
|
+
},
|
|
376
|
+
media: {
|
|
377
|
+
sold_variants: variantSignals.length === 1 ? variantSignals : [],
|
|
378
|
+
allow_other_variant_colors: variantSignals.length === 1 ? false : null,
|
|
379
|
+
prefer: ["clean product renders", "lifestyle images without embedded text"],
|
|
380
|
+
avoid: ["wrong product variant", "blurry stock-like crops"],
|
|
381
|
+
},
|
|
382
|
+
offer_presentation: {
|
|
383
|
+
bundle_cards: {
|
|
384
|
+
primary_price: null,
|
|
385
|
+
show_total: "multi_pack_only",
|
|
386
|
+
show_compare_price: false,
|
|
387
|
+
savings_badge: "rounded_percentage",
|
|
388
|
+
hide_duplicate_discount_copy: true,
|
|
389
|
+
},
|
|
390
|
+
post_purchase: {
|
|
391
|
+
show_voucher_code: "when_customer_needs_to_see_it",
|
|
392
|
+
show_retail_price: true,
|
|
393
|
+
show_shipping: "if_charged",
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
promo_urgency: {
|
|
397
|
+
header_claim_source: hasPromoSignals(spec) ? "campaign_offers" : "none",
|
|
398
|
+
timer_label: hasPromoSignals(spec) ? null : "Limited-time offer",
|
|
399
|
+
show_promo_code_in_timer: false,
|
|
400
|
+
exit_pop: {
|
|
401
|
+
enabled: hasExitPop,
|
|
402
|
+
code_source: hasExitPop ? "campaign_spec" : null,
|
|
403
|
+
},
|
|
404
|
+
forbid_placeholders: true,
|
|
405
|
+
},
|
|
406
|
+
commerce_surfaces: {
|
|
407
|
+
payment_methods_allowed: paymentMethods.length ? paymentMethods : ["card"],
|
|
408
|
+
hidden_payment_methods: [],
|
|
409
|
+
order_bump: {
|
|
410
|
+
enabled: hasOrderBump,
|
|
411
|
+
display_style: hasOrderBump ? "compact_switch" : null,
|
|
412
|
+
},
|
|
413
|
+
guarantees: {
|
|
414
|
+
use_campaign_or_merchant_policy_copy: true,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
canonical_display: {
|
|
418
|
+
product_name_source: "campaign_spec",
|
|
419
|
+
allow_runtime_name_override: false,
|
|
420
|
+
manual_overrides: {},
|
|
421
|
+
},
|
|
422
|
+
template_residue_policy: normalizeResiduePolicy(),
|
|
423
|
+
qa_policy: normalizeQaPolicy(),
|
|
424
|
+
confidence: {
|
|
425
|
+
campaign_intent: "low",
|
|
426
|
+
design_authority: Object.keys(designAuthority).length ? "medium" : "low",
|
|
427
|
+
brand: "low",
|
|
428
|
+
media: variantSignals.length === 1 ? "medium" : "low",
|
|
429
|
+
offer_presentation: "low",
|
|
430
|
+
promo_urgency: hasPromoSignals(spec) ? "low" : "medium",
|
|
431
|
+
commerce_surfaces: paymentMethods.length ? "medium" : "low",
|
|
432
|
+
template_family: templateFamily || null,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function evaluateCampaignBuildBrief(brief, { spec, activePages, pageMappings, sourceAssetCrawl }) {
|
|
438
|
+
const questions = [];
|
|
439
|
+
const gates = [];
|
|
440
|
+
const variantSignals = collectVariantSignals(spec, sourceAssetCrawl);
|
|
441
|
+
const regulated = hasRegulatedSignals(spec);
|
|
442
|
+
const pageIds = [...new Set((activePages || []).map((page) => page.id).filter(Boolean))];
|
|
443
|
+
|
|
444
|
+
const missingDesignAuthority = pageIds.filter((pageId) => !isObject(brief.design_authority?.[pageId]) || !isNonEmptyString(brief.design_authority?.[pageId]?.source));
|
|
445
|
+
if (missingDesignAuthority.length) {
|
|
446
|
+
addQuestion(questions, "page_design_authority", {
|
|
447
|
+
detail: `Missing page authority for: ${missingDesignAuthority.join(", ")}.`,
|
|
448
|
+
options: ["provided design export", "selected template", "template adapted to another page"],
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (!isNonEmptyString(brief.brand?.commerce_palette_source) || !isNonEmptyString(brief.brand?.cta_style)) {
|
|
452
|
+
addQuestion(questions, "brand_palette_cta", {
|
|
453
|
+
detail: "Missing brand.commerce_palette_source or brand.cta_style.",
|
|
454
|
+
options: ["landing palette + matched CTA", "shared token file", "template palette"],
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (variantSignals.length > 1 && (!Array.isArray(brief.media?.sold_variants) || brief.media.sold_variants.length === 0 || typeof brief.media.allow_other_variant_colors !== "boolean")) {
|
|
458
|
+
addQuestion(questions, "variant_media_rules", {
|
|
459
|
+
detail: `Detected multiple variant/media signals: ${variantSignals.slice(0, 8).join(", ")}.`,
|
|
460
|
+
options: ["sold variant only", "all available variants", "ask merchant per carousel"],
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (!isNonEmptyString(brief.offer_presentation?.bundle_cards?.primary_price)) {
|
|
464
|
+
addQuestion(questions, "bundle_pricing_presentation", {
|
|
465
|
+
detail: "Missing offer_presentation.bundle_cards.primary_price.",
|
|
466
|
+
options: ["discounted unit price", "savings-led", "full accounting"],
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (hasPromoSignals(spec) && (!isNonEmptyString(brief.promo_urgency?.header_claim_source) || !isNonEmptyString(brief.promo_urgency?.timer_label))) {
|
|
470
|
+
addQuestion(questions, "promo_urgency_copy", {
|
|
471
|
+
detail: "CampaignSpec appears to include promo/offer signals, but promo copy authority is incomplete.",
|
|
472
|
+
options: ["actual voucher code", "bundle savings claim", "no promo banner"],
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (!normalizePaymentList(brief.commerce_surfaces?.payment_methods_allowed).length) {
|
|
476
|
+
addQuestion(questions, "payment_methods_trust", {
|
|
477
|
+
detail: "Missing commerce_surfaces.payment_methods_allowed.",
|
|
478
|
+
options: ["card only", "card + declared wallets", "merchant-approved list"],
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (!isNonEmptyString(brief.canonical_display?.product_name_source)) {
|
|
482
|
+
addQuestion(questions, "canonical_display_names", {
|
|
483
|
+
detail: "Missing canonical_display.product_name_source.",
|
|
484
|
+
options: ["CampaignSpec names", "runtime/catalog names", "manual overrides"],
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
if (regulated && !hasComplianceBoundary(brief)) {
|
|
488
|
+
addQuestion(questions, "regulated_claims", {
|
|
489
|
+
detail: "Detected health, wellness, financial, or regulated-offer language without explicit claim boundaries.",
|
|
490
|
+
options: ["approved benefit language only", "conservative generic claims", "operator-provided compliance copy"],
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (brief.media?.allow_other_variant_colors === false && (!Array.isArray(brief.media.sold_variants) || brief.media.sold_variants.length === 0)) {
|
|
495
|
+
gates.push({
|
|
496
|
+
code: "build_brief.variant_rule_incomplete",
|
|
497
|
+
severity: "blocker",
|
|
498
|
+
field: "media.sold_variants",
|
|
499
|
+
message: "Brief forbids other variant colors but does not name the sold variants.",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (brief.template_residue_policy?.block_demo_payment_methods !== true) {
|
|
503
|
+
gates.push({
|
|
504
|
+
code: "build_brief.demo_payment_methods_allowed",
|
|
505
|
+
severity: "warning",
|
|
506
|
+
field: "template_residue_policy.block_demo_payment_methods",
|
|
507
|
+
message: "Brief does not explicitly block demo payment methods; polish/QA should verify template wallets are spec-backed.",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if ((pageMappings || []).some((page) => page?.skip_reason) && brief.qa_policy?.require_checkout_flow === true) {
|
|
511
|
+
gates.push({
|
|
512
|
+
code: "build_brief.partial_scope_qa_policy",
|
|
513
|
+
severity: "warning",
|
|
514
|
+
field: "qa_policy.require_checkout_flow",
|
|
515
|
+
message: "Brief requires checkout flow QA while some mapped pages are skipped; partial builds need explicit launch-readiness language.",
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { questions: questions.sort((a, b) => a.priority - b.priority), gates };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function addQuestion(questions, id, { detail = null, options = [] } = {}) {
|
|
523
|
+
const template = REQUIRED_HIGH_IMPACT_FIELDS.find((field) => field.id === id);
|
|
524
|
+
if (!template) return;
|
|
525
|
+
questions.push({
|
|
526
|
+
id,
|
|
527
|
+
priority: template.priority,
|
|
528
|
+
field: template.field,
|
|
529
|
+
question: template.question,
|
|
530
|
+
reason: detail ? `${template.reason} ${detail}` : template.reason,
|
|
531
|
+
options,
|
|
532
|
+
blocking: true,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function inferConversionGoal(activePages = []) {
|
|
537
|
+
const hasCheckout = activePages.some((page) => page?.type === "checkout");
|
|
538
|
+
const hasUpsell = activePages.some((page) => /upsell|downsell/i.test(String(page?.type || "")));
|
|
539
|
+
if (hasCheckout && hasUpsell) return "direct response funnel with post-purchase offers";
|
|
540
|
+
if (hasCheckout) return "single-product direct response funnel";
|
|
541
|
+
return "campaign landing experience";
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function normalizeMedia(media = {}) {
|
|
545
|
+
const value = objectOrEmpty(media);
|
|
546
|
+
return {
|
|
547
|
+
...value,
|
|
548
|
+
sold_variants: normalizeStringArray(value.sold_variants),
|
|
549
|
+
prefer: normalizeStringArray(value.prefer),
|
|
550
|
+
avoid: normalizeStringArray(value.avoid),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function normalizeOfferPresentation(offer = {}) {
|
|
555
|
+
const value = objectOrEmpty(offer);
|
|
556
|
+
return {
|
|
557
|
+
...value,
|
|
558
|
+
bundle_cards: objectOrEmpty(value.bundle_cards),
|
|
559
|
+
post_purchase: objectOrEmpty(value.post_purchase),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function normalizePromoUrgency(promo = {}) {
|
|
564
|
+
const value = objectOrEmpty(promo);
|
|
565
|
+
return {
|
|
566
|
+
...value,
|
|
567
|
+
exit_pop: objectOrEmpty(value.exit_pop),
|
|
568
|
+
forbid_placeholders: value.forbid_placeholders === true,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function normalizeCommerceSurfaces(commerce = {}) {
|
|
573
|
+
const value = objectOrEmpty(commerce);
|
|
574
|
+
return {
|
|
575
|
+
...value,
|
|
576
|
+
payment_methods_allowed: normalizePaymentList(value.payment_methods_allowed),
|
|
577
|
+
hidden_payment_methods: normalizePaymentList(value.hidden_payment_methods),
|
|
578
|
+
order_bump: objectOrEmpty(value.order_bump),
|
|
579
|
+
guarantees: objectOrEmpty(value.guarantees),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function normalizeResiduePolicy(policy = {}) {
|
|
584
|
+
const value = objectOrEmpty(policy);
|
|
585
|
+
return {
|
|
586
|
+
...value,
|
|
587
|
+
block_placeholders: value.block_placeholders !== false,
|
|
588
|
+
block_template_favicon: value.block_template_favicon !== false,
|
|
589
|
+
block_demo_payment_methods: value.block_demo_payment_methods !== false,
|
|
590
|
+
block_lorem_ipsum: value.block_lorem_ipsum !== false,
|
|
591
|
+
block_unapproved_tracking_claims: value.block_unapproved_tracking_claims !== false,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function normalizeQaPolicy(policy = {}) {
|
|
596
|
+
const value = objectOrEmpty(policy);
|
|
597
|
+
return {
|
|
598
|
+
...value,
|
|
599
|
+
enforcement: cloneJson(QA_POLICY_ENFORCEMENT),
|
|
600
|
+
require_desktop_mobile_screenshots: value.require_desktop_mobile_screenshots !== false,
|
|
601
|
+
require_checkout_flow: value.require_checkout_flow !== false,
|
|
602
|
+
require_post_purchase_flow: value.require_post_purchase_flow !== false,
|
|
603
|
+
fail_on_visible_placeholders: value.fail_on_visible_placeholders !== false,
|
|
604
|
+
compare_live_runtime_data_to_spec: value.compare_live_runtime_data_to_spec !== false,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function collectVariantSignals(spec, sourceAssetCrawl) {
|
|
609
|
+
const signals = new Set();
|
|
610
|
+
visitValues(spec, (value, keyPath) => {
|
|
611
|
+
const key = keyPath[keyPath.length - 1] || "";
|
|
612
|
+
if (!/(variant|color|colour|size|option|shade)/i.test(key)) return;
|
|
613
|
+
if (typeof value === "string" && value.trim().length <= 80) signals.add(value.trim().toLowerCase());
|
|
614
|
+
});
|
|
615
|
+
for (const ref of sourceAssetCrawl?.references || []) {
|
|
616
|
+
if (ref.asset_kind !== "image") continue;
|
|
617
|
+
const text = `${ref.source_path || ""} ${ref.normalized || ""}`.toLowerCase();
|
|
618
|
+
const matches = COLOR_WORDS.filter((color) => new RegExp(`(^|[^a-z])${escapeRegExp(color)}([^a-z]|$)`, "i").test(text));
|
|
619
|
+
for (const color of matches) signals.add(color);
|
|
620
|
+
}
|
|
621
|
+
return [...signals].filter(Boolean).sort();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function collectSpecPaymentMethods(spec) {
|
|
625
|
+
const methods = new Set();
|
|
626
|
+
visitValues(spec, (value, keyPath) => {
|
|
627
|
+
const path = keyPath.join(".").toLowerCase();
|
|
628
|
+
const inPaymentContext = /payment|wallet|express/.test(path);
|
|
629
|
+
if (!inPaymentContext) return;
|
|
630
|
+
if (typeof value === "string") {
|
|
631
|
+
const method = normalizePaymentMethod(value);
|
|
632
|
+
if (method) methods.add(method);
|
|
633
|
+
} else if (typeof value === "boolean" && value === true) {
|
|
634
|
+
const method = normalizePaymentMethod(keyPath[keyPath.length - 1]);
|
|
635
|
+
if (method) methods.add(method);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
return [...methods].sort();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function hasPromoSignals(spec) {
|
|
642
|
+
let found = false;
|
|
643
|
+
visitValues(spec, (value, keyPath) => {
|
|
644
|
+
if (found) return;
|
|
645
|
+
const path = keyPath.join(".").toLowerCase();
|
|
646
|
+
if (/(promo|voucher|coupon|discount|offer|timer|urgency|exit_intent)/.test(path) && value != null && value !== false) {
|
|
647
|
+
found = true;
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return found;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function hasOrderBumpSignals(value, keyPath = []) {
|
|
654
|
+
const key = keyPath[keyPath.length - 1] || "";
|
|
655
|
+
const normalizedKey = normalizeToken(key);
|
|
656
|
+
if (ORDER_BUMP_KEYS.has(normalizedKey)) {
|
|
657
|
+
if (isDisabledSignal(value)) return false;
|
|
658
|
+
if (isObject(value) && Object.hasOwn(value, "enabled") && isDisabledSignal(value.enabled)) return false;
|
|
659
|
+
if ((Array.isArray(value) || isObject(value)) && hasNestedOrderBumpSignals(value, keyPath)) return true;
|
|
660
|
+
return isMeaningfulSignal(value);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (Array.isArray(value)) {
|
|
664
|
+
return value.some((item, index) => hasOrderBumpSignals(item, [...keyPath, String(index)]));
|
|
665
|
+
}
|
|
666
|
+
if (isObject(value)) {
|
|
667
|
+
for (const [key, item] of Object.entries(value)) {
|
|
668
|
+
if (hasOrderBumpSignals(item, [...keyPath, key])) return true;
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (typeof value !== "string") return false;
|
|
674
|
+
|
|
675
|
+
const normalizedValue = normalizeToken(value);
|
|
676
|
+
return ORDER_BUMP_VALUE_KEYS.has(normalizedKey) && ORDER_BUMP_VALUES.has(normalizedValue);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function hasNestedOrderBumpSignals(value, keyPath) {
|
|
680
|
+
if (Array.isArray(value)) {
|
|
681
|
+
return value.some((item, index) => hasOrderBumpSignals(item, [...keyPath, String(index)]));
|
|
682
|
+
}
|
|
683
|
+
if (isObject(value)) {
|
|
684
|
+
return Object.entries(value).some(([key, item]) => hasOrderBumpSignals(item, [...keyPath, key]));
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function hasRegulatedSignals(spec) {
|
|
690
|
+
const text = JSON.stringify(spec || {}).toLowerCase();
|
|
691
|
+
return /\b(health|wellness|supplement|medical|doctor|clinical|cure|pain|anxiety|weight loss|finance|financial|loan|credit|insurance|investment)\b/.test(text);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function hasComplianceBoundary(brief) {
|
|
695
|
+
const compliance = brief.campaign_intent?.compliance || brief.compliance || {};
|
|
696
|
+
if (!isObject(compliance)) return false;
|
|
697
|
+
return ["forbidden_claims", "approved_benefit_language", "approved_claims", "copy_rules"].some((key) => {
|
|
698
|
+
const value = compliance[key];
|
|
699
|
+
return Array.isArray(value) ? value.length > 0 : isNonEmptyString(value);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function visitValues(value, visitor, keyPath = []) {
|
|
704
|
+
if (Array.isArray(value)) {
|
|
705
|
+
value.forEach((item, index) => visitValues(item, visitor, [...keyPath, String(index)]));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (isObject(value)) {
|
|
709
|
+
for (const [key, item] of Object.entries(value)) visitValues(item, visitor, [...keyPath, key]);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
visitor(value, keyPath);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function normalizePaymentList(value) {
|
|
716
|
+
const raw = Array.isArray(value) ? value : isNonEmptyString(value) ? [value] : [];
|
|
717
|
+
return [...new Set(raw.map(normalizePaymentMethod).filter(Boolean))].sort();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function normalizePaymentMethod(value) {
|
|
721
|
+
const normalized = normalizeToken(value);
|
|
722
|
+
return PAYMENT_METHOD_ALIASES.get(normalized) || null;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function normalizeToken(value) {
|
|
726
|
+
return String(value || "")
|
|
727
|
+
.trim()
|
|
728
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
729
|
+
.replace(/[^a-z0-9]+/gi, "_")
|
|
730
|
+
.replace(/^_+|_+$/g, "")
|
|
731
|
+
.toLowerCase();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function normalizeStringArray(value) {
|
|
735
|
+
if (!Array.isArray(value)) return [];
|
|
736
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function buildBriefFormatForPath(path) {
|
|
740
|
+
const ext = extname(path).toLowerCase();
|
|
741
|
+
return ext === ".json" ? "json" : "yaml";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function safeIsFile(path) {
|
|
745
|
+
try {
|
|
746
|
+
return existsSync(path) && statSync(path).isFile();
|
|
747
|
+
} catch {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function objectOrEmpty(value) {
|
|
753
|
+
return isObject(value) ? value : {};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function cloneJson(value) {
|
|
757
|
+
return JSON.parse(JSON.stringify(value ?? {}));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function isObject(value) {
|
|
761
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function isNonEmptyString(value) {
|
|
765
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function isMeaningfulSignal(value) {
|
|
769
|
+
if (isDisabledSignal(value)) return false;
|
|
770
|
+
if (Array.isArray(value)) return value.some(isMeaningfulSignal);
|
|
771
|
+
if (isObject(value)) return Object.values(value).some(isMeaningfulSignal);
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function isDisabledSignal(value) {
|
|
776
|
+
if (value == null || value === false) return true;
|
|
777
|
+
if (typeof value === "number") return value === 0;
|
|
778
|
+
if (typeof value === "string") return value.trim().length === 0 || DISABLED_SIGNAL_VALUES.has(normalizeToken(value));
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|