@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.
Files changed (310) hide show
  1. package/AGENTS.md +204 -0
  2. package/CHANGELOG.md +5002 -0
  3. package/CONTEXT.md +685 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +4 -0
  6. package/README.md +368 -0
  7. package/agents/claude/CLAUDE.md +32 -0
  8. package/agents/codex/AGENTS.md +27 -0
  9. package/agents/copilot/copilot-instructions.md +14 -0
  10. package/agents/cursor/campaigns-os.mdc +13 -0
  11. package/bin/campaigns-os.mjs +38 -0
  12. package/campaign-spec/README.md +138 -0
  13. package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
  14. package/campaign-spec/dist/analytics-vocabulary.js +74 -0
  15. package/campaign-spec/dist/index.d.ts +40 -0
  16. package/campaign-spec/dist/index.js +77 -0
  17. package/campaign-spec/dist/normalize.d.ts +22 -0
  18. package/campaign-spec/dist/normalize.js +41 -0
  19. package/campaign-spec/dist/routing.d.ts +190 -0
  20. package/campaign-spec/dist/routing.js +263 -0
  21. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
  22. package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
  23. package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
  24. package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
  25. package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
  26. package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
  27. package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
  28. package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
  29. package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
  30. package/campaign-spec/dist/rules/cycle-detection.js +141 -0
  31. package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
  32. package/campaign-spec/dist/rules/design-source-shape.js +142 -0
  33. package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
  34. package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
  35. package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
  36. package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
  37. package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
  38. package/campaign-spec/dist/rules/funnel-count.js +37 -0
  39. package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
  40. package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
  41. package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
  42. package/campaign-spec/dist/rules/funnel-identity.js +57 -0
  43. package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
  44. package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
  45. package/campaign-spec/dist/rules/index.d.ts +69 -0
  46. package/campaign-spec/dist/rules/index.js +131 -0
  47. package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
  48. package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
  49. package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
  50. package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
  51. package/campaign-spec/dist/rules/page-count.d.ts +11 -0
  52. package/campaign-spec/dist/rules/page-count.js +31 -0
  53. package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
  54. package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
  55. package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
  56. package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
  57. package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
  58. package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
  59. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
  60. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
  61. package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
  62. package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
  63. package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
  64. package/campaign-spec/dist/rules/schema-version.js +53 -0
  65. package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
  66. package/campaign-spec/dist/rules/sdk-version.js +96 -0
  67. package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
  68. package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
  69. package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
  70. package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
  71. package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
  72. package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
  73. package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
  74. package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
  75. package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
  76. package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
  77. package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
  78. package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
  79. package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
  80. package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
  81. package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
  82. package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
  83. package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
  84. package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
  85. package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
  86. package/campaign-spec/dist/sdk-version-parse.js +62 -0
  87. package/campaign-spec/dist/types.d.ts +674 -0
  88. package/campaign-spec/dist/types.js +40 -0
  89. package/campaign-spec/package.json +12 -0
  90. package/compatibility.json +25 -0
  91. package/contracts/agent-relevant-change-policy.v1.json +111 -0
  92. package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
  93. package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
  94. package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
  95. package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
  96. package/contracts/commerce-surface-catalog.json +2452 -0
  97. package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
  98. package/contracts/fixtures/orientation/envelope/current.json +95 -0
  99. package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
  100. package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
  101. package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
  102. package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
  103. package/contracts/fixtures/orientation/envelope/refused.json +100 -0
  104. package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
  105. package/contracts/fixtures/orientation/envelope/updated.json +135 -0
  106. package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
  107. package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
  108. package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
  109. package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
  110. package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
  111. package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
  112. package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
  113. package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
  114. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
  115. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
  116. package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
  117. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
  118. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
  119. package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
  120. package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
  121. package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
  122. package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
  123. package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
  124. package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
  125. package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
  126. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
  127. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
  128. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
  129. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
  130. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
  131. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
  132. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
  133. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
  134. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
  135. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
  136. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
  137. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
  138. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
  139. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
  140. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
  141. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
  142. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
  143. package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
  144. package/contracts/migration-sidecar-bundle.v0.json +149 -0
  145. package/contracts/orientation-limits.v1.json +41 -0
  146. package/contracts/orientation-reason-codes.v1.json +196 -0
  147. package/contracts/private-template-sources.json +8 -0
  148. package/contracts/release-ledger.json +4598 -0
  149. package/contracts/reserved-skill-names.json +13 -0
  150. package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
  151. package/contracts/supported-surface.json +180 -0
  152. package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
  153. package/contracts/template-brand-contract.apollo.v0.json +27 -0
  154. package/contracts/template-brand-contract.demeter.v0.json +27 -0
  155. package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
  156. package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
  157. package/contracts/template-brand-contract.olympus.v0.json +27 -0
  158. package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
  159. package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
  160. package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
  161. package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
  162. package/contracts/template-slot-manifest.apollo.v0.json +12 -0
  163. package/contracts/template-slot-manifest.demeter.v0.json +30 -0
  164. package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
  165. package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
  166. package/contracts/template-slot-manifest.olympus.v0.json +12 -0
  167. package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
  168. package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
  169. package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
  170. package/docs/brand-theme-bridge.md +159 -0
  171. package/docs/build-packet.md +1300 -0
  172. package/docs/campaign-build-brief.md +145 -0
  173. package/docs/campaign-standardization-report.md +329 -0
  174. package/docs/campaigns-os-build-flow.md +117 -0
  175. package/docs/design-source-package.md +784 -0
  176. package/docs/legacy-migration.md +58 -0
  177. package/docs/migration-sidecar-bundle.md +139 -0
  178. package/docs/orientation-contract-reference.md +1220 -0
  179. package/docs/polish-evidence.md +502 -0
  180. package/docs/qa-and-test-orders.md +1691 -0
  181. package/docs/release-ledger-authoring-guide.md +274 -0
  182. package/docs/runtime-readiness.md +211 -0
  183. package/docs/supported-surface.md +83 -0
  184. package/docs/versioning.md +55 -0
  185. package/docs/workflow-findings-sidecar.md +588 -0
  186. package/package.json +135 -0
  187. package/prompts/first-build.md +27 -0
  188. package/prompts/friction-log.md +30 -0
  189. package/schemas/campaign-build-brief.v1.schema.json +149 -0
  190. package/schemas/campaign-design-source-package.v0.schema.json +697 -0
  191. package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
  192. package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
  193. package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
  194. package/schemas/campaign-spec.v4.schema.json +582 -0
  195. package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
  196. package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
  197. package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
  198. package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
  199. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
  200. package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
  201. package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
  202. package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
  203. package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
  204. package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
  205. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
  206. package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
  207. package/schemas/source-html-manifest.v0.schema.json +252 -0
  208. package/skills/next-campaigns-build/SKILL.md +73 -0
  209. package/skills/next-campaigns-os/SKILL.md +101 -0
  210. package/skills/next-campaigns-os/references/session-intake.md +160 -0
  211. package/skills/next-campaigns-os-setup/SKILL.md +22 -0
  212. package/skills/next-campaigns-polish/SKILL.md +145 -0
  213. package/skills/next-campaigns-qa/SKILL.md +92 -0
  214. package/skills.json +56 -0
  215. package/skills.sh +64 -0
  216. package/src/adapter-decision-contract.mjs +333 -0
  217. package/src/brand-theme.mjs +1151 -0
  218. package/src/browser-launch.mjs +79 -0
  219. package/src/build-brief.mjs +781 -0
  220. package/src/built-site-scope.mjs +312 -0
  221. package/src/campaign-ecosystem.mjs +734 -0
  222. package/src/campaign-identity.mjs +405 -0
  223. package/src/campaign-workspace.mjs +127 -0
  224. package/src/checkpoint-waiver.mjs +302 -0
  225. package/src/cli.mjs +13442 -0
  226. package/src/commercial-journey.mjs +1119 -0
  227. package/src/commercial-parity.mjs +965 -0
  228. package/src/consent.mjs +347 -0
  229. package/src/content-residue.mjs +322 -0
  230. package/src/deadline.mjs +81 -0
  231. package/src/design-source-package.mjs +2604 -0
  232. package/src/deviation.mjs +107 -0
  233. package/src/doctor-check-registry.mjs +49 -0
  234. package/src/doctor-sidecar.mjs +106 -0
  235. package/src/finding-cause.mjs +557 -0
  236. package/src/findings.mjs +326 -0
  237. package/src/fs-identity.mjs +68 -0
  238. package/src/gate-actions.mjs +105 -0
  239. package/src/html-scan.mjs +53 -0
  240. package/src/install-mode.mjs +272 -0
  241. package/src/legacy-migration.d.ts +128 -0
  242. package/src/legacy-migration.mjs +510 -0
  243. package/src/lifecycle.mjs +338 -0
  244. package/src/local-proof.mjs +401 -0
  245. package/src/map-pin-writeback.mjs +210 -0
  246. package/src/orchestration-stage-contract.mjs +81 -0
  247. package/src/package-install-fixture.mjs +33 -0
  248. package/src/page-kit-build-summary.mjs +175 -0
  249. package/src/page-kit-campaign-config.mjs +57 -0
  250. package/src/page-kit-sdk-version.mjs +392 -0
  251. package/src/page-kit-store-profile.mjs +369 -0
  252. package/src/page-kit-sync.mjs +162 -0
  253. package/src/polish-browser.mjs +867 -0
  254. package/src/polish-capture.mjs +1094 -0
  255. package/src/polish-deadline.mjs +51 -0
  256. package/src/polish-gate.mjs +739 -0
  257. package/src/polish-node.mjs +639 -0
  258. package/src/polish-page-load.mjs +1100 -0
  259. package/src/private-template-source.mjs +237 -0
  260. package/src/proof-policy.mjs +82 -0
  261. package/src/qa-analytics-correctness.mjs +307 -0
  262. package/src/qa-analytics-errors.mjs +38 -0
  263. package/src/qa-analytics-parity.mjs +699 -0
  264. package/src/qa-binding-evidence.mjs +140 -0
  265. package/src/qa-browser.mjs +6608 -0
  266. package/src/qa-cart-entry.mjs +406 -0
  267. package/src/qa-commercial-parity.mjs +641 -0
  268. package/src/qa-node.mjs +3620 -0
  269. package/src/qa-order-bump.mjs +381 -0
  270. package/src/qa-parity-capture.mjs +428 -0
  271. package/src/qa-parity-fixture.mjs +359 -0
  272. package/src/qa-publish.mjs +362 -0
  273. package/src/qa-purchase-data-layer.mjs +263 -0
  274. package/src/qa-route-probe.mjs +272 -0
  275. package/src/qa-sidecar.mjs +188 -0
  276. package/src/qa-test-order-topology.mjs +207 -0
  277. package/src/qa-url-privacy.mjs +13 -0
  278. package/src/qa-verdict-discovery.mjs +192 -0
  279. package/src/qa-verdict-publish.mjs +105 -0
  280. package/src/qa-verdict.mjs +287 -0
  281. package/src/remit.mjs +388 -0
  282. package/src/repo-scan.mjs +83 -0
  283. package/src/route-identity.mjs +133 -0
  284. package/src/run-record-closeout.mjs +229 -0
  285. package/src/run-record.mjs +839 -0
  286. package/src/run-session.mjs +226 -0
  287. package/src/runtime-state-ignore.mjs +113 -0
  288. package/src/sdk-attribute-index.mjs +212 -0
  289. package/src/sdk-markup.mjs +358 -0
  290. package/src/sdk-meta-tags.mjs +52 -0
  291. package/src/shell-token.mjs +7 -0
  292. package/src/sidecar-bundle.mjs +399 -0
  293. package/src/source-asset-crawl.mjs +469 -0
  294. package/src/source-html-intake.mjs +627 -0
  295. package/src/source-html-manifest.mjs +276 -0
  296. package/src/source-prep.mjs +284 -0
  297. package/src/spec-derive-store.mjs +431 -0
  298. package/src/spec-derive.mjs +514 -0
  299. package/src/spec-fetch.mjs +66 -0
  300. package/src/spec-hash.mjs +48 -0
  301. package/src/spec-identity.mjs +27 -0
  302. package/src/stage-ledger.mjs +509 -0
  303. package/src/standardization-report.mjs +1297 -0
  304. package/src/template-brand-contract.mjs +473 -0
  305. package/src/template-freshness.mjs +196 -0
  306. package/src/template-reference.mjs +81 -0
  307. package/src/template-slot-manifest.mjs +150 -0
  308. package/src/text-safety.mjs +62 -0
  309. package/src/theme-gate.mjs +185 -0
  310. 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;