@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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelHypothesisLength — enforces the [10, 500] character bound on each
|
|
3
|
+
* funnel's `hypothesis` field. Short hypotheses signal a draft funnel that
|
|
4
|
+
* hasn't been thought through; long ones suggest the wrong field is being
|
|
5
|
+
* used.
|
|
6
|
+
*
|
|
7
|
+
* A hypothesis states what one path is testing against another, so it is
|
|
8
|
+
* required only when the spec has two or more funnels. A single-funnel spec
|
|
9
|
+
* may omit it (#294). Absent means null, undefined, or a string that is
|
|
10
|
+
* empty after trimming: editors clear a text field to "", not to null, and
|
|
11
|
+
* an optional field should not fail on the way an editor spells "nothing".
|
|
12
|
+
* When a real value is present the length bound still applies regardless of
|
|
13
|
+
* funnel count, and both bounds measure the trimmed value, so padding can
|
|
14
|
+
* neither rescue a short hypothesis nor sink a long one. The schema already
|
|
15
|
+
* types the field as optional; this rule owns the conditional requirement.
|
|
16
|
+
*
|
|
17
|
+
* Message text inherited verbatim from the pre-#110 validator at migration
|
|
18
|
+
* time — substrings ("at least 10 chars", "at most 500 chars") are matched
|
|
19
|
+
* by caller tests.
|
|
20
|
+
*/
|
|
21
|
+
import type { Rule } from '../types.ts';
|
|
22
|
+
export declare const FunnelHypothesisLength: Rule;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelHypothesisLength — enforces the [10, 500] character bound on each
|
|
3
|
+
* funnel's `hypothesis` field. Short hypotheses signal a draft funnel that
|
|
4
|
+
* hasn't been thought through; long ones suggest the wrong field is being
|
|
5
|
+
* used.
|
|
6
|
+
*
|
|
7
|
+
* A hypothesis states what one path is testing against another, so it is
|
|
8
|
+
* required only when the spec has two or more funnels. A single-funnel spec
|
|
9
|
+
* may omit it (#294). Absent means null, undefined, or a string that is
|
|
10
|
+
* empty after trimming: editors clear a text field to "", not to null, and
|
|
11
|
+
* an optional field should not fail on the way an editor spells "nothing".
|
|
12
|
+
* When a real value is present the length bound still applies regardless of
|
|
13
|
+
* funnel count, and both bounds measure the trimmed value, so padding can
|
|
14
|
+
* neither rescue a short hypothesis nor sink a long one. The schema already
|
|
15
|
+
* types the field as optional; this rule owns the conditional requirement.
|
|
16
|
+
*
|
|
17
|
+
* Message text inherited verbatim from the pre-#110 validator at migration
|
|
18
|
+
* time — substrings ("at least 10 chars", "at most 500 chars") are matched
|
|
19
|
+
* by caller tests.
|
|
20
|
+
*/
|
|
21
|
+
const MIN_LENGTH = 10;
|
|
22
|
+
const MAX_LENGTH = 500;
|
|
23
|
+
// Below this many funnels there is nothing to compare, so no hypothesis is owed.
|
|
24
|
+
const MIN_FUNNELS_FOR_REQUIRED = 2;
|
|
25
|
+
export const FunnelHypothesisLength = {
|
|
26
|
+
id: 'FunnelHypothesisLength',
|
|
27
|
+
severity: 'error',
|
|
28
|
+
tags: ['fast', 'spec-only'],
|
|
29
|
+
check(spec) {
|
|
30
|
+
const violations = [];
|
|
31
|
+
const hypothesisRequired = spec.funnels.length >= MIN_FUNNELS_FOR_REQUIRED;
|
|
32
|
+
spec.funnels.forEach((funnel, idx) => {
|
|
33
|
+
const fid = funnel.id || '(unnamed)';
|
|
34
|
+
const hyp = funnel.hypothesis == null ? '' : String(funnel.hypothesis).trim();
|
|
35
|
+
const absent = hyp === '';
|
|
36
|
+
if (absent && !hypothesisRequired)
|
|
37
|
+
return;
|
|
38
|
+
const path = `/funnels/${idx}/hypothesis`;
|
|
39
|
+
if (hyp.length < MIN_LENGTH) {
|
|
40
|
+
violations.push({
|
|
41
|
+
ruleId: 'FunnelHypothesisLength',
|
|
42
|
+
severity: 'error',
|
|
43
|
+
message: `Funnel "${fid}" — hypothesis must be at least ${MIN_LENGTH} chars (got ${hyp.length}).`,
|
|
44
|
+
path,
|
|
45
|
+
data: { funnelId: fid, length: hyp.length, min: MIN_LENGTH },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else if (hyp.length > MAX_LENGTH) {
|
|
49
|
+
violations.push({
|
|
50
|
+
ruleId: 'FunnelHypothesisLength',
|
|
51
|
+
severity: 'error',
|
|
52
|
+
message: `Funnel "${fid}" — hypothesis must be at most ${MAX_LENGTH} chars (got ${hyp.length}).`,
|
|
53
|
+
path,
|
|
54
|
+
data: { funnelId: fid, length: hyp.length, max: MAX_LENGTH },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return violations;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelIdentity — enforces per-funnel identity invariants:
|
|
3
|
+
* - id is present
|
|
4
|
+
* - id is globally unique across funnels
|
|
5
|
+
* - name is present (non-empty after trim)
|
|
6
|
+
*
|
|
7
|
+
* One rule, multiple potential violations. Message text was preserved
|
|
8
|
+
* verbatim from the pre-#110 validator at migration time and is now the
|
|
9
|
+
* canonical wording — caller tests across the repo match these substrings.
|
|
10
|
+
*
|
|
11
|
+
* Note: hypothesis length is a separate concern (FunnelHypothesisLength) so
|
|
12
|
+
* each can be filtered independently by tag.
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from '../types.ts';
|
|
15
|
+
export declare const FunnelIdentity: Rule;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelIdentity — enforces per-funnel identity invariants:
|
|
3
|
+
* - id is present
|
|
4
|
+
* - id is globally unique across funnels
|
|
5
|
+
* - name is present (non-empty after trim)
|
|
6
|
+
*
|
|
7
|
+
* One rule, multiple potential violations. Message text was preserved
|
|
8
|
+
* verbatim from the pre-#110 validator at migration time and is now the
|
|
9
|
+
* canonical wording — caller tests across the repo match these substrings.
|
|
10
|
+
*
|
|
11
|
+
* Note: hypothesis length is a separate concern (FunnelHypothesisLength) so
|
|
12
|
+
* each can be filtered independently by tag.
|
|
13
|
+
*/
|
|
14
|
+
export const FunnelIdentity = {
|
|
15
|
+
id: 'FunnelIdentity',
|
|
16
|
+
severity: 'error',
|
|
17
|
+
tags: ['fast', 'structure', 'spec-only'],
|
|
18
|
+
check(spec) {
|
|
19
|
+
const violations = [];
|
|
20
|
+
const seenIds = new Set();
|
|
21
|
+
spec.funnels.forEach((funnel, idx) => {
|
|
22
|
+
const fid = funnel.id || '(unnamed)';
|
|
23
|
+
const path = `/funnels/${idx}`;
|
|
24
|
+
if (!funnel.id) {
|
|
25
|
+
violations.push({
|
|
26
|
+
ruleId: 'FunnelIdentity',
|
|
27
|
+
severity: 'error',
|
|
28
|
+
message: 'Funnel is missing an id.',
|
|
29
|
+
path: `${path}/id`,
|
|
30
|
+
data: { funnelId: fid, funnelIdx: idx },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else if (seenIds.has(funnel.id)) {
|
|
34
|
+
violations.push({
|
|
35
|
+
ruleId: 'FunnelIdentity',
|
|
36
|
+
severity: 'error',
|
|
37
|
+
message: `Duplicate funnel id "${funnel.id}".`,
|
|
38
|
+
path: `${path}/id`,
|
|
39
|
+
data: { funnelId: funnel.id, funnelIdx: idx },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
seenIds.add(funnel.id);
|
|
44
|
+
}
|
|
45
|
+
if (!funnel.name || String(funnel.name).trim() === '') {
|
|
46
|
+
violations.push({
|
|
47
|
+
ruleId: 'FunnelIdentity',
|
|
48
|
+
severity: 'error',
|
|
49
|
+
message: `Funnel "${fid}" — name is required.`,
|
|
50
|
+
path: `${path}/name`,
|
|
51
|
+
data: { funnelId: fid, funnelIdx: idx },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return violations;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelWeightSum — flags funnel weight problems.
|
|
3
|
+
*
|
|
4
|
+
* Two failure modes, both emitted under the same rule id:
|
|
5
|
+
* 1. Per-funnel weight outside [0, 100] or non-finite.
|
|
6
|
+
* → Violation at /funnels/<idx>/weight, severity error.
|
|
7
|
+
* 2. Aggregate sum of valid weights != 100 (± 0.01 tolerance for float input
|
|
8
|
+
* like 33.33 + 33.33 + 33.34).
|
|
9
|
+
* → Violation at /funnels, severity error.
|
|
10
|
+
*
|
|
11
|
+
* Tags: fast, structure, spec-only. Cheap enough for per-keystroke Map
|
|
12
|
+
* Builder contexts (Decision: this is the "fast" rule shape we promised).
|
|
13
|
+
*
|
|
14
|
+
* NOTE: This rule could be split into FunnelWeightRange (per-funnel) and
|
|
15
|
+
* FunnelWeightSum (aggregate) for finer composition. Kept fused for now —
|
|
16
|
+
* the two checks share traversal and tend to fire together; splitting is
|
|
17
|
+
* cheap to do later when a caller actually needs to subset.
|
|
18
|
+
*/
|
|
19
|
+
import type { Rule } from '../types.ts';
|
|
20
|
+
export declare const FunnelWeightSum: Rule;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelWeightSum — flags funnel weight problems.
|
|
3
|
+
*
|
|
4
|
+
* Two failure modes, both emitted under the same rule id:
|
|
5
|
+
* 1. Per-funnel weight outside [0, 100] or non-finite.
|
|
6
|
+
* → Violation at /funnels/<idx>/weight, severity error.
|
|
7
|
+
* 2. Aggregate sum of valid weights != 100 (± 0.01 tolerance for float input
|
|
8
|
+
* like 33.33 + 33.33 + 33.34).
|
|
9
|
+
* → Violation at /funnels, severity error.
|
|
10
|
+
*
|
|
11
|
+
* Tags: fast, structure, spec-only. Cheap enough for per-keystroke Map
|
|
12
|
+
* Builder contexts (Decision: this is the "fast" rule shape we promised).
|
|
13
|
+
*
|
|
14
|
+
* NOTE: This rule could be split into FunnelWeightRange (per-funnel) and
|
|
15
|
+
* FunnelWeightSum (aggregate) for finer composition. Kept fused for now —
|
|
16
|
+
* the two checks share traversal and tend to fire together; splitting is
|
|
17
|
+
* cheap to do later when a caller actually needs to subset.
|
|
18
|
+
*/
|
|
19
|
+
const SUM_TOLERANCE = 0.01;
|
|
20
|
+
const EXPECTED_SUM = 100;
|
|
21
|
+
function toFiniteWeight(value) {
|
|
22
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
23
|
+
return Number.isFinite(num) ? num : null;
|
|
24
|
+
}
|
|
25
|
+
export const FunnelWeightSum = {
|
|
26
|
+
id: 'FunnelWeightSum',
|
|
27
|
+
severity: 'error',
|
|
28
|
+
tags: ['fast', 'structure', 'spec-only'],
|
|
29
|
+
check(spec) {
|
|
30
|
+
const violations = [];
|
|
31
|
+
let sum = 0;
|
|
32
|
+
let anyInRange = false;
|
|
33
|
+
spec.funnels.forEach((funnel, idx) => {
|
|
34
|
+
const weight = toFiniteWeight(funnel.weight);
|
|
35
|
+
if (weight == null || weight < 0 || weight > EXPECTED_SUM) {
|
|
36
|
+
// Message text is the canonical wording, inherited from the pre-#110
|
|
37
|
+
// validator at migration time. Caller tests across the repo match on
|
|
38
|
+
// this substring; improvements land in a follow-up when needed.
|
|
39
|
+
violations.push({
|
|
40
|
+
ruleId: 'FunnelWeightSum',
|
|
41
|
+
severity: 'error',
|
|
42
|
+
message: `Funnel "${funnel.id ?? '(unnamed)'}" — weight must be a number in [0, 100] (got ${funnel.weight}).`,
|
|
43
|
+
path: `/funnels/${idx}/weight`,
|
|
44
|
+
data: { funnelId: funnel.id, weight: funnel.weight },
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
anyInRange = true;
|
|
49
|
+
sum += weight;
|
|
50
|
+
});
|
|
51
|
+
// If no funnels had a valid weight, the sum check would be meaningless;
|
|
52
|
+
// the per-funnel violations carry the signal.
|
|
53
|
+
if (spec.funnels.length > 0 && anyInRange) {
|
|
54
|
+
if (Math.abs(sum - EXPECTED_SUM) > SUM_TOLERANCE) {
|
|
55
|
+
violations.push({
|
|
56
|
+
ruleId: 'FunnelWeightSum',
|
|
57
|
+
severity: 'error',
|
|
58
|
+
message: `Funnel weights must sum to 100 (got ${sum}).`,
|
|
59
|
+
path: '/funnels',
|
|
60
|
+
data: { sum, expected: EXPECTED_SUM, tolerance: SUM_TOLERANCE },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return violations;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule registry — preset RuleSet constants.
|
|
3
|
+
*
|
|
4
|
+
* Callers either pick a preset (allRules / fastRules / specOnlyRules) or
|
|
5
|
+
* filter inline:
|
|
6
|
+
*
|
|
7
|
+
* const myRules = allRules.filter(r => r.tags.includes('fast'))
|
|
8
|
+
*
|
|
9
|
+
* Tags are a closed taxonomy (see ../types.ts). To add a rule:
|
|
10
|
+
* 1. Create rules/<your-rule>.ts exporting a `Rule`.
|
|
11
|
+
* 2. Add it to `allRules` below.
|
|
12
|
+
* 3. fastRules / specOnlyRules pick up via tag filter automatically.
|
|
13
|
+
*
|
|
14
|
+
* Rules are listed in a stable order — the order callers see violations in
|
|
15
|
+
* for a multi-rule run. Keep cheap/metadata checks first so they surface
|
|
16
|
+
* before downstream complaints.
|
|
17
|
+
*/
|
|
18
|
+
import type { RuleSet } from '../types.ts';
|
|
19
|
+
export { SchemaVersion, SUPPORTED_SCHEMA_VERSIONS } from './schema-version.ts';
|
|
20
|
+
export { SdkVersion } from './sdk-version.ts';
|
|
21
|
+
export { UnknownTopLevelFields, KNOWN_TOP_LEVEL_FIELDS, KNOWN_CAMPAIGN_FIELDS } from './unknown-top-level-fields.ts';
|
|
22
|
+
export { CampaignMetadata } from './campaign-metadata.ts';
|
|
23
|
+
export { FunnelCount } from './funnel-count.ts';
|
|
24
|
+
export { FunnelIdentity } from './funnel-identity.ts';
|
|
25
|
+
export { FunnelHypothesisLength } from './funnel-hypothesis-length.ts';
|
|
26
|
+
export { FunnelWeightSum } from './funnel-weight-sum.ts';
|
|
27
|
+
export { PageCount } from './page-count.ts';
|
|
28
|
+
export { ThankYouRequirement } from './thank-you-requirement.ts';
|
|
29
|
+
export { PageIdUniqueness } from './page-id-uniqueness.ts';
|
|
30
|
+
export { UpsellHasPackages } from './upsell-has-packages.ts';
|
|
31
|
+
export { UpsellRoutingComplete } from './upsell-routing-complete.ts';
|
|
32
|
+
export { RouteFieldIgnoredForPageType } from './route-field-ignored-for-page-type.ts';
|
|
33
|
+
export { CheckoutHasSuccessUrl } from './checkout-has-success-url.ts';
|
|
34
|
+
export { UpsellWithoutCheckout } from './upsell-without-checkout.ts';
|
|
35
|
+
export { DownsellWithoutUpsell } from './downsell-without-upsell.ts';
|
|
36
|
+
export { PackagePricingSanity } from './package-pricing-sanity.ts';
|
|
37
|
+
export { ShippingMethodsPresent } from './shipping-methods-present.ts';
|
|
38
|
+
export { ShippingCountriesShape } from './shipping-countries-shape.ts';
|
|
39
|
+
export { OfferRefIntegrity } from './offer-ref-integrity.ts';
|
|
40
|
+
export { ExitIntentValidation } from './exit-intent-validation.ts';
|
|
41
|
+
export { PromoCodeInputValidation } from './promo-code-input-validation.ts';
|
|
42
|
+
export { DesignSourceShape } from './design-source-shape.ts';
|
|
43
|
+
export { AssemblyHintsShape } from './assembly-hints-shape.ts';
|
|
44
|
+
export { PromoCodesShape } from './promo-codes-shape.ts';
|
|
45
|
+
export { VariantLabelsShape } from './variant-labels-shape.ts';
|
|
46
|
+
export { StoreProfileShape } from './store-profile-shape.ts';
|
|
47
|
+
export { AnalyticsContractShape } from './analytics-contract-shape.ts';
|
|
48
|
+
export { CycleDetection } from './cycle-detection.ts';
|
|
49
|
+
/**
|
|
50
|
+
* Every rule in the registry. Callers should generally compose subsets from
|
|
51
|
+
* this rather than importing individual rules — it's the stable surface.
|
|
52
|
+
*
|
|
53
|
+
* Order: metadata/identity checks first (cheap, frame the spec), then
|
|
54
|
+
* structural checks (count, weights), then topology (cycles). Violations
|
|
55
|
+
* surface in this order so operators see "your schema_version is missing"
|
|
56
|
+
* before "your routing has a cycle."
|
|
57
|
+
*/
|
|
58
|
+
export declare const allRules: RuleSet;
|
|
59
|
+
/**
|
|
60
|
+
* Cheap rules safe for per-keystroke Map Builder contexts. Selected by the
|
|
61
|
+
* `fast` tag. Graph traversal rules (CycleDetection) intentionally absent.
|
|
62
|
+
*/
|
|
63
|
+
export declare const fastRules: RuleSet;
|
|
64
|
+
/**
|
|
65
|
+
* Rules that run against the spec alone, no live deployment needed. Used by
|
|
66
|
+
* QA spec-only mode and any CI context without a deployed URL. Selected by
|
|
67
|
+
* the `spec-only` tag.
|
|
68
|
+
*/
|
|
69
|
+
export declare const specOnlyRules: RuleSet;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule registry — preset RuleSet constants.
|
|
3
|
+
*
|
|
4
|
+
* Callers either pick a preset (allRules / fastRules / specOnlyRules) or
|
|
5
|
+
* filter inline:
|
|
6
|
+
*
|
|
7
|
+
* const myRules = allRules.filter(r => r.tags.includes('fast'))
|
|
8
|
+
*
|
|
9
|
+
* Tags are a closed taxonomy (see ../types.ts). To add a rule:
|
|
10
|
+
* 1. Create rules/<your-rule>.ts exporting a `Rule`.
|
|
11
|
+
* 2. Add it to `allRules` below.
|
|
12
|
+
* 3. fastRules / specOnlyRules pick up via tag filter automatically.
|
|
13
|
+
*
|
|
14
|
+
* Rules are listed in a stable order — the order callers see violations in
|
|
15
|
+
* for a multi-rule run. Keep cheap/metadata checks first so they surface
|
|
16
|
+
* before downstream complaints.
|
|
17
|
+
*/
|
|
18
|
+
import { SchemaVersion } from "./schema-version.js";
|
|
19
|
+
import { SdkVersion } from "./sdk-version.js";
|
|
20
|
+
import { UnknownTopLevelFields } from "./unknown-top-level-fields.js";
|
|
21
|
+
import { FunnelCount } from "./funnel-count.js";
|
|
22
|
+
import { FunnelIdentity } from "./funnel-identity.js";
|
|
23
|
+
import { FunnelHypothesisLength } from "./funnel-hypothesis-length.js";
|
|
24
|
+
import { FunnelWeightSum } from "./funnel-weight-sum.js";
|
|
25
|
+
import { PageCount } from "./page-count.js";
|
|
26
|
+
import { ThankYouRequirement } from "./thank-you-requirement.js";
|
|
27
|
+
import { PageIdUniqueness } from "./page-id-uniqueness.js";
|
|
28
|
+
import { UpsellHasPackages } from "./upsell-has-packages.js";
|
|
29
|
+
import { UpsellRoutingComplete } from "./upsell-routing-complete.js";
|
|
30
|
+
import { RouteTargetResolves } from "./route-target-resolves.js";
|
|
31
|
+
import { RouteFieldIgnoredForPageType } from "./route-field-ignored-for-page-type.js";
|
|
32
|
+
import { CheckoutHasSuccessUrl } from "./checkout-has-success-url.js";
|
|
33
|
+
import { UpsellWithoutCheckout } from "./upsell-without-checkout.js";
|
|
34
|
+
import { DownsellWithoutUpsell } from "./downsell-without-upsell.js";
|
|
35
|
+
import { CampaignMetadata } from "./campaign-metadata.js";
|
|
36
|
+
import { ShippingMethodsPresent } from "./shipping-methods-present.js";
|
|
37
|
+
import { ShippingCountriesShape } from "./shipping-countries-shape.js";
|
|
38
|
+
import { PackagePricingSanity } from "./package-pricing-sanity.js";
|
|
39
|
+
import { OfferRefIntegrity } from "./offer-ref-integrity.js";
|
|
40
|
+
import { ExitIntentValidation } from "./exit-intent-validation.js";
|
|
41
|
+
import { PromoCodeInputValidation } from "./promo-code-input-validation.js";
|
|
42
|
+
import { DesignSourceShape } from "./design-source-shape.js";
|
|
43
|
+
import { AssemblyHintsShape } from "./assembly-hints-shape.js";
|
|
44
|
+
import { PromoCodesShape } from "./promo-codes-shape.js";
|
|
45
|
+
import { VariantLabelsShape } from "./variant-labels-shape.js";
|
|
46
|
+
import { StoreProfileShape } from "./store-profile-shape.js";
|
|
47
|
+
import { AnalyticsContractShape } from "./analytics-contract-shape.js";
|
|
48
|
+
import { CycleDetection } from "./cycle-detection.js";
|
|
49
|
+
export { SchemaVersion, SUPPORTED_SCHEMA_VERSIONS } from "./schema-version.js";
|
|
50
|
+
export { SdkVersion } from "./sdk-version.js";
|
|
51
|
+
export { UnknownTopLevelFields, KNOWN_TOP_LEVEL_FIELDS, KNOWN_CAMPAIGN_FIELDS } from "./unknown-top-level-fields.js";
|
|
52
|
+
export { CampaignMetadata } from "./campaign-metadata.js";
|
|
53
|
+
export { FunnelCount } from "./funnel-count.js";
|
|
54
|
+
export { FunnelIdentity } from "./funnel-identity.js";
|
|
55
|
+
export { FunnelHypothesisLength } from "./funnel-hypothesis-length.js";
|
|
56
|
+
export { FunnelWeightSum } from "./funnel-weight-sum.js";
|
|
57
|
+
export { PageCount } from "./page-count.js";
|
|
58
|
+
export { ThankYouRequirement } from "./thank-you-requirement.js";
|
|
59
|
+
export { PageIdUniqueness } from "./page-id-uniqueness.js";
|
|
60
|
+
export { UpsellHasPackages } from "./upsell-has-packages.js";
|
|
61
|
+
export { UpsellRoutingComplete } from "./upsell-routing-complete.js";
|
|
62
|
+
export { RouteFieldIgnoredForPageType } from "./route-field-ignored-for-page-type.js";
|
|
63
|
+
export { CheckoutHasSuccessUrl } from "./checkout-has-success-url.js";
|
|
64
|
+
export { UpsellWithoutCheckout } from "./upsell-without-checkout.js";
|
|
65
|
+
export { DownsellWithoutUpsell } from "./downsell-without-upsell.js";
|
|
66
|
+
export { PackagePricingSanity } from "./package-pricing-sanity.js";
|
|
67
|
+
export { ShippingMethodsPresent } from "./shipping-methods-present.js";
|
|
68
|
+
export { ShippingCountriesShape } from "./shipping-countries-shape.js";
|
|
69
|
+
export { OfferRefIntegrity } from "./offer-ref-integrity.js";
|
|
70
|
+
export { ExitIntentValidation } from "./exit-intent-validation.js";
|
|
71
|
+
export { PromoCodeInputValidation } from "./promo-code-input-validation.js";
|
|
72
|
+
export { DesignSourceShape } from "./design-source-shape.js";
|
|
73
|
+
export { AssemblyHintsShape } from "./assembly-hints-shape.js";
|
|
74
|
+
export { PromoCodesShape } from "./promo-codes-shape.js";
|
|
75
|
+
export { VariantLabelsShape } from "./variant-labels-shape.js";
|
|
76
|
+
export { StoreProfileShape } from "./store-profile-shape.js";
|
|
77
|
+
export { AnalyticsContractShape } from "./analytics-contract-shape.js";
|
|
78
|
+
export { CycleDetection } from "./cycle-detection.js";
|
|
79
|
+
/**
|
|
80
|
+
* Every rule in the registry. Callers should generally compose subsets from
|
|
81
|
+
* this rather than importing individual rules — it's the stable surface.
|
|
82
|
+
*
|
|
83
|
+
* Order: metadata/identity checks first (cheap, frame the spec), then
|
|
84
|
+
* structural checks (count, weights), then topology (cycles). Violations
|
|
85
|
+
* surface in this order so operators see "your schema_version is missing"
|
|
86
|
+
* before "your routing has a cycle."
|
|
87
|
+
*/
|
|
88
|
+
export const allRules = [
|
|
89
|
+
SchemaVersion,
|
|
90
|
+
SdkVersion,
|
|
91
|
+
UnknownTopLevelFields,
|
|
92
|
+
CampaignMetadata,
|
|
93
|
+
FunnelCount,
|
|
94
|
+
FunnelIdentity,
|
|
95
|
+
FunnelHypothesisLength,
|
|
96
|
+
FunnelWeightSum,
|
|
97
|
+
PageCount,
|
|
98
|
+
ThankYouRequirement,
|
|
99
|
+
PageIdUniqueness,
|
|
100
|
+
UpsellHasPackages,
|
|
101
|
+
UpsellRoutingComplete,
|
|
102
|
+
RouteTargetResolves,
|
|
103
|
+
RouteFieldIgnoredForPageType,
|
|
104
|
+
CheckoutHasSuccessUrl,
|
|
105
|
+
UpsellWithoutCheckout,
|
|
106
|
+
DownsellWithoutUpsell,
|
|
107
|
+
PackagePricingSanity,
|
|
108
|
+
ShippingMethodsPresent,
|
|
109
|
+
ShippingCountriesShape,
|
|
110
|
+
OfferRefIntegrity,
|
|
111
|
+
ExitIntentValidation,
|
|
112
|
+
PromoCodeInputValidation,
|
|
113
|
+
DesignSourceShape,
|
|
114
|
+
AssemblyHintsShape,
|
|
115
|
+
PromoCodesShape,
|
|
116
|
+
VariantLabelsShape,
|
|
117
|
+
StoreProfileShape,
|
|
118
|
+
AnalyticsContractShape,
|
|
119
|
+
CycleDetection,
|
|
120
|
+
];
|
|
121
|
+
/**
|
|
122
|
+
* Cheap rules safe for per-keystroke Map Builder contexts. Selected by the
|
|
123
|
+
* `fast` tag. Graph traversal rules (CycleDetection) intentionally absent.
|
|
124
|
+
*/
|
|
125
|
+
export const fastRules = allRules.filter((r) => r.tags.includes('fast'));
|
|
126
|
+
/**
|
|
127
|
+
* Rules that run against the spec alone, no live deployment needed. Used by
|
|
128
|
+
* QA spec-only mode and any CI context without a deployed URL. Selected by
|
|
129
|
+
* the `spec-only` tag.
|
|
130
|
+
*/
|
|
131
|
+
export const specOnlyRules = allRules.filter((r) => r.tags.includes('spec-only'));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OfferRefIntegrity — page-level `offers[]` entries must reference ref_ids
|
|
3
|
+
* that exist in the spec's `offers[]` catalog. If an offer ref doesn't
|
|
4
|
+
* resolve, the runtime can't render the offer and the page silently degrades.
|
|
5
|
+
*
|
|
6
|
+
* Warning severity (not error) — preserves legacy classification. The
|
|
7
|
+
* mismatch usually means the spec is stale relative to the upstream API and
|
|
8
|
+
* needs a refresh.
|
|
9
|
+
*
|
|
10
|
+
* Message text inherited verbatim from the pre-#110 validator at migration time.
|
|
11
|
+
*/
|
|
12
|
+
import type { Rule } from '../types.ts';
|
|
13
|
+
export declare const OfferRefIntegrity: Rule;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OfferRefIntegrity — page-level `offers[]` entries must reference ref_ids
|
|
3
|
+
* that exist in the spec's `offers[]` catalog. If an offer ref doesn't
|
|
4
|
+
* resolve, the runtime can't render the offer and the page silently degrades.
|
|
5
|
+
*
|
|
6
|
+
* Warning severity (not error) — preserves legacy classification. The
|
|
7
|
+
* mismatch usually means the spec is stale relative to the upstream API and
|
|
8
|
+
* needs a refresh.
|
|
9
|
+
*
|
|
10
|
+
* Message text inherited verbatim from the pre-#110 validator at migration time.
|
|
11
|
+
*/
|
|
12
|
+
function buildRefIdIndex(catalog) {
|
|
13
|
+
const set = new Set();
|
|
14
|
+
for (const offer of catalog) {
|
|
15
|
+
// Catalog offers without a ref_id (e.g. package_ref_id-keyed offers) are
|
|
16
|
+
// excluded from the index — indexing String(undefined) would let a page
|
|
17
|
+
// offer that also lacks ref_id "resolve" against "undefined" and turn the
|
|
18
|
+
// whole rule into a silent pass.
|
|
19
|
+
if (offer.ref_id === undefined || offer.ref_id === null)
|
|
20
|
+
continue;
|
|
21
|
+
set.add(String(offer.ref_id));
|
|
22
|
+
}
|
|
23
|
+
return set;
|
|
24
|
+
}
|
|
25
|
+
export const OfferRefIntegrity = {
|
|
26
|
+
id: 'OfferRefIntegrity',
|
|
27
|
+
severity: 'warning',
|
|
28
|
+
tags: ['fast', 'references', 'spec-only'],
|
|
29
|
+
check(spec) {
|
|
30
|
+
const catalog = spec.offers ?? [];
|
|
31
|
+
const refIds = buildRefIdIndex(catalog);
|
|
32
|
+
const violations = [];
|
|
33
|
+
spec.funnels.forEach((funnel, funnelIdx) => {
|
|
34
|
+
const pages = funnel.pages ?? [];
|
|
35
|
+
pages.forEach((page, pageIdx) => {
|
|
36
|
+
if (!page.offers || page.offers.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
page.offers.forEach((offer, offerIdx) => {
|
|
39
|
+
// Page offers with no ref_id are skipped, not flagged: offer identity
|
|
40
|
+
// canonicalization between ref_id and package_ref_id is deliberately
|
|
41
|
+
// deferred to a later decision pass, so this rule only judges refs
|
|
42
|
+
// that actually exist.
|
|
43
|
+
if (offer.ref_id === undefined || offer.ref_id === null)
|
|
44
|
+
return;
|
|
45
|
+
if (!refIds.has(String(offer.ref_id))) {
|
|
46
|
+
violations.push({
|
|
47
|
+
ruleId: 'OfferRefIntegrity',
|
|
48
|
+
severity: 'warning',
|
|
49
|
+
message: `"${page.label}" — Offer ref ${offer.ref_id} not found in campaign offers. Refresh API data to sync.`,
|
|
50
|
+
path: `/funnels/${funnelIdx}/pages/${pageIdx}/offers/${offerIdx}/ref_id`,
|
|
51
|
+
data: { pageId: page.id, refId: offer.ref_id },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return violations;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PackagePricingSanity — per-package sanity check: a package with price_retail
|
|
3
|
+
* explicitly set to $0 while price > 0 is almost certainly a data entry error
|
|
4
|
+
* (the retail and sale prices got swapped, or someone meant to leave retail
|
|
5
|
+
* unset rather than zero it out).
|
|
6
|
+
*
|
|
7
|
+
* Warning severity. Message text inherited verbatim from the pre-#110
|
|
8
|
+
* validator. Note the parseFloat() coercion — packages sometimes
|
|
9
|
+
* arrive with string-typed prices from upstream CSV imports.
|
|
10
|
+
*/
|
|
11
|
+
import type { Rule } from '../types.ts';
|
|
12
|
+
export declare const PackagePricingSanity: Rule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PackagePricingSanity — per-package sanity check: a package with price_retail
|
|
3
|
+
* explicitly set to $0 while price > 0 is almost certainly a data entry error
|
|
4
|
+
* (the retail and sale prices got swapped, or someone meant to leave retail
|
|
5
|
+
* unset rather than zero it out).
|
|
6
|
+
*
|
|
7
|
+
* Warning severity. Message text inherited verbatim from the pre-#110
|
|
8
|
+
* validator. Note the parseFloat() coercion — packages sometimes
|
|
9
|
+
* arrive with string-typed prices from upstream CSV imports.
|
|
10
|
+
*/
|
|
11
|
+
export const PackagePricingSanity = {
|
|
12
|
+
id: 'PackagePricingSanity',
|
|
13
|
+
severity: 'warning',
|
|
14
|
+
tags: ['fast', 'spec-only'],
|
|
15
|
+
check(spec) {
|
|
16
|
+
const violations = [];
|
|
17
|
+
spec.funnels.forEach((funnel, funnelIdx) => {
|
|
18
|
+
const pages = funnel.pages ?? [];
|
|
19
|
+
pages.forEach((page, pageIdx) => {
|
|
20
|
+
if (!page.packages)
|
|
21
|
+
return;
|
|
22
|
+
page.packages.forEach((pkg, pkgIdx) => {
|
|
23
|
+
if (pkg.price_retail === undefined)
|
|
24
|
+
return;
|
|
25
|
+
const retail = parseFloat(String(pkg.price_retail));
|
|
26
|
+
const price = parseFloat(String(pkg.price));
|
|
27
|
+
if (retail === 0 && price > 0) {
|
|
28
|
+
const name = pkg.name || pkg.ref_id;
|
|
29
|
+
violations.push({
|
|
30
|
+
ruleId: 'PackagePricingSanity',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
message: `"${page.label}" — Package "${name}" has retail price $0.00. Verify pricing with offers model.`,
|
|
33
|
+
path: `/funnels/${funnelIdx}/pages/${pageIdx}/packages/${pkgIdx}/price_retail`,
|
|
34
|
+
data: { pageId: page.id, packageName: name, price, priceRetail: retail },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
return violations;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageCount — requires at least one page somewhere in the spec (aggregated
|
|
3
|
+
* across all funnels). When this fails, every downstream page-level rule
|
|
4
|
+
* has nothing to check, so this is the first signal operators see.
|
|
5
|
+
*
|
|
6
|
+
* Message wording: "Funnel must have at least 1 page." Inherited verbatim
|
|
7
|
+
* from the pre-#110 validator. Reads as singular but the check is across
|
|
8
|
+
* the union of funnels.
|
|
9
|
+
*/
|
|
10
|
+
import type { Rule } from '../types.ts';
|
|
11
|
+
export declare const PageCount: Rule;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageCount — requires at least one page somewhere in the spec (aggregated
|
|
3
|
+
* across all funnels). When this fails, every downstream page-level rule
|
|
4
|
+
* has nothing to check, so this is the first signal operators see.
|
|
5
|
+
*
|
|
6
|
+
* Message wording: "Funnel must have at least 1 page." Inherited verbatim
|
|
7
|
+
* from the pre-#110 validator. Reads as singular but the check is across
|
|
8
|
+
* the union of funnels.
|
|
9
|
+
*/
|
|
10
|
+
export const PageCount = {
|
|
11
|
+
id: 'PageCount',
|
|
12
|
+
severity: 'error',
|
|
13
|
+
tags: ['fast', 'structure', 'spec-only'],
|
|
14
|
+
check(spec) {
|
|
15
|
+
let total = 0;
|
|
16
|
+
for (const funnel of spec.funnels) {
|
|
17
|
+
total += funnel.pages?.length ?? 0;
|
|
18
|
+
}
|
|
19
|
+
if (total > 0)
|
|
20
|
+
return [];
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
ruleId: 'PageCount',
|
|
24
|
+
severity: 'error',
|
|
25
|
+
message: 'Funnel must have at least 1 page.',
|
|
26
|
+
path: '/funnels',
|
|
27
|
+
data: { totalPages: 0 },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageIdUniqueness — page IDs must be globally unique across all funnels
|
|
3
|
+
* because routing targets (next_page, success_url, on_accept, on_decline)
|
|
4
|
+
* reference pages by id alone with no funnel scope.
|
|
5
|
+
*
|
|
6
|
+
* Reports each duplicated id exactly once, at the path of the SECOND
|
|
7
|
+
* occurrence (the offender, not the original). Pages without an id are
|
|
8
|
+
* silently skipped — that's a different concern (page identity, not yet
|
|
9
|
+
* extracted as its own rule).
|
|
10
|
+
*
|
|
11
|
+
* Message text inherited verbatim from the pre-#110 validator at migration time.
|
|
12
|
+
*/
|
|
13
|
+
import type { Rule } from '../types.ts';
|
|
14
|
+
export declare const PageIdUniqueness: Rule;
|