@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,21 @@
1
+ /**
2
+ * SchemaVersion — requires spec.schema_version and validates it against the
3
+ * supported lineage matrix.
4
+ *
5
+ * The missing-value message text is inherited verbatim from the pre-#110
6
+ * validator at migration time so the strangler swap was bit-for-bit invisible
7
+ * to callers; the wording is now canonical. Membership validation against
8
+ * SUPPORTED_SCHEMA_VERSIONS is the strict layer added on top: an unsupported
9
+ * lineage errors here at authoring/export time instead of surfacing as
10
+ * downstream shape drift.
11
+ *
12
+ * SUPPORTED_SCHEMA_VERSIONS must stay identical to the schema_version enum in
13
+ * schemas/campaign-spec.v4.schema.json (and compatibility.json's campaign_spec
14
+ * range). The rule stays pure (no filesystem), so the constant duplicates the
15
+ * schema enum — the sync test in ../test/rules/schema-version.test.ts is the
16
+ * drift gate, following the UnknownTopLevelFields pattern.
17
+ */
18
+ import type { Rule } from '../types.ts';
19
+ /** Supported CampaignSpec lineage versions — keep in sync with the v4 JSON Schema enum. */
20
+ export declare const SUPPORTED_SCHEMA_VERSIONS: readonly string[];
21
+ export declare const SchemaVersion: Rule;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * SchemaVersion — requires spec.schema_version and validates it against the
3
+ * supported lineage matrix.
4
+ *
5
+ * The missing-value message text is inherited verbatim from the pre-#110
6
+ * validator at migration time so the strangler swap was bit-for-bit invisible
7
+ * to callers; the wording is now canonical. Membership validation against
8
+ * SUPPORTED_SCHEMA_VERSIONS is the strict layer added on top: an unsupported
9
+ * lineage errors here at authoring/export time instead of surfacing as
10
+ * downstream shape drift.
11
+ *
12
+ * SUPPORTED_SCHEMA_VERSIONS must stay identical to the schema_version enum in
13
+ * schemas/campaign-spec.v4.schema.json (and compatibility.json's campaign_spec
14
+ * range). The rule stays pure (no filesystem), so the constant duplicates the
15
+ * schema enum — the sync test in ../test/rules/schema-version.test.ts is the
16
+ * drift gate, following the UnknownTopLevelFields pattern.
17
+ */
18
+ /** Supported CampaignSpec lineage versions — keep in sync with the v4 JSON Schema enum. */
19
+ export const SUPPORTED_SCHEMA_VERSIONS = Object.freeze(['4.2', '4.3']);
20
+ export const SchemaVersion = {
21
+ id: 'SchemaVersion',
22
+ severity: 'error',
23
+ tags: ['fast', 'spec-only'],
24
+ check(spec) {
25
+ if (!spec.schema_version) {
26
+ return [
27
+ {
28
+ ruleId: 'SchemaVersion',
29
+ severity: 'error',
30
+ message: 'Missing schema_version',
31
+ path: '/schema_version',
32
+ },
33
+ ];
34
+ }
35
+ if (!SUPPORTED_SCHEMA_VERSIONS.includes(spec.schema_version)) {
36
+ return [
37
+ {
38
+ ruleId: 'SchemaVersion',
39
+ severity: 'error',
40
+ message: `Unsupported schema_version ${JSON.stringify(spec.schema_version)}. ` +
41
+ `Supported versions: ${SUPPORTED_SCHEMA_VERSIONS.join(', ')}.`,
42
+ path: '/schema_version',
43
+ data: {
44
+ value: spec.schema_version,
45
+ supported: [...SUPPORTED_SCHEMA_VERSIONS],
46
+ check: 'schema-version-supported',
47
+ },
48
+ },
49
+ ];
50
+ }
51
+ return [];
52
+ },
53
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * SdkVersion — requires a strict released SDK version on the spec for export.
3
+ *
4
+ * Two acceptable locations: spec.global_config.sdk_version (CANONICAL —
5
+ * 33/33 real Map Builder exports declare it there) or
6
+ * spec.runtime.sdk_version (accepted alias, seen only in local drafts and
7
+ * always redundant there). Either being present satisfies the presence
8
+ * requirement; the missing-everywhere violation path points at the canonical
9
+ * home.
10
+ *
11
+ * Strictness mirrors the downstream Page Kit SDK-version checkpoint
12
+ * (src/page-kit-sdk-version.mjs) via the shared parser in
13
+ * ../sdk-version-parse.ts, so external authors get the same rejection at
14
+ * authoring/export time that the checkpoint would deliver after Build Packet
15
+ * preparation:
16
+ * - missing everywhere → error (message unchanged — the "SDK
17
+ * version is required" substring is matched by caller tests)
18
+ * - non-string / empty / padded / prerelease / non-canonical → error at the
19
+ * location carrying the bad value (one violation per bad location)
20
+ * - conflicting dual declarations → error (escalated from the former
21
+ * warning; the checkpoint already treats conflicts as non-waivable, and
22
+ * every real dual-declaration spec in the corpus agrees)
23
+ * - both present and equal → passes (real drafts do this)
24
+ */
25
+ import type { Rule } from '../types.ts';
26
+ export declare const SdkVersion: Rule;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * SdkVersion — requires a strict released SDK version on the spec for export.
3
+ *
4
+ * Two acceptable locations: spec.global_config.sdk_version (CANONICAL —
5
+ * 33/33 real Map Builder exports declare it there) or
6
+ * spec.runtime.sdk_version (accepted alias, seen only in local drafts and
7
+ * always redundant there). Either being present satisfies the presence
8
+ * requirement; the missing-everywhere violation path points at the canonical
9
+ * home.
10
+ *
11
+ * Strictness mirrors the downstream Page Kit SDK-version checkpoint
12
+ * (src/page-kit-sdk-version.mjs) via the shared parser in
13
+ * ../sdk-version-parse.ts, so external authors get the same rejection at
14
+ * authoring/export time that the checkpoint would deliver after Build Packet
15
+ * preparation:
16
+ * - missing everywhere → error (message unchanged — the "SDK
17
+ * version is required" substring is matched by caller tests)
18
+ * - non-string / empty / padded / prerelease / non-canonical → error at the
19
+ * location carrying the bad value (one violation per bad location)
20
+ * - conflicting dual declarations → error (escalated from the former
21
+ * warning; the checkpoint already treats conflicts as non-waivable, and
22
+ * every real dual-declaration spec in the corpus agrees)
23
+ * - both present and equal → passes (real drafts do this)
24
+ */
25
+ import { describeSdkVersionRejection, parseSdkVersion } from "../sdk-version-parse.js";
26
+ function displayValue(value) {
27
+ if (typeof value === 'string')
28
+ return JSON.stringify(value);
29
+ try {
30
+ return String(JSON.stringify(value));
31
+ }
32
+ catch {
33
+ return String(value);
34
+ }
35
+ }
36
+ export const SdkVersion = {
37
+ id: 'SdkVersion',
38
+ severity: 'error',
39
+ tags: ['fast', 'spec-only'],
40
+ check(spec) {
41
+ const globalConfig = spec.global_config;
42
+ const runtime = spec.runtime;
43
+ const hasCanonical = globalConfig != null && Object.hasOwn(globalConfig, 'sdk_version');
44
+ const hasAlias = runtime != null && Object.hasOwn(runtime, 'sdk_version');
45
+ if (!hasCanonical && !hasAlias) {
46
+ return [
47
+ {
48
+ ruleId: 'SdkVersion',
49
+ severity: 'error',
50
+ message: 'SDK version is required for spec export.',
51
+ path: '/global_config/sdk_version',
52
+ },
53
+ ];
54
+ }
55
+ const violations = [];
56
+ const declarations = [
57
+ ...(hasCanonical
58
+ ? [{ location: 'global_config.sdk_version', path: '/global_config/sdk_version', value: globalConfig.sdk_version }]
59
+ : []),
60
+ ...(hasAlias
61
+ ? [{ location: 'runtime.sdk_version', path: '/runtime/sdk_version', value: runtime.sdk_version }]
62
+ : []),
63
+ ];
64
+ for (const declaration of declarations) {
65
+ const parsed = parseSdkVersion(declaration.value);
66
+ if (parsed.ok)
67
+ continue;
68
+ violations.push({
69
+ ruleId: 'SdkVersion',
70
+ severity: 'error',
71
+ message: `${declaration.location} must be a released semantic version in canonical ` +
72
+ `MAJOR.MINOR.PATCH form; got ${displayValue(declaration.value)} ` +
73
+ `(${describeSdkVersionRejection(parsed.reason)}).`,
74
+ path: declaration.path,
75
+ data: { value: declaration.value, reason: parsed.reason, check: 'sdk-version-strict' },
76
+ });
77
+ }
78
+ if (violations.length > 0)
79
+ return violations;
80
+ const canonical = globalConfig?.sdk_version;
81
+ const alias = runtime?.sdk_version;
82
+ if (hasCanonical && hasAlias && canonical !== alias) {
83
+ return [
84
+ {
85
+ ruleId: 'SdkVersion',
86
+ severity: 'error',
87
+ message: `Conflicting SDK version declarations: global_config.sdk_version=${canonical} vs ` +
88
+ `runtime.sdk_version=${alias}. global_config is canonical — align or remove the runtime alias.`,
89
+ path: '/runtime/sdk_version',
90
+ data: { global_config: canonical, runtime: alias, check: 'sdk-version-conflict' },
91
+ },
92
+ ];
93
+ }
94
+ return [];
95
+ },
96
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ShippingCountriesShape — campaign.available_shipping_countries must be
3
+ * either the literal string "all", an array of country codes, or absent.
4
+ * Anything else (a single string, an object, a number) is malformed.
5
+ *
6
+ * Warning severity. Message text inherited verbatim from the pre-#110
7
+ * validator at migration time.
8
+ */
9
+ import type { Rule } from '../types.ts';
10
+ export declare const ShippingCountriesShape: Rule;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ShippingCountriesShape — campaign.available_shipping_countries must be
3
+ * either the literal string "all", an array of country codes, or absent.
4
+ * Anything else (a single string, an object, a number) is malformed.
5
+ *
6
+ * Warning severity. Message text inherited verbatim from the pre-#110
7
+ * validator at migration time.
8
+ */
9
+ function isValidShape(countries) {
10
+ return countries == null || countries === 'all' || Array.isArray(countries);
11
+ }
12
+ export const ShippingCountriesShape = {
13
+ id: 'ShippingCountriesShape',
14
+ severity: 'warning',
15
+ tags: ['fast', 'spec-only'],
16
+ check(spec) {
17
+ const countries = spec.campaign?.available_shipping_countries;
18
+ if (isValidShape(countries))
19
+ return [];
20
+ return [
21
+ {
22
+ ruleId: 'ShippingCountriesShape',
23
+ severity: 'warning',
24
+ message: 'Shipping countries must be "all" or an array of country codes.',
25
+ path: '/campaign/available_shipping_countries',
26
+ data: { received: countries },
27
+ },
28
+ ];
29
+ },
30
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ShippingMethodsPresent — warn when the spec declares no shipping methods.
3
+ * Missing/undefined or empty array both fire; matches legacy behaviour.
4
+ *
5
+ * Warning severity (not error) — the campaign can still ship technically,
6
+ * the warning surfaces that the operator hasn't chosen methods yet.
7
+ * Message text inherited verbatim from the pre-#110 validator at migration time.
8
+ */
9
+ import type { Rule } from '../types.ts';
10
+ export declare const ShippingMethodsPresent: Rule;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ShippingMethodsPresent — warn when the spec declares no shipping methods.
3
+ * Missing/undefined or empty array both fire; matches legacy behaviour.
4
+ *
5
+ * Warning severity (not error) — the campaign can still ship technically,
6
+ * the warning surfaces that the operator hasn't chosen methods yet.
7
+ * Message text inherited verbatim from the pre-#110 validator at migration time.
8
+ */
9
+ export const ShippingMethodsPresent = {
10
+ id: 'ShippingMethodsPresent',
11
+ severity: 'warning',
12
+ tags: ['fast', 'spec-only'],
13
+ check(spec) {
14
+ const methods = spec.shipping_methods ?? [];
15
+ if (methods.length > 0)
16
+ return [];
17
+ return [
18
+ {
19
+ ruleId: 'ShippingMethodsPresent',
20
+ severity: 'warning',
21
+ message: 'No shipping methods selected.',
22
+ path: '/shipping_methods',
23
+ },
24
+ ];
25
+ },
26
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * StoreProfileShape — light validation for two campaign-level store
3
+ * profile fields introduced in Slice 4f.
4
+ *
5
+ * 1. campaign.store_phone_tel must be a `tel:`-prefixed URI containing
6
+ * a digit-shaped number when present. The build wires this value
7
+ * into `<a href="tel:...">` attributes; a value without the scheme
8
+ * renders a broken link, and a value containing HTML metacharacters
9
+ * or alternative schemes (javascript:, data:) is rejected as a
10
+ * defense-in-depth measure for downstream templates that may skip
11
+ * href escaping. Accepted shape: `tel:` then optional `+`, then a
12
+ * sequence of at least 4 chars drawn from digits, spaces, dashes,
13
+ * parens, and dots.
14
+ *
15
+ * 2. campaign.allowed_domains shape check: when present, must be a
16
+ * non-empty array of hostname strings. The Campaigns API treats
17
+ * domain allowlisting as the access boundary for public-by-design
18
+ * keys; the spec carries the allowlist so the build packet can bind
19
+ * config.js to the right surface.
20
+ *
21
+ * Scope note: this rule fires only when allowed_domains is
22
+ * explicitly set. Absence is silent — a missing allowlist is
23
+ * common in pre-launch / draft specs and belongs to a separate
24
+ * launch-readiness check, not a shape rule. The launch gate
25
+ * can be added later without changing this rule's contract.
26
+ *
27
+ * Both checks are warning severity. Never blocks a build.
28
+ */
29
+ import type { Rule } from '../types.ts';
30
+ export declare const StoreProfileShape: Rule;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * StoreProfileShape — light validation for two campaign-level store
3
+ * profile fields introduced in Slice 4f.
4
+ *
5
+ * 1. campaign.store_phone_tel must be a `tel:`-prefixed URI containing
6
+ * a digit-shaped number when present. The build wires this value
7
+ * into `<a href="tel:...">` attributes; a value without the scheme
8
+ * renders a broken link, and a value containing HTML metacharacters
9
+ * or alternative schemes (javascript:, data:) is rejected as a
10
+ * defense-in-depth measure for downstream templates that may skip
11
+ * href escaping. Accepted shape: `tel:` then optional `+`, then a
12
+ * sequence of at least 4 chars drawn from digits, spaces, dashes,
13
+ * parens, and dots.
14
+ *
15
+ * 2. campaign.allowed_domains shape check: when present, must be a
16
+ * non-empty array of hostname strings. The Campaigns API treats
17
+ * domain allowlisting as the access boundary for public-by-design
18
+ * keys; the spec carries the allowlist so the build packet can bind
19
+ * config.js to the right surface.
20
+ *
21
+ * Scope note: this rule fires only when allowed_domains is
22
+ * explicitly set. Absence is silent — a missing allowlist is
23
+ * common in pre-launch / draft specs and belongs to a separate
24
+ * launch-readiness check, not a shape rule. The launch gate
25
+ * can be added later without changing this rule's contract.
26
+ *
27
+ * Both checks are warning severity. Never blocks a build.
28
+ */
29
+ function isNonEmptyString(value) {
30
+ return typeof value === 'string' && value.trim().length > 0;
31
+ }
32
+ // `tel:` followed by optional `+`, then 4+ chars of digits / space / dash /
33
+ // parens / dot. Rejects HTML metachars, javascript:/data: schemes, control
34
+ // chars, and obvious garbage. Liberal enough for international formats
35
+ // (e.g. "tel:+44 (0)20 7946 0958"); strict enough to keep a downstream
36
+ // `<a href>` safe even if a template skips escaping.
37
+ const TEL_URI_REGEX = /^tel:\+?[0-9 .()\-]{4,}$/i;
38
+ export const StoreProfileShape = {
39
+ id: 'StoreProfileShape',
40
+ severity: 'warning',
41
+ tags: ['fast', 'spec-only'],
42
+ check(spec) {
43
+ const violations = [];
44
+ const campaign = spec.campaign;
45
+ // 1. tel: prefix on store_phone_tel.
46
+ if (campaign?.store_phone_tel !== undefined) {
47
+ const value = campaign.store_phone_tel;
48
+ if (typeof value !== 'string') {
49
+ violations.push({
50
+ ruleId: 'StoreProfileShape',
51
+ severity: 'warning',
52
+ message: `campaign.store_phone_tel must be a string (got ${value === null ? 'null' : typeof value}).`,
53
+ path: '/campaign/store_phone_tel',
54
+ data: { check: 'store-phone-tel-bad-type' },
55
+ });
56
+ }
57
+ else if (!isNonEmptyString(value)) {
58
+ violations.push({
59
+ ruleId: 'StoreProfileShape',
60
+ severity: 'warning',
61
+ message: 'campaign.store_phone_tel is set but empty; remove the field or set a tel:-prefixed value (e.g. "tel:+18005551234").',
62
+ path: '/campaign/store_phone_tel',
63
+ data: { check: 'store-phone-tel-empty' },
64
+ });
65
+ }
66
+ else {
67
+ const trimmed = value.trim();
68
+ if (!trimmed.toLowerCase().startsWith('tel:')) {
69
+ violations.push({
70
+ ruleId: 'StoreProfileShape',
71
+ severity: 'warning',
72
+ message: `campaign.store_phone_tel "${value}" must be a tel: URI (e.g. "tel:+18005551234"); without the scheme it renders a broken link in <a href> attributes.`,
73
+ path: '/campaign/store_phone_tel',
74
+ data: { check: 'store-phone-tel-missing-scheme', value },
75
+ });
76
+ }
77
+ else if (!TEL_URI_REGEX.test(trimmed)) {
78
+ violations.push({
79
+ ruleId: 'StoreProfileShape',
80
+ severity: 'warning',
81
+ message: `campaign.store_phone_tel "${value}" is not a recognized tel: number. Use digits, spaces, dashes, parens, and dots only (e.g. "tel:+18005551234" or "tel:+44 (0)20 7946 0958"); HTML metacharacters and alternative schemes (javascript:, data:) are rejected as a defense-in-depth measure.`,
82
+ path: '/campaign/store_phone_tel',
83
+ data: { check: 'store-phone-tel-bad-content', value },
84
+ });
85
+ }
86
+ }
87
+ }
88
+ // 2. allowed_domains shape (when present — absence is silent; see header
89
+ // note about scope).
90
+ const domains = campaign?.allowed_domains;
91
+ if (domains === undefined || domains === null) {
92
+ // Intentionally silent — launch-readiness check lives elsewhere.
93
+ }
94
+ else if (!Array.isArray(domains)) {
95
+ violations.push({
96
+ ruleId: 'StoreProfileShape',
97
+ severity: 'warning',
98
+ message: `campaign.allowed_domains must be an array of hostnames; got ${typeof domains}.`,
99
+ path: '/campaign/allowed_domains',
100
+ data: { check: 'allowed-domains-bad-shape', value: domains },
101
+ });
102
+ }
103
+ else if (domains.length === 0) {
104
+ violations.push({
105
+ ruleId: 'StoreProfileShape',
106
+ severity: 'warning',
107
+ message: 'campaign.allowed_domains is empty. Add at least one production hostname (e.g. "store.example.com") before launch so the SDK key has a valid origin.',
108
+ path: '/campaign/allowed_domains',
109
+ data: { check: 'allowed-domains-empty' },
110
+ });
111
+ }
112
+ else {
113
+ domains.forEach((domain, idx) => {
114
+ if (!isNonEmptyString(domain)) {
115
+ violations.push({
116
+ ruleId: 'StoreProfileShape',
117
+ severity: 'warning',
118
+ message: `campaign.allowed_domains[${idx}] must be a non-empty hostname string; got ${JSON.stringify(domain)}.`,
119
+ path: `/campaign/allowed_domains/${idx}`,
120
+ data: { check: 'allowed-domain-bad-entry', index: idx, value: domain },
121
+ });
122
+ }
123
+ });
124
+ }
125
+ return violations;
126
+ },
127
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ThankYouRequirement — every spec must include at least one thank-you page
3
+ * (terminal node of the funnel topology). Downgrades to a warning when the
4
+ * spec is a partial build (build_scope.mode === 'partial'), because partial
5
+ * builds may legitimately end on a non-terminal page that routes into an
6
+ * existing downstream campaign.
7
+ *
8
+ * Only fires when the spec has at least one page — if there are zero pages
9
+ * altogether, PageCount handles that signal and this rule stays silent to
10
+ * avoid double-reporting.
11
+ *
12
+ * Message text and the partial-scope severity downgrade are inherited
13
+ * verbatim from the pre-#110 validator at migration time.
14
+ */
15
+ import type { Rule } from '../types.ts';
16
+ export declare const ThankYouRequirement: Rule;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ThankYouRequirement — every spec must include at least one thank-you page
3
+ * (terminal node of the funnel topology). Downgrades to a warning when the
4
+ * spec is a partial build (build_scope.mode === 'partial'), because partial
5
+ * builds may legitimately end on a non-terminal page that routes into an
6
+ * existing downstream campaign.
7
+ *
8
+ * Only fires when the spec has at least one page — if there are zero pages
9
+ * altogether, PageCount handles that signal and this rule stays silent to
10
+ * avoid double-reporting.
11
+ *
12
+ * Message text and the partial-scope severity downgrade are inherited
13
+ * verbatim from the pre-#110 validator at migration time.
14
+ */
15
+ export const ThankYouRequirement = {
16
+ id: 'ThankYouRequirement',
17
+ severity: 'error',
18
+ tags: ['fast', 'structure', 'spec-only'],
19
+ check(spec) {
20
+ let hasThankYou = false;
21
+ let totalPages = 0;
22
+ for (const funnel of spec.funnels) {
23
+ const pages = funnel.pages ?? [];
24
+ totalPages += pages.length;
25
+ for (const page of pages) {
26
+ if (page.type === 'thankyou') {
27
+ hasThankYou = true;
28
+ break;
29
+ }
30
+ }
31
+ if (hasThankYou)
32
+ break;
33
+ }
34
+ // Silent when zero pages — PageCount already speaks to that.
35
+ if (hasThankYou || totalPages === 0)
36
+ return [];
37
+ const isPartialScope = spec.build_scope?.mode === 'partial';
38
+ return [
39
+ {
40
+ ruleId: 'ThankYouRequirement',
41
+ severity: isPartialScope ? 'warning' : 'error',
42
+ message: isPartialScope
43
+ ? 'Spec has no Thank You page. Valid for partial builds when traffic continues to an existing downstream route.'
44
+ : 'Funnel must end with a Thank You page.',
45
+ path: '/funnels',
46
+ data: { isPartialScope },
47
+ },
48
+ ];
49
+ },
50
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * UnknownTopLevelFields — reports top-level spec keys and campaign.* keys
3
+ * that are not part of the known CampaignSpec v4 surface.
4
+ *
5
+ * Warning severity: unknown fields are legal (the schema is deliberately
6
+ * permissive — schemas/campaign-spec.v4.schema.json keeps
7
+ * additionalProperties open), but naming them surfaces dialect drift early
8
+ * instead of letting a misspelled or legacy key ride along silently.
9
+ * Rejection-at-export belongs to validation profiles in a later change.
10
+ *
11
+ * Skipped on purpose:
12
+ * - keys starting with "_" (builder/ops annotations: _provenance,
13
+ * _accept_label, ...);
14
+ * - keys ending with "_note" (real exports use *_note keys as inline
15
+ * annotations);
16
+ * - sdk_hints interiors (template-handoff vocabulary lives there; this
17
+ * rule only reads the top level and campaign.*).
18
+ *
19
+ * The known sets below MIRROR schemas/campaign-spec.v4.schema.json
20
+ * (top-level `properties` and `$defs.campaign.properties`). Rules stay pure
21
+ * and filesystem-free, so the sets are duplicated here; the sync test
22
+ * (test/rules/unknown-top-level-fields.test.ts) asserts they match the
23
+ * schema exactly — update both together.
24
+ */
25
+ import type { Rule } from '../types.ts';
26
+ export declare const KNOWN_TOP_LEVEL_FIELDS: readonly string[];
27
+ export declare const KNOWN_CAMPAIGN_FIELDS: readonly string[];
28
+ export declare const UnknownTopLevelFields: Rule;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * UnknownTopLevelFields — reports top-level spec keys and campaign.* keys
3
+ * that are not part of the known CampaignSpec v4 surface.
4
+ *
5
+ * Warning severity: unknown fields are legal (the schema is deliberately
6
+ * permissive — schemas/campaign-spec.v4.schema.json keeps
7
+ * additionalProperties open), but naming them surfaces dialect drift early
8
+ * instead of letting a misspelled or legacy key ride along silently.
9
+ * Rejection-at-export belongs to validation profiles in a later change.
10
+ *
11
+ * Skipped on purpose:
12
+ * - keys starting with "_" (builder/ops annotations: _provenance,
13
+ * _accept_label, ...);
14
+ * - keys ending with "_note" (real exports use *_note keys as inline
15
+ * annotations);
16
+ * - sdk_hints interiors (template-handoff vocabulary lives there; this
17
+ * rule only reads the top level and campaign.*).
18
+ *
19
+ * The known sets below MIRROR schemas/campaign-spec.v4.schema.json
20
+ * (top-level `properties` and `$defs.campaign.properties`). Rules stay pure
21
+ * and filesystem-free, so the sets are duplicated here; the sync test
22
+ * (test/rules/unknown-top-level-fields.test.ts) asserts they match the
23
+ * schema exactly — update both together.
24
+ */
25
+ export const KNOWN_TOP_LEVEL_FIELDS = [
26
+ 'schema_version',
27
+ 'builder_version',
28
+ 'generated_at',
29
+ 'campaign',
30
+ 'global_config',
31
+ 'runtime',
32
+ 'funnels',
33
+ 'funnel_pages',
34
+ 'shipping_methods',
35
+ 'offers',
36
+ 'spec_identity',
37
+ 'build_scope',
38
+ 'analytics',
39
+ 'slug',
40
+ 'map_id',
41
+ 'saved_at',
42
+ 'deployment',
43
+ 'draft',
44
+ 'draft_notes',
45
+ 'figma_url',
46
+ ];
47
+ export const KNOWN_CAMPAIGN_FIELDS = [
48
+ 'ref_id',
49
+ 'id',
50
+ 'name',
51
+ 'slug',
52
+ 'currency',
53
+ 'language',
54
+ 'payment_env_key',
55
+ 'campaigns_api_key',
56
+ 'available_payment_methods',
57
+ 'available_express_payment_methods',
58
+ 'available_currencies',
59
+ 'available_shipping_countries',
60
+ 'tracking',
61
+ 'preferred_template_family',
62
+ 'allowed_domains',
63
+ 'route_root',
64
+ 'store_url',
65
+ 'store_name',
66
+ 'store_contact',
67
+ 'store_terms',
68
+ 'store_shipping',
69
+ 'store_privacy',
70
+ 'store_returns',
71
+ 'store_phone',
72
+ 'store_phone_tel',
73
+ 'footer_links',
74
+ 'seo',
75
+ ];
76
+ function skipKey(key) {
77
+ return key.startsWith('_') || key.endsWith('_note');
78
+ }
79
+ export const UnknownTopLevelFields = {
80
+ id: 'UnknownTopLevelFields',
81
+ severity: 'warning',
82
+ tags: ['fast', 'spec-only'],
83
+ check(spec) {
84
+ const violations = [];
85
+ const knownTop = new Set(KNOWN_TOP_LEVEL_FIELDS);
86
+ const knownCampaign = new Set(KNOWN_CAMPAIGN_FIELDS);
87
+ for (const key of Object.keys(spec)) {
88
+ if (skipKey(key) || knownTop.has(key))
89
+ continue;
90
+ violations.push({
91
+ ruleId: 'UnknownTopLevelFields',
92
+ severity: 'warning',
93
+ message: `Unknown top-level field "${key}" — not part of the CampaignSpec v4 surface. Check for a typo or a legacy key; unknown fields are carried through but never read.`,
94
+ path: `/${key}`,
95
+ data: { scope: 'top-level', key },
96
+ });
97
+ }
98
+ const campaign = spec.campaign;
99
+ if (campaign && typeof campaign === 'object' && !Array.isArray(campaign)) {
100
+ for (const key of Object.keys(campaign)) {
101
+ if (skipKey(key) || knownCampaign.has(key))
102
+ continue;
103
+ violations.push({
104
+ ruleId: 'UnknownTopLevelFields',
105
+ severity: 'warning',
106
+ message: `Unknown campaign field "campaign.${key}" — not part of the CampaignSpec v4 surface. Check for a typo or a legacy key; unknown fields are carried through but never read.`,
107
+ path: `/campaign/${key}`,
108
+ data: { scope: 'campaign', key },
109
+ });
110
+ }
111
+ }
112
+ return violations;
113
+ },
114
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * UpsellHasPackages — upsell and downsell pages must declare at least one
3
+ * package, since the runtime renders a bundle/offer selector for these page
4
+ * types and an empty packages array has nothing to show.
5
+ *
6
+ * Message text inherited verbatim from the pre-#110 validator at migration time.
7
+ */
8
+ import type { Rule } from '../types.ts';
9
+ export declare const UpsellHasPackages: Rule;