@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,297 @@
1
+ {
2
+ "_note": "The single authority for how a checkout of this repository at one commit becomes a usable installed runtime. No checker, schema, document, or test may carry its own copy of a command, a version bound, a timeout, an input path, or an output rule stated here — every one of them is read from this file. It is data, never code: a consumer executes exactly the argv enumerated in steps[] and never a command assembled from repository data. Registered as a HASHED supported-surface entry rather than a named one, deliberately departing from the policy contracts introduced alongside it: a reason-code vocabulary grows additively and can safely live behind a named entry, but any change to the commands, network policy, accepted tool versions, inputs, or output verification here is an agent-relevant release event by the recipe's own rule. Only a hashed entry makes such a change require surface_version to advance in the same change (scripts/check-supported-surface.mjs --base). A recipe whose commands can change without a version bump is not a contract. Changing this file also owes a release-ledger entry.",
3
+ "_growth_note": "When a bound below is genuinely reached, the answer is to find out why before raising it. A dependency install that exceeds its bound on a warm machine is a supply-chain change, not a slow morning; an output inventory that exceeds its file or byte bound is a build that went wrong, not a package that grew 50x overnight. Raising a bound is the fallback, it advances recipe_revision, and it owes a release-ledger entry. Widening the accepted npm range follows the same path, and its trigger is external and checkable: widen when a Node release line ships that npm major by default, not when a particular machine happens to have it installed.",
4
+ "schema": "campaigns-os-runtime-recipe/v1",
5
+ "recipe_kind": "campaigns-os-node-v1",
6
+ "recipe_revision": "1.0.1",
7
+ "refusal_reason_code": "runtime_recipe_refused",
8
+ "fail_closed": true,
9
+ "unperformable_check_disposition": "failed",
10
+ "_kind_versus_revision_note": "The line is consumer comprehension, not semantic significance. A NEW KIND is anything an installed consumer would have to newly understand in order to execute the document correctly: a different command or package manager, a changed network policy shape, a new KIND of output check, or a new required field. An older consumer must fail closed on it, and a consumer release comes first. A REVISION re-parameterises fields the consumer already understands: accepted version bounds, timeout values, the enumerated input set, the expected output inventory. An older consumer executes a revision correctly, with different numbers. Both owe a release-ledger entry; only a new kind gates on a consumer release. The test for which one applies is answerable in a fixture — does a consumer built against this schema parse and execute the document? — rather than by judgement about how big the change feels.",
11
+ "preconditions": {
12
+ "_note": "All four must already hold before any step runs. They are fixed booleans because a precondition a document could switch off is not a precondition.",
13
+ "target_oid_resolved": true,
14
+ "lockfile_present": true,
15
+ "clean_staging_generation": true,
16
+ "input_fingerprint_recorded": true
17
+ },
18
+ "tooling": {
19
+ "node": {
20
+ "range": ">=20.19.0 <25",
21
+ "min_inclusive": "20.19.0",
22
+ "max_exclusive": "25.0.0",
23
+ "verified": [
24
+ "22.23.1"
25
+ ],
26
+ "applies_to": "The Node runtime executing both steps and, afterwards, the prepared generation.",
27
+ "rationale": "The lower bound is the target's own declared minimum. The upper bound is the recipe's, not the target's: the target declares an open-ended minimum, and an open-ended range is not a bound. Delegating the ceiling to the checkout under evaluation would let that checkout widen the accepted runtime of the consumer evaluating it. Qualifying a new Node major is a one-line revision plus a ledger entry, which is the intended cost."
28
+ },
29
+ "npm": {
30
+ "range": "10 || 11",
31
+ "min_inclusive": "10.0.0",
32
+ "max_exclusive": "12.0.0",
33
+ "accepted_majors": [
34
+ 10,
35
+ 11
36
+ ],
37
+ "verified": [
38
+ "10.9.8",
39
+ "11.19.1"
40
+ ],
41
+ "applies_to": "The package manager executing the install and build steps.",
42
+ "rationale": "Both majors were run end to end against this recipe and produced byte-identical output, and together they are exactly the set that supported Node release lines ship by default. Later majors are opt-in installs rather than what the ecosystem is running, so they stay outside the range until a Node line bundles one; that trigger is an external fact rather than a taste call."
43
+ },
44
+ "target_engines": {
45
+ "field": "engines.node",
46
+ "observed": ">=20.19.0",
47
+ "_note": "The exact value this revision was authored against. A consumer compares the target's live value to this string and refuses on any difference, per on_disagreement. That is deliberately strict: a widened engines range in the target is precisely the silent widening this contract exists to catch, and re-agreeing is a one-line revision."
48
+ },
49
+ "on_disagreement": "refuse"
50
+ },
51
+ "network": {
52
+ "_note": "Two independent bounds that must both hold. The per-step policy bounds WHERE bytes may come from; the integrity digests recorded in the lockfile bound WHICH bytes are acceptable — that second bound is declared at `target_expectations.lockfile.integrity_pinned`, over the file at `target_expectations.lockfile.path`, which is also listed in `inputs.files`. Neither bound substitutes for the other. WHO ENFORCES THIS, PRECISELY: this object is a declaration the CONSUMER enforces. The argv in `steps[].args` deliberately does not carry it — there is no `--registry` and no offline flag there, and no `env` field on a step. A conforming consumer constructs the process environment for each step so that its package manager resolves only from that step's declared `hosts` (and from nothing at all where the policy is `deny`), owns the cache and both npmrc paths per `cache_ownership`, and refuses inherited npmrc, proxy, and credential configuration per the three `inherit_*` fields above. Carrying the pin in argv instead would change the recipe's commands, which is a new recipe KIND rather than a revision; it is a legitimate future design, not a silent fix. WHAT THAT GUARANTEES: a package-manager configuration bound, not a host-level network sandbox. It cannot stop a process from opening a socket to some other address. What bounds that is the other half of the recipe: `--ignore-scripts` on both steps means no third-party dependency code executes during preparation at all, so the only programs that run are the package manager and the compiler. Read each step's policy as 'the consumer configures this step's process to resolve packages only from that step's declared hosts, which for a `deny` step is none at all, and no third-party code runs that could disregard it' — which is true and checkable. Do not read it as 'the host is prevented from reaching anything else', which would require a sandbox this contract does not specify. A consumer that adds a real network sandbox strengthens this bound without changing a field below, and is encouraged to; a consumer that treats the declared hosts as advisory violates it.",
53
+ "cache_ownership": "consumer_profile",
54
+ "inherit_proxy": false,
55
+ "inherit_credentials": false,
56
+ "inherit_npmrc": false,
57
+ "per_step": {
58
+ "install": {
59
+ "policy": "allowlist",
60
+ "hosts": [],
61
+ "integrity_source": "package-lock.json (lockfileVersion 3, per-package integrity digests)",
62
+ "rationale": "The install step is the only step that needs bytes it does not already have, and it needs them from exactly one place. Measured cold-cache install is a few seconds, so the network window is small. The cache is an optimisation and never a correctness input: an offline-after-warm policy would make a stale or poisoned cache silently change what gets built, with no fetch left to catch it. Note that `args` above carries no registry flag — the consumer is responsible for setting the package manager's registry to this host in the process environment before invoking that argv, and for refusing any inherited npmrc, proxy, or credential configuration that could redirect it. Combined with `--ignore-scripts`, nothing that runs during install is third-party code that could disregard the setting. The integrity digests remain the independent second bound, which is what makes a redirected or substituted byte stream fail even if the first bound were evaded."
63
+ },
64
+ "build": {
65
+ "policy": "deny",
66
+ "hosts": [],
67
+ "rationale": "The build step is a local type-directed compile and needs no network at all. Declaring that turns an assumption into a check. As with install, `args` above carries no offline flag — the consumer puts the package manager into offline mode through the process environment it constructs for this step. `tsc` opens no sockets of its own, and `--ignore-scripts` keeps pre/post hooks from introducing any."
68
+ }
69
+ }
70
+ },
71
+ "steps": [
72
+ {
73
+ "id": "install",
74
+ "executable": "npm",
75
+ "args": [
76
+ "ci",
77
+ "--ignore-scripts",
78
+ "--no-audit",
79
+ "--fund=false"
80
+ ],
81
+ "cwd": "target_root",
82
+ "stdin": "closed",
83
+ "lifecycle_scripts": "disabled",
84
+ "timeout_bound": "install_seconds",
85
+ "rationale": "ci rather than install, so the lockfile is authoritative and the tree is reproducible. --ignore-scripts is the load-bearing flag: it suppresses every dependency lifecycle script and the target's own prepare. Exactly one dependency in the resolved tree declares an install script, and it ships a prebuilt binary in its published tarball, so nothing in the tree needs its scripts to function. --no-audit and --fund=false remove two network- and output-side effects that are not part of preparing a runtime."
86
+ },
87
+ {
88
+ "id": "build",
89
+ "executable": "npm",
90
+ "args": [
91
+ "run",
92
+ "--ignore-scripts",
93
+ "build:spec"
94
+ ],
95
+ "cwd": "target_root",
96
+ "stdin": "closed",
97
+ "lifecycle_scripts": "disabled",
98
+ "timeout_bound": "build_seconds",
99
+ "rationale": "Not redundant with the install step. Because install runs with lifecycle scripts disabled, the target's prepare script does not fire and the output directory is absent afterwards; this step is the only thing that builds the runtime under the recipe's own flags. --ignore-scripts here means the named script runs while its pre and post siblings do not, so the build is exactly one contracted command rather than an open-ended chain the target can extend."
100
+ }
101
+ ],
102
+ "inputs": {
103
+ "_note": "The complete input set, enumerated explicitly rather than globbed. The compiler's configured include globs are NOT the input set: two root modules enter the compilation transitively through imports from the entry module and are emitted, so a fingerprint derived from the globs would cover 36 of the 38 compiled sources and miss one of the larger emitted surfaces. Test fixtures and the package's own tests are not inputs; nothing under them is emitted. A checker resolves the compiler's actual file list and asserts it equals this enumeration, so this list cannot rot silently.",
104
+ "fingerprint_algorithm": "sha256",
105
+ "files": [
106
+ "campaign-spec/analytics-vocabulary.ts",
107
+ "campaign-spec/index.ts",
108
+ "campaign-spec/normalize.ts",
109
+ "campaign-spec/package.json",
110
+ "campaign-spec/routing.ts",
111
+ "campaign-spec/rules/analytics-contract-shape.ts",
112
+ "campaign-spec/rules/assembly-hints-shape.ts",
113
+ "campaign-spec/rules/campaign-metadata.ts",
114
+ "campaign-spec/rules/checkout-has-success-url.ts",
115
+ "campaign-spec/rules/cycle-detection.ts",
116
+ "campaign-spec/rules/design-source-shape.ts",
117
+ "campaign-spec/rules/downsell-without-upsell.ts",
118
+ "campaign-spec/rules/exit-intent-validation.ts",
119
+ "campaign-spec/rules/funnel-count.ts",
120
+ "campaign-spec/rules/funnel-hypothesis-length.ts",
121
+ "campaign-spec/rules/funnel-identity.ts",
122
+ "campaign-spec/rules/funnel-weight-sum.ts",
123
+ "campaign-spec/rules/index.ts",
124
+ "campaign-spec/rules/offer-ref-integrity.ts",
125
+ "campaign-spec/rules/package-pricing-sanity.ts",
126
+ "campaign-spec/rules/page-count.ts",
127
+ "campaign-spec/rules/page-id-uniqueness.ts",
128
+ "campaign-spec/rules/promo-code-input-validation.ts",
129
+ "campaign-spec/rules/promo-codes-shape.ts",
130
+ "campaign-spec/rules/route-field-ignored-for-page-type.ts",
131
+ "campaign-spec/rules/route-target-resolves.ts",
132
+ "campaign-spec/rules/schema-version.ts",
133
+ "campaign-spec/rules/sdk-version.ts",
134
+ "campaign-spec/rules/shipping-countries-shape.ts",
135
+ "campaign-spec/rules/shipping-methods-present.ts",
136
+ "campaign-spec/rules/store-profile-shape.ts",
137
+ "campaign-spec/rules/thank-you-requirement.ts",
138
+ "campaign-spec/rules/unknown-top-level-fields.ts",
139
+ "campaign-spec/rules/upsell-has-packages.ts",
140
+ "campaign-spec/rules/upsell-routing-complete.ts",
141
+ "campaign-spec/rules/upsell-without-checkout.ts",
142
+ "campaign-spec/rules/variant-labels-shape.ts",
143
+ "campaign-spec/sdk-version-parse.ts",
144
+ "campaign-spec/tsconfig.build.json",
145
+ "campaign-spec/types.ts",
146
+ "package-lock.json",
147
+ "package.json"
148
+ ]
149
+ },
150
+ "outputs": {
151
+ "_note": "The output directory is a build product. It is untracked and git-ignored, no committed copy exists, and the copy in a published tarball exists only because packing runs the prepare script. There is therefore no baseline hash for its CONTENTS that this repository could publish, and any acceptance criterion phrased as 'output matches expected hashes' is not implementable as written. Verification is self-consistency instead: the inventory is complete and has nothing extra, the recorded hashes still hold, the entry module imports, the type entry is present, and the inputs that produced the output still match the inputs at the target commit. The build is deterministic — independent clean checkouts at one commit produce byte-identical output, and the compiler config emits neither source maps nor declaration maps, so no absolute paths or timestamps are embedded — which is what makes content hashing a sound strategy rather than a hopeful one.",
152
+ "directory": "campaign-spec/dist",
153
+ "committed": false,
154
+ "module_source_root": "campaign-spec/",
155
+ "expected_module_derivation": {
156
+ "rule": "For every enumerated input under module_source_root whose path ends in .ts, the build is expected to emit campaign-spec/dist/<path relative to module_source_root, with .ts replaced> once per entry in emitted_extensions. The expected inventory is exactly that set: nothing missing, nothing extra.",
157
+ "emitted_extensions": [
158
+ ".js",
159
+ ".d.ts"
160
+ ]
161
+ },
162
+ "type_entry": "campaign-spec/dist/index.d.ts",
163
+ "checks": [
164
+ {
165
+ "id": "dist_inventory",
166
+ "kind": "dist_inventory",
167
+ "mandatory": true,
168
+ "detects": [
169
+ "absent",
170
+ "extra"
171
+ ],
172
+ "applies_to": "The set of files present under outputs.directory after the build step.",
173
+ "rationale": "Catches both directions. A missing module is an incomplete emit; an unexpected file is as much a signal as a missing one, because it means something other than the declared build wrote into the output directory."
174
+ },
175
+ {
176
+ "id": "content_hash_stability",
177
+ "kind": "content_hash_stability",
178
+ "mandatory": true,
179
+ "detects": [
180
+ "corrupt"
181
+ ],
182
+ "applies_to": "The bytes of every file under outputs.directory, hashed with the inputs.fingerprint_algorithm digest and recorded in the generation manifest.",
183
+ "rationale": "Detects any post-build modification of the prepared runtime. Costs single-digit milliseconds against an output measured in hundreds of kilobytes. It is a self-consistency check, not a comparison against a hash published here: no such hash can exist, because the output is not committed."
184
+ },
185
+ {
186
+ "id": "module_import_smoke",
187
+ "kind": "module_import_smoke",
188
+ "mandatory": true,
189
+ "depth": "shallow",
190
+ "detects": [
191
+ "corrupt"
192
+ ],
193
+ "applies_to": "Importing the emitted package entry module once.",
194
+ "rationale": "A file can hash cleanly and still be unloadable. Shallow for this revision: importing the entry module transitively loads most of the emitted graph for one import's cost. A per-module deep import is a revision, warranted if a partial emit is ever actually observed rather than in anticipation of one."
195
+ },
196
+ {
197
+ "id": "type_entry_presence",
198
+ "kind": "type_entry_presence",
199
+ "mandatory": true,
200
+ "detects": [
201
+ "absent"
202
+ ],
203
+ "applies_to": "outputs.type_entry.",
204
+ "rationale": "Consumers resolve types for the package export through this file, and the pack check already asserts its presence in the published tarball. Cheap, and it guards a real declared contract rather than an internal detail."
205
+ },
206
+ {
207
+ "id": "cli_skill_commit_agreement",
208
+ "kind": "cli_skill_commit_agreement",
209
+ "mandatory": true,
210
+ "detects": [
211
+ "mismatched_generation"
212
+ ],
213
+ "applies_to": "The executable and the skills tree resolved by the running session.",
214
+ "rationale": "Asserts that the executable a session runs and the skills tree it loads resolve below the same generation path and the same target OID. Two halves of a session drawn from different generations is the failure this catches, and neither hashes nor imports would notice it."
215
+ },
216
+ {
217
+ "id": "tool_versions",
218
+ "kind": "tool_versions",
219
+ "mandatory": true,
220
+ "detects": [
221
+ "unsupported_tooling"
222
+ ],
223
+ "applies_to": "The Node and npm versions that actually executed the steps, against tooling.node and tooling.npm.",
224
+ "rationale": "Recorded after the fact as well as checked before, so a generation carries evidence of what built it rather than only of what was permitted to."
225
+ },
226
+ {
227
+ "id": "input_fingerprint",
228
+ "kind": "input_fingerprint",
229
+ "mandatory": true,
230
+ "detects": [
231
+ "stale"
232
+ ],
233
+ "applies_to": "The digest over the enumerated inputs.files recorded at build time, compared against the same digest computed at the target commit.",
234
+ "rationale": "The one check that cannot be dropped. Stale output is internally consistent — its hashes are correct, it imports, its types are present — so it is invisible to every output-side check. Only comparing the inputs that produced it against the inputs at the target commit catches it."
235
+ }
236
+ ]
237
+ },
238
+ "bounds": {
239
+ "install_seconds": {
240
+ "value": 180,
241
+ "unit": "seconds",
242
+ "measured_baseline": "about 3.2 seconds on a cold cache, about 0.3 seconds warm",
243
+ "applies_to": "Wall-clock duration of the install step.",
244
+ "rationale": "Roughly 57x the measured cold-cache cost, and deliberately generous. The measurement is a fast local connection, which is the best case rather than the typical one; this bound has to hold on a cold cache, a congested network, a loaded machine, and in CI. A timeout that trips on a slow morning produces a refusal the operator cannot act on."
245
+ },
246
+ "build_seconds": {
247
+ "value": 90,
248
+ "unit": "seconds",
249
+ "measured_baseline": "about 0.8 seconds",
250
+ "applies_to": "Wall-clock duration of the build step and the output checks that follow it.",
251
+ "rationale": "Roughly 115x the measured cost. The whole mandatory check set adds well under a second on top, so nothing here is deferred for cost. Generous for the same reason as the install bound."
252
+ },
253
+ "transaction_seconds": {
254
+ "value": 450,
255
+ "unit": "seconds",
256
+ "measured_baseline": "about 4 seconds end to end",
257
+ "applies_to": "Wall-clock duration of the whole preparation transaction: preconditions, both steps, and every output check.",
258
+ "rationale": "Roughly 110x measured. It bounds the transaction as a whole rather than being the sum of its parts, so a phase that stalls short of its own bound still cannot hold a preparation open indefinitely."
259
+ },
260
+ "max_output_bytes": {
261
+ "value": 16777216,
262
+ "unit": "bytes",
263
+ "measured_baseline": "240,359 bytes",
264
+ "applies_to": "Total bytes of all files under outputs.directory after the build step.",
265
+ "rationale": "16 MiB, roughly 70x the measured output. This bound does not exist in the performance budget it otherwise mirrors; it is added so that a build which goes haywire is a typed refusal rather than a filled disk."
266
+ },
267
+ "max_output_files": {
268
+ "value": 4096,
269
+ "unit": "files",
270
+ "measured_baseline": "76 files",
271
+ "applies_to": "Count of files under outputs.directory after the build step.",
272
+ "rationale": "Roughly 54x the measured count. Paired with max_output_bytes because the two catch different runaway shapes: many small files, and few enormous ones."
273
+ }
274
+ },
275
+ "target_expectations": {
276
+ "_note": "What this revision assumes about the target, stated as values a checker can compare rather than as prose a reader has to trust. Each one is a thing that, if it changed without the recipe changing, would silently alter what preparation does: a widened engine range, a rewritten build script, a lockfile format the install step reads differently, or a new dependency that would execute code the moment the suppressing flag was dropped.",
277
+ "package_json": "package.json",
278
+ "lockfile": {
279
+ "path": "package-lock.json",
280
+ "lockfile_version": 3,
281
+ "integrity_pinned": true
282
+ },
283
+ "scripts": {
284
+ "build:spec": "tsc -p campaign-spec/tsconfig.build.json",
285
+ "prepare": "npm run build:spec"
286
+ },
287
+ "install_script_dependencies": [
288
+ "fsevents"
289
+ ]
290
+ },
291
+ "capabilities": {
292
+ "_note": "What a generation prepared by this recipe can and cannot do. Stated explicitly because 'runtime ready' invites the wrong reading: the same flag that makes the install safe also means the prepared generation has no browser to drive. Preparing a runtime and being able to run browser QA are different readiness questions, and this recipe answers only the first.",
293
+ "type_check": true,
294
+ "build_spec": true,
295
+ "browser_qa": false
296
+ }
297
+ }
@@ -0,0 +1,299 @@
1
+ {
2
+ "_note": "The single authority for how a checkout of this repository at one commit becomes a usable installed runtime. No checker, schema, document, or test may carry its own copy of a command, a version bound, a timeout, an input path, or an output rule stated here — every one of them is read from this file. It is data, never code: a consumer executes exactly the argv enumerated in steps[] and never a command assembled from repository data. Registered as a HASHED supported-surface entry rather than a named one, deliberately departing from the policy contracts introduced alongside it: a reason-code vocabulary grows additively and can safely live behind a named entry, but any change to the commands, network policy, accepted tool versions, inputs, or output verification here is an agent-relevant release event by the recipe's own rule. Only a hashed entry makes such a change require surface_version to advance in the same change (scripts/check-supported-surface.mjs --base). A recipe whose commands can change without a version bump is not a contract. Changing this file also owes a release-ledger entry.",
3
+ "_growth_note": "When a bound below is genuinely reached, the answer is to find out why before raising it. A dependency install that exceeds its bound on a warm machine is a supply-chain change, not a slow morning; an output inventory that exceeds its file or byte bound is a build that went wrong, not a package that grew 50x overnight. Raising a bound is the fallback, it advances recipe_revision, and it owes a release-ledger entry. Widening the accepted npm range follows the same path, and its trigger is external and checkable: widen when a Node release line ships that npm major by default, not when a particular machine happens to have it installed.",
4
+ "schema": "campaigns-os-runtime-recipe/v1",
5
+ "recipe_kind": "campaigns-os-node-v1",
6
+ "recipe_revision": "1.0.1",
7
+ "refusal_reason_code": "runtime_recipe_refused",
8
+ "fail_closed": true,
9
+ "unperformable_check_disposition": "failed",
10
+ "_kind_versus_revision_note": "The line is consumer comprehension, not semantic significance. A NEW KIND is anything an installed consumer would have to newly understand in order to execute the document correctly: a different command or package manager, a changed network policy shape, a new KIND of output check, or a new required field. An older consumer must fail closed on it, and a consumer release comes first. A REVISION re-parameterises fields the consumer already understands: accepted version bounds, timeout values, the enumerated input set, the expected output inventory. An older consumer executes a revision correctly, with different numbers. Both owe a release-ledger entry; only a new kind gates on a consumer release. The test for which one applies is answerable in a fixture — does a consumer built against this schema parse and execute the document? — rather than by judgement about how big the change feels.",
11
+ "preconditions": {
12
+ "_note": "All four must already hold before any step runs. They are fixed booleans because a precondition a document could switch off is not a precondition.",
13
+ "target_oid_resolved": true,
14
+ "lockfile_present": true,
15
+ "clean_staging_generation": true,
16
+ "input_fingerprint_recorded": true
17
+ },
18
+ "tooling": {
19
+ "node": {
20
+ "range": ">=20.19.0 <25",
21
+ "min_inclusive": "20.19.0",
22
+ "max_exclusive": "25.0.0",
23
+ "verified": [
24
+ "22.23.1"
25
+ ],
26
+ "applies_to": "The Node runtime executing both steps and, afterwards, the prepared generation.",
27
+ "rationale": "The lower bound is the target's own declared minimum. The upper bound is the recipe's, not the target's: the target declares an open-ended minimum, and an open-ended range is not a bound. Delegating the ceiling to the checkout under evaluation would let that checkout widen the accepted runtime of the consumer evaluating it. Qualifying a new Node major is a one-line revision plus a ledger entry, which is the intended cost."
28
+ },
29
+ "npm": {
30
+ "range": "10 || 11",
31
+ "min_inclusive": "10.0.0",
32
+ "max_exclusive": "12.0.0",
33
+ "accepted_majors": [
34
+ 10,
35
+ 11
36
+ ],
37
+ "verified": [
38
+ "10.9.8",
39
+ "11.19.1"
40
+ ],
41
+ "applies_to": "The package manager executing the install and build steps.",
42
+ "rationale": "Both majors were run end to end against this recipe and produced byte-identical output, and together they are exactly the set that supported Node release lines ship by default. Later majors are opt-in installs rather than what the ecosystem is running, so they stay outside the range until a Node line bundles one; that trigger is an external fact rather than a taste call."
43
+ },
44
+ "target_engines": {
45
+ "field": "engines.node",
46
+ "observed": ">=20.19.0",
47
+ "_note": "The exact value this revision was authored against. A consumer compares the target's live value to this string and refuses on any difference, per on_disagreement. That is deliberately strict: a widened engines range in the target is precisely the silent widening this contract exists to catch, and re-agreeing is a one-line revision."
48
+ },
49
+ "on_disagreement": "refuse"
50
+ },
51
+ "network": {
52
+ "_note": "Two independent bounds that must both hold. The per-step policy bounds WHERE bytes may come from; the integrity digests recorded in the lockfile bound WHICH bytes are acceptable — that second bound is declared at `target_expectations.lockfile.integrity_pinned`, over the file at `target_expectations.lockfile.path`, which is also listed in `inputs.files`. Neither bound substitutes for the other. WHO ENFORCES THIS, PRECISELY: this object is a declaration the CONSUMER enforces. The argv in `steps[].args` deliberately does not carry it — there is no `--registry` and no offline flag there, and no `env` field on a step. A conforming consumer constructs the process environment for each step so that its package manager resolves only from that step's declared `hosts` (and from nothing at all where the policy is `deny`), owns the cache and both npmrc paths per `cache_ownership`, and refuses inherited npmrc, proxy, and credential configuration per the three `inherit_*` fields above. Carrying the pin in argv instead would change the recipe's commands, which is a new recipe KIND rather than a revision; it is a legitimate future design, not a silent fix. WHAT THAT GUARANTEES: a package-manager configuration bound, not a host-level network sandbox. It cannot stop a process from opening a socket to some other address. What bounds that is the other half of the recipe: `--ignore-scripts` on both steps means no third-party dependency code executes during preparation at all, so the only programs that run are the package manager and the compiler. Read each step's policy as 'the consumer configures this step's process to resolve packages only from that step's declared hosts, which for a `deny` step is none at all, and no third-party code runs that could disregard it' — which is true and checkable. Do not read it as 'the host is prevented from reaching anything else', which would require a sandbox this contract does not specify. A consumer that adds a real network sandbox strengthens this bound without changing a field below, and is encouraged to; a consumer that treats the declared hosts as advisory violates it.",
53
+ "cache_ownership": "consumer_profile",
54
+ "inherit_proxy": false,
55
+ "inherit_credentials": false,
56
+ "inherit_npmrc": false,
57
+ "per_step": {
58
+ "install": {
59
+ "policy": "allowlist",
60
+ "hosts": [
61
+ "registry.npmjs.org"
62
+ ],
63
+ "integrity_source": "package-lock.json (lockfileVersion 3, per-package integrity digests)",
64
+ "rationale": "The install step is the only step that needs bytes it does not already have, and it needs them from exactly one place. Measured cold-cache install is a few seconds, so the network window is small. The cache is an optimisation and never a correctness input: an offline-after-warm policy would make a stale or poisoned cache silently change what gets built, with no fetch left to catch it. Note that `args` above carries no registry flag — the consumer is responsible for setting the package manager's registry to this host in the process environment before invoking that argv, and for refusing any inherited npmrc, proxy, or credential configuration that could redirect it. Combined with `--ignore-scripts`, nothing that runs during install is third-party code that could disregard the setting. The integrity digests remain the independent second bound, which is what makes a redirected or substituted byte stream fail even if the first bound were evaded."
65
+ },
66
+ "build": {
67
+ "policy": "deny",
68
+ "hosts": [],
69
+ "rationale": "The build step is a local type-directed compile and needs no network at all. Declaring that turns an assumption into a check. As with install, `args` above carries no offline flag — the consumer puts the package manager into offline mode through the process environment it constructs for this step. `tsc` opens no sockets of its own, and `--ignore-scripts` keeps pre/post hooks from introducing any."
70
+ }
71
+ }
72
+ },
73
+ "steps": [
74
+ {
75
+ "id": "install",
76
+ "executable": "npm",
77
+ "args": [
78
+ "ci",
79
+ "--ignore-scripts",
80
+ "--no-audit",
81
+ "--fund=false"
82
+ ],
83
+ "cwd": "target_root",
84
+ "stdin": "closed",
85
+ "lifecycle_scripts": "disabled",
86
+ "timeout_bound": "install_seconds",
87
+ "rationale": "ci rather than install, so the lockfile is authoritative and the tree is reproducible. --ignore-scripts is the load-bearing flag: it suppresses every dependency lifecycle script and the target's own prepare. Exactly one dependency in the resolved tree declares an install script, and it ships a prebuilt binary in its published tarball, so nothing in the tree needs its scripts to function. --no-audit and --fund=false remove two network- and output-side effects that are not part of preparing a runtime."
88
+ },
89
+ {
90
+ "id": "build",
91
+ "executable": "npm",
92
+ "args": [
93
+ "run",
94
+ "--ignore-scripts",
95
+ "build:spec"
96
+ ],
97
+ "cwd": "target_root",
98
+ "stdin": "closed",
99
+ "lifecycle_scripts": "disabled",
100
+ "timeout_bound": "build_seconds",
101
+ "rationale": "Not redundant with the install step. Because install runs with lifecycle scripts disabled, the target's prepare script does not fire and the output directory is absent afterwards; this step is the only thing that builds the runtime under the recipe's own flags. --ignore-scripts here means the named script runs while its pre and post siblings do not, so the build is exactly one contracted command rather than an open-ended chain the target can extend."
102
+ }
103
+ ],
104
+ "inputs": {
105
+ "_note": "The complete input set, enumerated explicitly rather than globbed. The compiler's configured include globs are NOT the input set: two root modules enter the compilation transitively through imports from the entry module and are emitted, so a fingerprint derived from the globs would cover 36 of the 38 compiled sources and miss one of the larger emitted surfaces. Test fixtures and the package's own tests are not inputs; nothing under them is emitted. A checker resolves the compiler's actual file list and asserts it equals this enumeration, so this list cannot rot silently.",
106
+ "fingerprint_algorithm": "sha256",
107
+ "files": [
108
+ "campaign-spec/analytics-vocabulary.ts",
109
+ "campaign-spec/index.ts",
110
+ "campaign-spec/normalize.ts",
111
+ "campaign-spec/package.json",
112
+ "campaign-spec/routing.ts",
113
+ "campaign-spec/rules/analytics-contract-shape.ts",
114
+ "campaign-spec/rules/assembly-hints-shape.ts",
115
+ "campaign-spec/rules/campaign-metadata.ts",
116
+ "campaign-spec/rules/checkout-has-success-url.ts",
117
+ "campaign-spec/rules/cycle-detection.ts",
118
+ "campaign-spec/rules/design-source-shape.ts",
119
+ "campaign-spec/rules/downsell-without-upsell.ts",
120
+ "campaign-spec/rules/exit-intent-validation.ts",
121
+ "campaign-spec/rules/funnel-count.ts",
122
+ "campaign-spec/rules/funnel-hypothesis-length.ts",
123
+ "campaign-spec/rules/funnel-identity.ts",
124
+ "campaign-spec/rules/funnel-weight-sum.ts",
125
+ "campaign-spec/rules/index.ts",
126
+ "campaign-spec/rules/offer-ref-integrity.ts",
127
+ "campaign-spec/rules/package-pricing-sanity.ts",
128
+ "campaign-spec/rules/page-count.ts",
129
+ "campaign-spec/rules/page-id-uniqueness.ts",
130
+ "campaign-spec/rules/promo-code-input-validation.ts",
131
+ "campaign-spec/rules/promo-codes-shape.ts",
132
+ "campaign-spec/rules/route-field-ignored-for-page-type.ts",
133
+ "campaign-spec/rules/route-target-resolves.ts",
134
+ "campaign-spec/rules/schema-version.ts",
135
+ "campaign-spec/rules/sdk-version.ts",
136
+ "campaign-spec/rules/shipping-countries-shape.ts",
137
+ "campaign-spec/rules/shipping-methods-present.ts",
138
+ "campaign-spec/rules/store-profile-shape.ts",
139
+ "campaign-spec/rules/thank-you-requirement.ts",
140
+ "campaign-spec/rules/unknown-top-level-fields.ts",
141
+ "campaign-spec/rules/upsell-has-packages.ts",
142
+ "campaign-spec/rules/upsell-routing-complete.ts",
143
+ "campaign-spec/rules/upsell-without-checkout.ts",
144
+ "campaign-spec/rules/variant-labels-shape.ts",
145
+ "campaign-spec/sdk-version-parse.ts",
146
+ "campaign-spec/tsconfig.build.json",
147
+ "campaign-spec/types.ts",
148
+ "package-lock.json",
149
+ "package.json"
150
+ ]
151
+ },
152
+ "outputs": {
153
+ "_note": "The output directory is a build product. It is untracked and git-ignored, no committed copy exists, and the copy in a published tarball exists only because packing runs the prepare script. There is therefore no baseline hash for its CONTENTS that this repository could publish, and any acceptance criterion phrased as 'output matches expected hashes' is not implementable as written. Verification is self-consistency instead: the inventory is complete and has nothing extra, the recorded hashes still hold, the entry module imports, the type entry is present, and the inputs that produced the output still match the inputs at the target commit. The build is deterministic — independent clean checkouts at one commit produce byte-identical output, and the compiler config emits neither source maps nor declaration maps, so no absolute paths or timestamps are embedded — which is what makes content hashing a sound strategy rather than a hopeful one.",
154
+ "directory": "campaign-spec/dist",
155
+ "committed": true,
156
+ "module_source_root": "campaign-spec/",
157
+ "expected_module_derivation": {
158
+ "rule": "For every enumerated input under module_source_root whose path ends in .ts, the build is expected to emit campaign-spec/dist/<path relative to module_source_root, with .ts replaced> once per entry in emitted_extensions. The expected inventory is exactly that set: nothing missing, nothing extra.",
159
+ "emitted_extensions": [
160
+ ".js",
161
+ ".d.ts"
162
+ ]
163
+ },
164
+ "type_entry": "campaign-spec/dist/index.d.ts",
165
+ "checks": [
166
+ {
167
+ "id": "dist_inventory",
168
+ "kind": "dist_inventory",
169
+ "mandatory": true,
170
+ "detects": [
171
+ "absent",
172
+ "extra"
173
+ ],
174
+ "applies_to": "The set of files present under outputs.directory after the build step.",
175
+ "rationale": "Catches both directions. A missing module is an incomplete emit; an unexpected file is as much a signal as a missing one, because it means something other than the declared build wrote into the output directory."
176
+ },
177
+ {
178
+ "id": "content_hash_stability",
179
+ "kind": "content_hash_stability",
180
+ "mandatory": true,
181
+ "detects": [
182
+ "corrupt"
183
+ ],
184
+ "applies_to": "The bytes of every file under outputs.directory, hashed with the inputs.fingerprint_algorithm digest and recorded in the generation manifest.",
185
+ "rationale": "Detects any post-build modification of the prepared runtime. Costs single-digit milliseconds against an output measured in hundreds of kilobytes. It is a self-consistency check, not a comparison against a hash published here: no such hash can exist, because the output is not committed."
186
+ },
187
+ {
188
+ "id": "module_import_smoke",
189
+ "kind": "module_import_smoke",
190
+ "mandatory": true,
191
+ "depth": "shallow",
192
+ "detects": [
193
+ "corrupt"
194
+ ],
195
+ "applies_to": "Importing the emitted package entry module once.",
196
+ "rationale": "A file can hash cleanly and still be unloadable. Shallow for this revision: importing the entry module transitively loads most of the emitted graph for one import's cost. A per-module deep import is a revision, warranted if a partial emit is ever actually observed rather than in anticipation of one."
197
+ },
198
+ {
199
+ "id": "type_entry_presence",
200
+ "kind": "type_entry_presence",
201
+ "mandatory": true,
202
+ "detects": [
203
+ "absent"
204
+ ],
205
+ "applies_to": "outputs.type_entry.",
206
+ "rationale": "Consumers resolve types for the package export through this file, and the pack check already asserts its presence in the published tarball. Cheap, and it guards a real declared contract rather than an internal detail."
207
+ },
208
+ {
209
+ "id": "cli_skill_commit_agreement",
210
+ "kind": "cli_skill_commit_agreement",
211
+ "mandatory": true,
212
+ "detects": [
213
+ "mismatched_generation"
214
+ ],
215
+ "applies_to": "The executable and the skills tree resolved by the running session.",
216
+ "rationale": "Asserts that the executable a session runs and the skills tree it loads resolve below the same generation path and the same target OID. Two halves of a session drawn from different generations is the failure this catches, and neither hashes nor imports would notice it."
217
+ },
218
+ {
219
+ "id": "tool_versions",
220
+ "kind": "tool_versions",
221
+ "mandatory": true,
222
+ "detects": [
223
+ "unsupported_tooling"
224
+ ],
225
+ "applies_to": "The Node and npm versions that actually executed the steps, against tooling.node and tooling.npm.",
226
+ "rationale": "Recorded after the fact as well as checked before, so a generation carries evidence of what built it rather than only of what was permitted to."
227
+ },
228
+ {
229
+ "id": "input_fingerprint",
230
+ "kind": "input_fingerprint",
231
+ "mandatory": true,
232
+ "detects": [
233
+ "stale"
234
+ ],
235
+ "applies_to": "The digest over the enumerated inputs.files recorded at build time, compared against the same digest computed at the target commit.",
236
+ "rationale": "The one check that cannot be dropped. Stale output is internally consistent — its hashes are correct, it imports, its types are present — so it is invisible to every output-side check. Only comparing the inputs that produced it against the inputs at the target commit catches it."
237
+ }
238
+ ]
239
+ },
240
+ "bounds": {
241
+ "install_seconds": {
242
+ "value": 180,
243
+ "unit": "seconds",
244
+ "measured_baseline": "about 3.2 seconds on a cold cache, about 0.3 seconds warm",
245
+ "applies_to": "Wall-clock duration of the install step.",
246
+ "rationale": "Roughly 57x the measured cold-cache cost, and deliberately generous. The measurement is a fast local connection, which is the best case rather than the typical one; this bound has to hold on a cold cache, a congested network, a loaded machine, and in CI. A timeout that trips on a slow morning produces a refusal the operator cannot act on."
247
+ },
248
+ "build_seconds": {
249
+ "value": 90,
250
+ "unit": "seconds",
251
+ "measured_baseline": "about 0.8 seconds",
252
+ "applies_to": "Wall-clock duration of the build step and the output checks that follow it.",
253
+ "rationale": "Roughly 115x the measured cost. The whole mandatory check set adds well under a second on top, so nothing here is deferred for cost. Generous for the same reason as the install bound."
254
+ },
255
+ "transaction_seconds": {
256
+ "value": 450,
257
+ "unit": "seconds",
258
+ "measured_baseline": "about 4 seconds end to end",
259
+ "applies_to": "Wall-clock duration of the whole preparation transaction: preconditions, both steps, and every output check.",
260
+ "rationale": "Roughly 110x measured. It bounds the transaction as a whole rather than being the sum of its parts, so a phase that stalls short of its own bound still cannot hold a preparation open indefinitely."
261
+ },
262
+ "max_output_bytes": {
263
+ "value": 16777216,
264
+ "unit": "bytes",
265
+ "measured_baseline": "240,359 bytes",
266
+ "applies_to": "Total bytes of all files under outputs.directory after the build step.",
267
+ "rationale": "16 MiB, roughly 70x the measured output. This bound does not exist in the performance budget it otherwise mirrors; it is added so that a build which goes haywire is a typed refusal rather than a filled disk."
268
+ },
269
+ "max_output_files": {
270
+ "value": 4096,
271
+ "unit": "files",
272
+ "measured_baseline": "76 files",
273
+ "applies_to": "Count of files under outputs.directory after the build step.",
274
+ "rationale": "Roughly 54x the measured count. Paired with max_output_bytes because the two catch different runaway shapes: many small files, and few enormous ones."
275
+ }
276
+ },
277
+ "target_expectations": {
278
+ "_note": "What this revision assumes about the target, stated as values a checker can compare rather than as prose a reader has to trust. Each one is a thing that, if it changed without the recipe changing, would silently alter what preparation does: a widened engine range, a rewritten build script, a lockfile format the install step reads differently, or a new dependency that would execute code the moment the suppressing flag was dropped.",
279
+ "package_json": "package.json",
280
+ "lockfile": {
281
+ "path": "package-lock.json",
282
+ "lockfile_version": 3,
283
+ "integrity_pinned": true
284
+ },
285
+ "scripts": {
286
+ "build:spec": "tsc -p campaign-spec/tsconfig.build.json",
287
+ "prepare": "npm run build:spec"
288
+ },
289
+ "install_script_dependencies": [
290
+ "fsevents"
291
+ ]
292
+ },
293
+ "capabilities": {
294
+ "_note": "What a generation prepared by this recipe can and cannot do. Stated explicitly because 'runtime ready' invites the wrong reading: the same flag that makes the install safe also means the prepared generation has no browser to drive. Preparing a runtime and being able to run browser QA are different readiness questions, and this recipe answers only the first.",
295
+ "type_check": true,
296
+ "build_spec": true,
297
+ "browser_qa": false
298
+ }
299
+ }