@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,274 @@
1
+ # Release-ledger authoring guide
2
+
3
+ Every agent-relevant change to this repository gets one entry in
4
+ `contracts/release-ledger.json`. This is how you write one, and what CI will do
5
+ to you if you do not.
6
+
7
+ If you are an agent reading this repository rather than changing it, you want
8
+ [`AGENTS.md`](../AGENTS.md) and
9
+ [`docs/orientation-contract-reference.md`](orientation-contract-reference.md)
10
+ instead.
11
+
12
+ ## Why a ledger as well as a changelog
13
+
14
+ `CHANGELOG.md` is keyed to the supported-surface version. That misses a whole
15
+ class of change that a downstream agent very much cares about: a renamed CLI
16
+ flag, a rewritten contract doc, a new or reworded skill, a changed
17
+ generated-runtime input, a widened compatibility statement. None of those need
18
+ touch a hashed file, so none of them move `surface_version`, so an agent reading
19
+ only the changelog concludes nothing happened.
20
+
21
+ The ledger records those. The changelog stays the human narrative, and each
22
+ ledger entry links to exactly one changelog section so the two can never tell
23
+ different stories.
24
+
25
+ ## What counts as agent-relevant
26
+
27
+ One definition, one file: `contracts/agent-relevant-change-policy.v1.json`. The
28
+ classifier, the gate, the generated reference, and every test read it. There is
29
+ no second copy to keep in sync, and you should not make one.
30
+
31
+ It defines ten semantic classes — `schema`, `hashed_surface`, `named_surface`,
32
+ `cli_surface`, `skill`, `package_export`, `compatibility_policy`,
33
+ `documentation`, `workflow`, `generated_runtime` — and classifies a changed path
34
+ in a fixed, total order:
35
+
36
+ 1. **Self-referential exemptions.** `contracts/release-ledger.json` and
37
+ `CHANGELOG.md`. Recording a change is not itself a recorded change.
38
+ 2. **Explicit rules.** Ordered, first match wins.
39
+ 3. **Derived from the supported surface.** Every path in `hashed{}` or `named[]`
40
+ is agent-relevant by construction. This pass runs *before* the ignore list on
41
+ purpose: a path you just added to the supported surface must never be
42
+ swallowed by a broad ignore prefix like `docs/` or `contracts/`.
43
+ 4. **Ignored, with a stated reason.** "No agent impact" is an assertion someone
44
+ wrote down, not an omission someone forgot.
45
+ 5. **Unclassified — an error.** A path matching nothing fails the gate. The
46
+ classifier fails closed, which means adding a new top-level file makes you
47
+ say what it is.
48
+
49
+ ## Writing an entry
50
+
51
+ ```jsonc
52
+ {
53
+ "id": "RL-0007", // never reused, never renumbered
54
+ "sequence": 7, // exactly one more than the last entry
55
+ "date": "2026-09-01", // not earlier than the last entry
56
+ "kind": "release", // or "amendment"
57
+ "surface_version": "1.15.0", // null for a same-surface change
58
+ "changelog_section": "1.15.0",
59
+ "changelog_sha256": "<sha256 of that section body>",
60
+ "agent_impact": "What a consumer must do differently. 'None.' is fine — but write it.",
61
+ "compatibility": "compatible | additive | breaking",
62
+ "migration": "The exact action, or \"none\". A breaking entry may not say none.",
63
+ "changes": [
64
+ {
65
+ "class": "named_surface",
66
+ "path": "docs/build-packet.md",
67
+ "surface_entry": "docs/build-packet.md",
68
+ "summary": "One sentence, written for a consumer."
69
+ }
70
+ ],
71
+ "entry_sha256": "<sha256 of this entry with entry_sha256 removed>"
72
+ }
73
+ ```
74
+
75
+ `sequence` and `id` are two different things. `sequence` is the entry's
76
+ position in the array and must equal that position: the checker derives the
77
+ expected value from the index, not from the previous entry's claim, so it is the
78
+ authoritative order. `id` is a unique, immutable label (`RL-NNNN`) that is never
79
+ reused and never renumbered; nothing requires its number to match `sequence`.
80
+ The two therefore diverge legitimately, and already do in this ledger: when two
81
+ PRs are open at once, the second to merge restamps its `sequence` to sit after
82
+ the first while keeping the id it was written with. Read order from `sequence`
83
+ and identity from `id`; never renumber an id to close the gap.
84
+
85
+ Two hashes, two jobs. `changelog_sha256` catches a changelog section edited
86
+ after the fact. `entry_sha256` catches a historical entry edited in place, even
87
+ in a squashed history where the diff is gone.
88
+
89
+ Computing them:
90
+
91
+ ```bash
92
+ node --input-type=module -e '
93
+ import { readFileSync } from "node:fs";
94
+ import { parseChangelogSections, entryHash } from "./scripts/orientation-contract.mjs";
95
+ const section = parseChangelogSections(readFileSync("CHANGELOG.md", "utf8"))
96
+ .find((s) => s.section_id === "1.15.0");
97
+ console.log("changelog_sha256:", section.body_sha256);
98
+ '
99
+ ```
100
+
101
+ Then add the entry with that `changelog_sha256`, and compute `entry_sha256` the
102
+ same way with `entryHash(entry)`.
103
+
104
+ ### Same-surface changes
105
+
106
+ Set `surface_version` to `null` and link a changelog section identified as
107
+ `<current-version>+agent.<n>` — for example `1.14.0+agent.1`. It sorts at the
108
+ same version, so nobody reads it as a release that did not happen, and it gives
109
+ the entry a real section to point at.
110
+
111
+ ### Path-less change items
112
+
113
+ A CLI flag has no file of its own. Record it with `"path": null` and name the
114
+ affected `surface_entry` (the command). The gate still requires that some
115
+ classified path of the same class changed in the range, so a flag change cannot
116
+ be recorded without the bytes actually moving somewhere.
117
+
118
+ ### Fixes that touch only policy-ignored paths
119
+
120
+ A fix living entirely in paths the policy ignores — `src/` other than
121
+ `src/cli.mjs`, `scripts/`, tests and fixtures — carries a same-surface CHANGELOG
122
+ section (`X.Y.Z+agent.N`) and **no ledger entry**. There is nothing for an entry
123
+ to claim: every change item must map to a classified changed path in the range,
124
+ and an ignored path is never classified, so an entry written for such a PR is
125
+ refused by the backward direction of the gate rather than merely unnecessary. A
126
+ path-less item fails the same way, because no classified change of its class
127
+ exists in the range. The ignore list and its stated reasons are in
128
+ [`contracts/agent-relevant-change-policy.v1.json`](../contracts/agent-relevant-change-policy.v1.json).
129
+
130
+ The dividing line inside `src/` is `src/cli.mjs`: an explicit rule classifies it
131
+ as `cli_surface`, so any change to it is agent-relevant and owes an entry, even
132
+ when the behaviour change originates in a helper module beside it.
133
+
134
+ ### Amendments
135
+
136
+ Historical entries are never edited. When an entry turns out to be wrong or
137
+ incomplete, append a correction:
138
+
139
+ ```jsonc
140
+ {
141
+ "id": "RL-0008",
142
+ "sequence": 8,
143
+ "kind": "amendment",
144
+ "amends": "RL-0007",
145
+ "amendment_reason": "RL-0007 was recorded as compatible; it removed a documented guarantee.",
146
+ "surface_version": null,
147
+ "changelog_section": "1.15.0+agent.1",
148
+ "compatibility": "breaking",
149
+ "migration": "Stop relying on the removed guarantee; see docs/build-packet.md.",
150
+ "agent_impact": "Treat the 1.15.0 packet doc change as breaking, not compatible.",
151
+ "changes": [ /* … */ ]
152
+ }
153
+ ```
154
+
155
+ An amendment is the only entry kind whose change items may map to no changed
156
+ path in its own range, because it corrects meaning rather than moving bytes.
157
+
158
+ ### Correcting a section's bytes
159
+
160
+ A changelog section an entry hashes is as frozen as the entry. When the section
161
+ itself has to change — a stray merge-conflict marker committed inside it is the
162
+ case that has happened — the historical entry cannot take the new hash, and a
163
+ plain amendment pointing at some other section leaves the stale hash failing.
164
+ So an amendment may link the **same** `changelog_section` as the entry it
165
+ amends, carrying that section's current `changelog_sha256`. The checker then
166
+ reads the amended entry's hash as superseded, and the amendment's own hash
167
+ keeps the section pinned. Only an amendment, and only one that names the
168
+ entry currently holding the link, may re-link a section; any other second link
169
+ still fails the one-to-one rule. Say in `amendment_reason` what changed in the
170
+ section and why.
171
+
172
+ `scripts/check-changelog-structure.mjs` (part of `npm run check`) refuses the
173
+ marker lines outright, in `CHANGELOG.md` and under `docs/`, and also holds the
174
+ section layout: identifiers unique, `+agent.N` sections in one run directly
175
+ above their release with N descending (newest first), and every ledger
176
+ `changelog_section` naming a section that exists. Insert a new `+agent.N`
177
+ section at the top of its release's run, not directly above the release
178
+ heading.
179
+
180
+ ## Running the gate
181
+
182
+ ```bash
183
+ node ./scripts/check-release-ledger.mjs # structure, hashes, limits
184
+ node ./scripts/check-release-ledger.mjs --base origin/main # the two-way gate
185
+ ```
186
+
187
+ Without `--base` the checker validates the ledger as it stands. The completeness
188
+ gate needs a comparison point, so pass `--base` in CI and before you open a PR.
189
+ `npm run check` runs the structural half.
190
+
191
+ ## What passes and what fails
192
+
193
+ The authoritative matrix is data, not prose:
194
+ `contracts/fixtures/orientation/release-gate/cases.json`. Every case there is a
195
+ test in `scripts/check-release-ledger.test.mjs`. Add a case and you have added a
196
+ test.
197
+
198
+ Passes:
199
+
200
+ - A hashed schema changed, `surface_version` advanced, one entry claims it.
201
+ - A named contract doc changed with no surface bump, one entry with
202
+ `surface_version: null` and a `+agent.N` changelog section.
203
+ - A CLI flag, a skill, a workflow, or the compatibility statement changed, same
204
+ shape.
205
+ - Several changes in one entry, each path covered by exactly one change item.
206
+ - An amendment that maps to no changed path.
207
+
208
+ Fails:
209
+
210
+ - An agent-relevant path with no change item — the failure the gate exists for.
211
+ - A change item naming a path that did not change and is not an amendment.
212
+ - A change item claiming an implementation path the policy excludes.
213
+ - A duplicate entry id, or one entry recording the same `(class, path,
214
+ surface_entry)` identity twice. Identity is unique WITHIN an entry, not across
215
+ the ledger: a path is touched by many releases over a repository's life, and
216
+ each of those is a real change that must be recordable. Recording one change
217
+ twice inside a single range is caught by the coverage rule instead — a path
218
+ covered by more than one change item fails.
219
+ - A sequence gap, or a date earlier than the previous entry.
220
+ - A `breaking` entry whose migration is `none`, or a blank `agent_impact`.
221
+ - A missing changelog section, a duplicate section identifier, or a stale
222
+ `changelog_sha256`.
223
+ - A stale `entry_sha256`.
224
+ - Any commit-shaped property on an entry — see below.
225
+ - A rewritten or deleted historical entry.
226
+ - Two entries claiming the same `surface_version`.
227
+ - A changed path the policy has never seen.
228
+ - A supported-surface bump that no new entry claims.
229
+ - A path-less change item with no classified change of its class in the range.
230
+ - An amendment with no `amends`, an `amends` naming no earlier entry, or no
231
+ `amendment_reason`; or a non-amendment carrying either field.
232
+
233
+ Only entries NEW in the comparison range are classified against the current
234
+ policy and supported surface. A historical entry was written under the policy in
235
+ force at the time and the ledger is append-only, so re-judging it under a
236
+ tightened policy would fail a document nobody is permitted to edit. Everything
237
+ else — shape, ordering, sequence, hashes, changelog correspondence — applies to
238
+ every entry.
239
+
240
+ ## Entries carry no commit
241
+
242
+ An entry cannot name the commit containing it without being rewritten after that
243
+ commit exists. The schema rejects any commit-shaped property, and the checker
244
+ says so by name rather than reporting a generic "unexpected property".
245
+
246
+ A consumer derives the introducing commit from history at the target OID:
247
+ oldest-first over the commits touching the ledger, crediting each entry id to the
248
+ first commit whose ledger blob contains it. Merge commits are handled by that
249
+ walk without a special case.
250
+
251
+ The walk covers the FULL history ending at the target, with history
252
+ simplification disabled (`git rev-list --full-history --reverse --topo-order
253
+ <oid> -- contracts/release-ledger.json`). A walk that starts at some base loses
254
+ every entry introduced before it; a simplified walk can drop the side-branch
255
+ commit that actually introduced an entry. `AGENTS.md` states both properties for
256
+ consumers.
257
+
258
+ ## Size bounds
259
+
260
+ `contracts/orientation-limits.v1.json` bounds what a consumer reads: source
261
+ bytes, section count, section bytes, envelope bytes, ledger entries. Exceeding
262
+ one is a refusal with `orientation_too_large`, never a truncation. So is a
263
+ measurement that is absent or non-finite: a bound nobody measured is a bound
264
+ nobody enforced.
265
+
266
+ Every bound is a whole-artifact guardrail — the complete changelog and the
267
+ complete ledger at the target commit, not a baseline-to-target window. That is
268
+ the conservative direction, since a window is always a subset of the whole. Two
269
+ of them (section count, ledger entries) grow monotonically; when one is reached
270
+ the answer is baseline rotation, described in the contract's `_growth_note`, not
271
+ a quiet raise.
272
+
273
+ Raising a limit is a reviewed policy change: advance `limits_version`, and the
274
+ change owes its own ledger entry like anything else.
@@ -0,0 +1,211 @@
1
+ <!--
2
+ GENERATED FILE — do not edit.
3
+ Source: contracts/runtime-recipe.campaigns-os-node-v1.json
4
+ Regenerate: node ./scripts/generate-runtime-readiness.mjs --write
5
+ -->
6
+
7
+ # Runtime readiness
8
+
9
+ How a checkout of this repository at one commit becomes a usable installed runtime, and how a consumer decides whether a prepared one is still trustworthy. Everything below is generated from `contracts/runtime-recipe.campaigns-os-node-v1.json`, which is the only authority for these values.
10
+
11
+ Recipe kind `campaigns-os-node-v1`, revision `1.0.1`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.33.0`.
12
+
13
+ ## What this is
14
+
15
+ A recipe is data, not code. A consumer executes exactly the commands enumerated here and never a command assembled from repository data. This repository publishes the recipe; the installed consumer bootstrap executes it, using its own released parser rather than anything loaded from the checkout under evaluation.
16
+
17
+ Enforcement is fail-closed. Every field is normative: `fail_closed` is `true` and a check that cannot be performed counts as `failed`, never as skipped. A refusal carries the stable reason code `runtime_recipe_refused`.
18
+
19
+ ## What a prepared runtime can and cannot do
20
+
21
+ 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.
22
+
23
+ | Capability | Available |
24
+ |---|---|
25
+ | `type_check` | yes |
26
+ | `build_spec` | yes |
27
+ | `browser_qa` | **no** |
28
+
29
+ ## Preconditions
30
+
31
+ 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.
32
+
33
+ - `target_oid_resolved`
34
+ - `lockfile_present`
35
+ - `clean_staging_generation`
36
+ - `input_fingerprint_recorded`
37
+
38
+ ## Tool versions
39
+
40
+ | Tool | Accepted range | Verified against | Rationale |
41
+ |---|---|---|---|
42
+ | Node | `>=20.19.0 <25` | `22.23.1` | 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. |
43
+ | npm | `10 || 11` | `10.9.8`, `11.19.1` | 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. |
44
+
45
+ The contract declares its own ranges rather than inheriting the target's. It records the target's `engines.node` as `>=20.19.0`; on disagreement the disposition is `refuse`. 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.
46
+
47
+ ## Network
48
+
49
+ 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.
50
+
51
+ | Step | Policy | Hosts | Rationale |
52
+ |---|---|---|---|
53
+ | `install` | `allowlist` | `registry.npmjs.org` | 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. |
54
+ | `build` | `deny` | none | 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. |
55
+
56
+ Cache ownership is `consumer_profile`. Inherited proxy configuration: `false`. Inherited credentials: `false`. Inherited npm configuration file: `false`.
57
+
58
+ ## Steps
59
+
60
+ ### install
61
+
62
+ ```
63
+ npm ci --ignore-scripts --no-audit --fund=false
64
+ ```
65
+
66
+ Working directory `target_root`, stdin `closed`, lifecycle scripts `disabled`, bounded by `install_seconds`.
67
+
68
+ 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.
69
+
70
+ ### build
71
+
72
+ ```
73
+ npm run --ignore-scripts build:spec
74
+ ```
75
+
76
+ Working directory `target_root`, stdin `closed`, lifecycle scripts `disabled`, bounded by `build_seconds`.
77
+
78
+ 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.
79
+
80
+ ## Inputs
81
+
82
+ 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.
83
+
84
+ Fingerprint algorithm `sha256`, over 42 enumerated files:
85
+
86
+ - `campaign-spec/analytics-vocabulary.ts`
87
+ - `campaign-spec/index.ts`
88
+ - `campaign-spec/normalize.ts`
89
+ - `campaign-spec/package.json`
90
+ - `campaign-spec/routing.ts`
91
+ - `campaign-spec/rules/analytics-contract-shape.ts`
92
+ - `campaign-spec/rules/assembly-hints-shape.ts`
93
+ - `campaign-spec/rules/campaign-metadata.ts`
94
+ - `campaign-spec/rules/checkout-has-success-url.ts`
95
+ - `campaign-spec/rules/cycle-detection.ts`
96
+ - `campaign-spec/rules/design-source-shape.ts`
97
+ - `campaign-spec/rules/downsell-without-upsell.ts`
98
+ - `campaign-spec/rules/exit-intent-validation.ts`
99
+ - `campaign-spec/rules/funnel-count.ts`
100
+ - `campaign-spec/rules/funnel-hypothesis-length.ts`
101
+ - `campaign-spec/rules/funnel-identity.ts`
102
+ - `campaign-spec/rules/funnel-weight-sum.ts`
103
+ - `campaign-spec/rules/index.ts`
104
+ - `campaign-spec/rules/offer-ref-integrity.ts`
105
+ - `campaign-spec/rules/package-pricing-sanity.ts`
106
+ - `campaign-spec/rules/page-count.ts`
107
+ - `campaign-spec/rules/page-id-uniqueness.ts`
108
+ - `campaign-spec/rules/promo-code-input-validation.ts`
109
+ - `campaign-spec/rules/promo-codes-shape.ts`
110
+ - `campaign-spec/rules/route-field-ignored-for-page-type.ts`
111
+ - `campaign-spec/rules/route-target-resolves.ts`
112
+ - `campaign-spec/rules/schema-version.ts`
113
+ - `campaign-spec/rules/sdk-version.ts`
114
+ - `campaign-spec/rules/shipping-countries-shape.ts`
115
+ - `campaign-spec/rules/shipping-methods-present.ts`
116
+ - `campaign-spec/rules/store-profile-shape.ts`
117
+ - `campaign-spec/rules/thank-you-requirement.ts`
118
+ - `campaign-spec/rules/unknown-top-level-fields.ts`
119
+ - `campaign-spec/rules/upsell-has-packages.ts`
120
+ - `campaign-spec/rules/upsell-routing-complete.ts`
121
+ - `campaign-spec/rules/upsell-without-checkout.ts`
122
+ - `campaign-spec/rules/variant-labels-shape.ts`
123
+ - `campaign-spec/sdk-version-parse.ts`
124
+ - `campaign-spec/tsconfig.build.json`
125
+ - `campaign-spec/types.ts`
126
+ - `package-lock.json`
127
+ - `package.json`
128
+
129
+ ## Outputs
130
+
131
+ 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.
132
+
133
+ Directory `campaign-spec/dist`. Committed: `false`. Type entry `campaign-spec/dist/index.d.ts`.
134
+
135
+ Expected inventory is derived, never listed twice. 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. Emitted extensions: `.js`, `.d.ts`. At this revision that derivation yields 76 files.
136
+
137
+ ### Mandatory checks
138
+
139
+ Every check below is mandatory; there is no optional check, because an optional check is an advisory bound under another name.
140
+
141
+ | Check | Kind | Detects | Applies to | Rationale |
142
+ |---|---|---|---|---|
143
+ | `dist_inventory` | `dist_inventory` | `absent`, `extra` | The set of files present under outputs.directory after the build step. | 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. |
144
+ | `content_hash_stability` | `content_hash_stability` | `corrupt` | The bytes of every file under outputs.directory, hashed with the inputs.fingerprint_algorithm digest and recorded in the generation manifest. | 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. |
145
+ | `module_import_smoke` | `module_import_smoke` (shallow) | `corrupt` | Importing the emitted package entry module once. | 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. |
146
+ | `type_entry_presence` | `type_entry_presence` | `absent` | outputs.type_entry. | 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. |
147
+ | `cli_skill_commit_agreement` | `cli_skill_commit_agreement` | `mismatched_generation` | The executable and the skills tree resolved by the running session. | 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. |
148
+ | `tool_versions` | `tool_versions` | `unsupported_tooling` | The Node and npm versions that actually executed the steps, against tooling.node and tooling.npm. | 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. |
149
+ | `input_fingerprint` | `input_fingerprint` | `stale` | The digest over the enumerated inputs.files recorded at build time, compared against the same digest computed at the target commit. | 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. |
150
+
151
+ ### Prepared-runtime states
152
+
153
+ Declarative descriptions of the prepared-runtime states an output-check implementation must distinguish. A test builds each state from the accepted recipe rather than from paths written down here, so adding a module to the input set cannot leave a fixture describing an inventory that no longer exists.
154
+
155
+ | State | Expect | Detected as | Why it matters |
156
+ |---|---|---|---|
157
+ | `healthy` | pass | — | The control. Without it the four failure fixtures prove only that the checker fails, not that it discriminates. |
158
+ | `absent` | fail | `absent` | Nothing was built, or the output directory was removed after the build. The inventory check is the only one that can report this cleanly; every other check would report a cascade. |
159
+ | `extra` | fail | `extra` | Something other than the declared build wrote into the output directory. An unexpected file is as much a signal as a missing one. |
160
+ | `corrupt` | fail | `corrupt` | A file was modified after the build recorded its digest. Caught by hash stability; the import smoke is the second line for the case where the alteration also makes the module unloadable. |
161
+ | `stale` | fail | `stale` | The output is internally consistent — complete inventory, correct hashes, imports fine — but was produced from inputs that no longer match the target commit. Only the input fingerprint sees this. |
162
+
163
+ ## What the recipe assumes about the target
164
+
165
+ 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.
166
+
167
+ | Assumption | Value |
168
+ |---|---|
169
+ | Manifest | `package.json` |
170
+ | Lockfile | `package-lock.json`, version `3`, integrity pinned `true` |
171
+ | Script `build:spec` | `tsc -p campaign-spec/tsconfig.build.json` |
172
+ | Script `prepare` | `npm run build:spec` |
173
+ | Dependencies declaring an install script | `fsevents` |
174
+
175
+ ## Bounds
176
+
177
+ | Bound | Value | Measured baseline | Applies to | Rationale |
178
+ |---|---|---|---|---|
179
+ | `install_seconds` | 180 seconds | about 3.2 seconds on a cold cache, about 0.3 seconds warm | Wall-clock duration of the install step. | 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. |
180
+ | `build_seconds` | 90 seconds | about 0.8 seconds | Wall-clock duration of the build step and the output checks that follow it. | 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. |
181
+ | `transaction_seconds` | 450 seconds | about 4 seconds end to end | Wall-clock duration of the whole preparation transaction: preconditions, both steps, and every output check. | 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. |
182
+ | `max_output_bytes` | 16777216 bytes | 240,359 bytes | Total bytes of all files under outputs.directory after the build step. | 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. |
183
+ | `max_output_files` | 4096 files | 76 files | Count of files under outputs.directory after the build step. | Roughly 54x the measured count. Paired with max_output_bytes because the two catch different runaway shapes: many small files, and few enormous ones. |
184
+
185
+ 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.
186
+
187
+ ## Changing the recipe
188
+
189
+ 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.
190
+
191
+ The recipe and its schema are both HASHED supported-surface entries, so changing either requires `surface_version` to advance in the same change. 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.
192
+
193
+ ## Refusals
194
+
195
+ These documents are refused. Each is a single-mutation fixture under `contracts/fixtures/runtime-recipe/reject/`, so a refusal is always attributable to one change.
196
+
197
+ | Fixture | Why it is refused |
198
+ |---|---|
199
+ | `reject/unknown-kind.json` | recipe_kind names a kind this schema version does not define. An installed consumer was not released knowing how to execute it, so it fails closed rather than guessing that a v2 is a v1 with extras. |
200
+ | `reject/unknown-revision.json` | recipe_revision leaves the major line the kind defines. A revision may only re-parameterise fields the consumer already understands; a different major is a shape change wearing a revision's clothes. |
201
+ | `reject/unknown-network-policy.json` | A safety-critical enum: the install step declares a network policy outside the defined set. There is no allow-all value, and an unrecognized one is refused rather than treated as permissive. |
202
+ | `reject/allowlist-without-hosts.json` | An allowlist with no hosts is not a bound, it is an empty declaration that reads like one. The schema requires a non-empty host list whenever the policy is an allowlist. |
203
+ | `reject/unknown-output-check.json` | A safety-critical enum: an output check names a kind the consumer cannot perform. A new KIND of check is a new recipe kind, because an installed consumer cannot perform a check it was not released knowing. |
204
+ | `reject/unknown-step-id.json` | A safety-critical enum: a step identity outside the defined set. Steps are identified rather than positional, so an unrecognized id is a command the consumer has no contract for. |
205
+ | `reject/lifecycle-scripts-enabled.json` | A safety-critical enum: a step that permits lifecycle scripts. Enabling them would let the target run arbitrary code during preparation, which is the single thing the recipe's flags exist to prevent. |
206
+ | `reject/engines-disagreement-warns.json` | A safety-critical enum: disagreement between the contract's tool range and the target's declared engines resolved as a warning. An accept-with-warning path produces a build made under conditions nobody approved. |
207
+ | `reject/advisory-enforcement.json` | fail_closed switched off. Advisory bounds record the right numbers and enforce nothing, so the first time a bound matters you discover it was decorative. |
208
+ | `reject/unperformable-check-skipped.json` | A safety-critical enum: a check that cannot be performed treated as skipped. A skipped check reports success it never established. |
209
+ | `reject/committed-output-claim.json` | The recipe claims its output directory is a committed artifact. It is not: the directory is git-ignored and untracked, so no baseline for its contents can exist here and verification must be self-consistency. |
210
+ | `reject/unpinned-lockfile.json` | The recipe claims its lockfile is not integrity-pinned. The network allowlist bounds where bytes may come from and the lockfile's digests bound which bytes are acceptable; dropping the second leaves the first standing alone, which it was never meant to do. |
211
+ | `reject/missing-required-field.json` | The enumerated input set is absent. Without it there is nothing to fingerprint, and staleness — the one failure mode no output-side check can see — becomes undetectable. |
@@ -0,0 +1,83 @@
1
+ # Supported surface
2
+
3
+ This repo stopped being an implementation the day other tooling started building
4
+ on it. Campaigns Agent pins schemas, contract docs, and a CLI argv surface;
5
+ the private ops repo vendors the runtime schemas behind a byte-parity gate;
6
+ page-kit campaign repos consume the artifacts the CLI emits. This document — and
7
+ its machine twin, [`contracts/supported-surface.json`](../contracts/supported-surface.json),
8
+ enforced by `scripts/check-supported-surface.mjs` in `npm run check` and CI —
9
+ names exactly what those consumers may depend on. If it is not listed, it is
10
+ implementation detail, however stable it looks.
11
+
12
+ ## What is supported
13
+
14
+ | Surface | Contract | Change discipline |
15
+ |---|---|---|
16
+ | `schemas/*.schema.json` (all of them) | The portable contract catalog: CampaignSpec, Design Source Package, Build Packet, Build Context, Assembly Report, Doctor Output, sidecar-bundle conformance, Run Record, Workflow Finding, Build Brief, Source-HTML Manifest, Tooling Orientation, Release Ledger, QA Verdict, the QA Verdict sidecar projection, Runtime Recipe, and the legacy-migration inventory/plan/receipt trio. | Hashed. Any content change requires updating the recorded hash **and** bumping `surface_version` in the same PR. A shape change that alters meaning gets a new schema-version const — one version identifier must never cover two shapes (the 2026-08 assembly-report drift is the incident this rule encodes). Additions to an open `v0` schema are expected and consumers must tolerate unknown fields; the security-sensitive legacy-migration schemas are closed, so additions there require a new lineage. 1.28.0 (RL entry `surface_version: 1.28.0`, breaking) removed the two required Build Packet booleans `qa.test_orders_allowed` and `qa.sandbox_test_card_confirmed` (nothing read them; test orders run from `--test-order <mode>` alone), added `local-serve` to the `deploy.target` enum, and added the optional `remit_result` / `remit_base_kind` fields to the Run Record. 1.30.0 (additive) added the optional `data_layer` record to the QA Verdict's `test_orders[]` entries — the order's `dl_purchase` reading (#325). 1.33.0 (additive) added the optional `qa_verdict_publish` block to the Run Record — which verdict was posted to the QA portal, by `qa run` or `qa publish`, and what the portal answered, in the `remit_result` vocabulary (#328). |
17
+ | CLI commands: `start`, `prepare-build`, `build`, `polish`, `checkpoint`, `page-kit`, `spec`, `doctor`, `bundle`, `next`, `theme`, `tooling`, `install-skills`, `install-agent-context`, `validate-assembly-report`, `telemetry`, `standardize`, `qa`, `findings`, `run-record`, `run` | Scriptable entry points. The founding list (1.0.0) was the argv surface Campaigns Agent's remit fixture pins; 1.25.0 adds the partner entry path the public guides instruct — `install-skills` (skill install), `tooling status` (preflight), `next --packet` (the runtime cursor) — plus `theme`, `install-agent-context`, `validate-assembly-report`, and `telemetry`, since consent is part of the partner contract. `validate-build-packet`, formerly an undocumented and unsupported alias of `doctor`, was removed in 1.25.0+agent.7 (RL-0036) and now gets the unknown-command error; use `doctor`. `standardization-report`, a supported but redundant second spelling of `standardize` — same flags, same output, same exit codes — was removed in 1.27.0 and now gets the same unknown-command error; use `standardize`. Removing a supported command is a breaking change and is why that release moved the minor. `bundle check` validates the canonical JSON readback set and makes QA required only under `--require-qa`. `polish capture` owns page-load evidence production. `checkpoint waive` currently accepts four registered gates: `page_kit.store_profile`, `page_kit.sdk_version`, `polish.hidden_eager_media`, and `built_output.upsell_selector_scope`. `page-kit sync` (added in 1.29.0) writes the CampaignSpec's Store Profile fields and SDK pin into the target's `_data/campaigns.json` entry — the repair the first two gates name — and is the only `page-kit` subcommand. `spec derive` (added in 1.31.0) is the reverse write for the repo-derived field class (#432): the target's SDK pin, page routes and analytics ids into the packet's local CampaignSpec; it is the only `spec` subcommand, and the `page_kit.sdk_version.repo_newer` advisory names it; its `--write-map` flag (1.33.0+agent.2) also records the derived pin in the saved Map's Build hints field through the proxy Worker, never moving a Map pin backwards. `qa publish` (added in 1.33.0) posts an already-stored verdict to the QA portal without a re-run or an order, refusing a stale `spec_hash` or a verdict its Run Record already records as published (#328). Within Polish, only the broader Source Freshness waiver remains on its existing report lane; theme and QA decisions also retain their existing lanes. | Any change to this list — adding, renaming, or removing a command — bumps `surface_version` in the same PR, and since 1.25.0 `check-supported-surface.mjs --base` enforces that (before, only hashed and named entries owed a bump, so `checkpoint` landed unbumped). Subcommands, registered gates, and flags may grow freely beneath a listed command. Removing a listed command from dispatch fails the gate outright. Do not infer support for an unregistered checkpoint from the top-level command. |
18
+ | `bin/campaigns-os.mjs` (`campaigns-os`) | The CLI entry itself. | Declared in `package.json` `bin`; the gate fails if it disappears. |
19
+ | Package export `./campaign-spec` | The versioned campaign-spec rule registry, consumed as `@nextcommerce/campaigns-os` (pinned by consumers' lockfiles; lockstep policy — ADR-003 in the ops repo). | Behavior-guarded from the consumer side by their contract tests; the export path itself is gated here. |
20
+ | Package exports `./commercial-journey` and `./commercial-parity` | Portable scenario planning, response normalization, contract-governed source extraction, Exact-only parity comparison, and deterministic QA assertion serialization. These modules own no network transport and do not calculate prices locally. | Consumers execute descriptors through a supported calculate transport, then pass captured envelopes into the pure normalizer. Existing export paths are gated and may not be renamed or removed without a breaking surface change. |
21
+ | Package export `./legacy-migration` and its three schema exports | Portable SDK 0.3.x migration inventory, preview-plan, receipt, Offer request/readback, and token-free evidence helpers. Guide: [`docs/legacy-migration.md`](legacy-migration.md). | Pure contract only: no authenticated transport, write executor, audit store, receipt store, sessions, deletes, or rollback. Consumers own execution and must retain the guarded apply protocol. |
22
+ | Package export `./text-safety` | `singleLineField`, `singleLineFragment` and `singleLineDetail`: flatten a value this toolkit did not author to one line with no control characters before it is rendered into a single-line notice. `singleLineField` is for a value printed as its own field (a run id, a target path): every control character becomes U+FFFD and nothing else changes. `singleLineFragment` is for a value folded into a sentence (a gate's repair command or instruction): line breaks and tabs become spaces, runs of whitespace collapse, the ends are trimmed, and every other control character becomes U+FFFD. `singleLineDetail` is `singleLineFragment` plus Markdown escaping, a length cap and a placeholder for an empty value (a quoted loader message). | Pure string functions, no imports, no I/O. Added at surface 1.27.0; `singleLineFragment` added at 1.27.0+agent.2. The escape set may widen; a value that was already safe stays unchanged. |
23
+ | Contract docs: `CONTEXT.md`, `docs/campaigns-os-build-flow.md`, `docs/build-packet.md`, `docs/migration-sidecar-bundle.md`, `docs/design-source-package.md`, `docs/campaign-build-brief.md`, `docs/campaign-standardization-report.md`, `docs/brand-theme-bridge.md`, `docs/qa-and-test-orders.md`, `docs/legacy-migration.md`, `docs/versioning.md`, `docs/workflow-findings-sidecar.md`, this file | Named entry points consumers pin for context. | Content evolves freely; the path must keep existing. |
24
+ | `skills.json` + `skills/` + `skills.sh` | Versioned skill packages and their installer. | Governed by `check-skill-versions.mjs` (parity + bump gate + reserved external names). `skills.json` ships in the npm pack as of surface 1.0.0. |
25
+ | `compatibility.json` | The published compatibility statement. | Named; must keep existing. |
26
+ | Orientation contract: `AGENTS.md`, `CHANGELOG.md`, `contracts/release-ledger.json`, `contracts/agent-relevant-change-policy.v1.json`, `contracts/orientation-limits.v1.json`, `contracts/orientation-reason-codes.v1.json`, `docs/orientation-contract-reference.md`, `docs/release-ledger-authoring-guide.md`, `contracts/fixtures/orientation/canonicalization/v1.json` | The declarative data a consumer reads from Git objects to decide whether a commit is safe to work against, without executing anything from this repository. Schema `campaigns-os-tooling-orientation/v1`; ledger schema `campaigns-os-release-ledger/v1`. The canonicalization fixture gives independent implementations exact ledger and changelog bytes plus their expected SHA-256 digests. Entry point: [`AGENTS.md`](../AGENTS.md). | Named. The ledger is append-only: a correction ships as a new amendment entry, never an edit. `scripts/check-release-ledger.mjs` enforces the two-way gate; `docs/orientation-contract-reference.md` and the canonicalization fixture are generated together, and CI fails if either is stale. |
27
+ | Orientation fixtures: `contracts/fixtures/orientation/envelope/*.json`, `contracts/fixtures/orientation/hostile-target/**` (as named) | The bytes a consumer's parser validates against: one envelope per terminal outcome, plus a hostile target carrying Git hooks, an executable file, and npm lifecycle scripts for proving a reader executes nothing. The hostile target carries a second invariant for the runtime recipe: preparing it must run the recipe's own two steps and no lifecycle script reachable from them. | Named. Regenerate the envelopes with `npm run generate:orientation-docs`. Fixtures under `contracts/fixtures/` that are **not** named here — including the legacy-migration conformance corpus — are this repo's own test data and are not supported. |
28
+ | Runtime recipe: `contracts/runtime-recipe.campaigns-os-node-v1.json` | The declarative description of how a checkout at one commit becomes a usable installed runtime: exact commands, accepted tool ranges, per-step network policy, the enumerated input set, the mandatory output checks, and the enforced bounds. This repository publishes it; the consumer bootstrap executes it. Guide: [`docs/runtime-readiness.md`](runtime-readiness.md). | **Hashed**, deliberately unlike the orientation policy contracts beside it. Any change to commands, network policy, tool versions, inputs, or output verification is an agent-relevant release event, and only a hashed entry makes such a change require `surface_version` to advance in the same PR. A recipe whose commands can change without a version bump is not a contract. |
29
+ | Runtime-recipe fixtures: `contracts/fixtures/runtime-recipe/**` (as named), `docs/runtime-readiness.md` | Accept and single-mutation reject documents a consumer's parser validates against, the prepared-runtime states its output checks must distinguish, and the generated guide. | Named. All of it is generated — regenerate with `npm run generate:runtime-docs`; CI fails on a stale copy. |
30
+ | Migration sidecar bundle: `contracts/migration-sidecar-bundle.v0.json`, `docs/migration-sidecar-bundle.md`, and `contracts/fixtures/sidecar-bundle/production-shaped/**` (as named) | The strict machine contract and production-shaped consumer fixture for the root Build Packet plus Build Context, Assembly Report, Doctor Output, and QA Verdict JSON sidecars. Packet selection uses `generated_at`, never mtime; raw spec integrity is distinct from canonical material identity; safe repository-relative spellings normalize without accepting traversal; markdown may coexist but is never readback truth. | The machine contract and schemas are hashed. The fixture is named byte-for-byte consumer input and must keep passing `campaigns-os bundle check --require-qa`. A required QA verdict with `disposition: blocked` keeps handoff nonconformant and sets `stage_blocked`. A doctor sidecar recording a blocked run emits `bundle.doctor_output.blocked`, and a blocked QA verdict `bundle.qa_verdict.blocked`, as warnings by default and errors under `--require-qa`; `stage_blocked` is unchanged; `status: conformant` never asserts that doctor or QA passed. |
31
+
32
+ The Run Record lifecycle block keeps two duration meanings explicit:
33
+ `duration_ms` is summed active command time, while `wall_clock_duration_ms` is
34
+ the elapsed span between the first command start and last completion. Run
35
+ sessions bind to an explicit packet across working directories, retain blocked
36
+ QA attempts for repair, and close only on a ready verdict or explicit `run end`.
37
+ Doctor and QA producers update only their matching Assembly Report stage with
38
+ their current output paths and timestamps; they do not synthesize historical
39
+ stage completion.
40
+
41
+ Everything on this list must also **ship in the npm tarball** — the gate checks
42
+ `package.json` `files[]` coverage, so "supported" can never mean "absent from
43
+ the package a consumer installs."
44
+
45
+ ## What is NOT supported
46
+
47
+ - `src/**` except the files reached through the explicit
48
+ `./commercial-journey`, `./commercial-parity`, and `./legacy-migration` package exports — including
49
+ files downstream context spines currently read
50
+ (`src/cli.mjs`, `src/qa-*.mjs`, `src/doctor-check-registry.mjs`, …). Reading
51
+ them for context is fine; importing or pinning behavior from them is not.
52
+ Doctor issue **codes** are contract-adjacent but currently governed by the
53
+ ops-repo ADR-003 parity baseline, not this manifest.
54
+ - `scripts/**` — repo checkers, including this gate's own implementation.
55
+ - `examples/**`, `prompts/**`, `agents/**` — illustrative, regenerated at will.
56
+ - `contracts/**` other than `supported-surface.json`,
57
+ `reserved-skill-names.json`, and the orientation contract/fixture entries
58
+ named in the manifest — internal build/QA contract data. In particular,
59
+ `contracts/fixtures/orientation/release-gate/cases.json` is this repo's own
60
+ release-gate test matrix, not a consumer contract.
61
+ - CLI output text, log lines, and human-facing handoff strings. Machine-readable
62
+ artifact fields are governed by their schemas, not by prose.
63
+
64
+ ## Changing the surface
65
+
66
+ 1. Make the change and update `contracts/supported-surface.json` (hash and/or
67
+ entries) in the same PR.
68
+ 2. Bump `surface_version` when any hashed file changed (the `--base` gate in CI
69
+ enforces this; parity runs in every `npm run check`). Also bump it when the
70
+ manifest adds a `cli_commands`, `package_exports`, or `bin` entry: those are
71
+ additive public-surface expansions even though the gate cannot yet derive
72
+ the owed bump automatically.
73
+ 3. Add a release-ledger entry. Every agent-relevant change — hashed or named
74
+ path, CLI command/subcommand/flag, skill, schema, package export,
75
+ compatibility policy, agent-facing documentation, workflow, or generated
76
+ runtime — owes exactly one entry in `contracts/release-ledger.json`, whether
77
+ or not `surface_version` moved. `scripts/check-release-ledger.mjs --base`
78
+ enforces this in both directions. See
79
+ [the authoring guide](release-ledger-authoring-guide.md).
80
+ 4. Breaking a consumer-visible shape? New schema-version const, and say so in
81
+ the PR body — downstream pins (Campaigns Agent context spine, ops-repo
82
+ `public-contracts.manifest.json`) update on their own cadence against a
83
+ version they can see move.