@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,107 @@
1
+ // Agent deviation telemetry: makes "the agent ignored Campaigns OS and
2
+ // wandered" measurable instead of anecdotal.
3
+ //
4
+ // `campaigns-os next` records its recommendation (stage + the commands it
5
+ // expects next) on the active run session. When a later pipeline-advancing
6
+ // command does not match that recommendation, a deviation entry is appended to
7
+ // a sidecar journal. Deviations are TELEMETRY, not blocks — hard gates live in
8
+ // `next`/doctor/qa. An agent can declare intent with --deviation-reason so an
9
+ // intentional detour is distinguishable from drift.
10
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
11
+ import { dirname, resolve } from "node:path";
12
+
13
+ export const DEVIATION_SCHEMA = "campaigns-os-agent-deviation/v0";
14
+ export const DEVIATION_JOURNAL_REL_PATH = ".campaign-runtime/agent-deviations.jsonl";
15
+
16
+ // Commands that advance the pipeline. Read-only / bookkeeping commands
17
+ // (doctor, next, findings, telemetry, run, validate-*) never deviate.
18
+ export const TRACKED_STAGE_COMMANDS = Object.freeze(new Set(["start", "prepare-build", "theme", "polish", "qa", "run-record"]));
19
+
20
+ // Commands every recommendation implicitly allows for its stage. Stage work is
21
+ // agent/skill work, so the expected command set is small and explicit.
22
+ const EXPECTED_COMMANDS_BY_STAGE = Object.freeze({
23
+ "prepare-build": ["prepare-build", "start"],
24
+ setup: ["theme"],
25
+ build: ["theme"],
26
+ polish: ["polish", "theme"],
27
+ deploy: [],
28
+ qa: ["qa", "theme"],
29
+ done: ["run-record"],
30
+ });
31
+
32
+ // The command word of a produced command line, whichever install prefix it
33
+ // was spelled with (bare `campaigns-os`, `npx campaigns-os`, `npm run
34
+ // campaigns-os --`, or `npx --yes <git-spec>` from an npx cache).
35
+ export function commandWord(command) {
36
+ if (typeof command !== "string") return null;
37
+ const stripped = command
38
+ .replace(/^npx\s+--yes\s+\S+\s+/, "campaigns-os ")
39
+ .replace(/^npx\s+campaigns-os\s+/, "campaigns-os ")
40
+ .replace(/^npm\s+run\s+campaigns-os\s+--\s+/, "campaigns-os ");
41
+ return stripped.match(/^campaigns-os\s+([a-z-]+)/)?.[1] || null;
42
+ }
43
+
44
+ export function expectedCommandsForStage(stage, requiredActions = []) {
45
+ const base = EXPECTED_COMMANDS_BY_STAGE[stage] || [];
46
+ // Gate required_actions name exact commands ("campaigns-os theme generate
47
+ // ..."); their command words are expected too.
48
+ const fromActions = requiredActions
49
+ .map((action) => commandWord(action?.command))
50
+ .filter(Boolean);
51
+ return [...new Set([...base, ...fromActions])];
52
+ }
53
+
54
+ export function buildRecommendation({ stage, status, expectedCommands, now = new Date() }) {
55
+ return {
56
+ stage,
57
+ status,
58
+ expected_commands: expectedCommands,
59
+ issued_at: now.toISOString(),
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Compare a pipeline-advancing command against the session's last
65
+ * recommendation. Returns a deviation entry or null.
66
+ */
67
+ export function detectDeviation({ lastRecommendation, command, argvShape = [], runId = null, deviationReason = null, now = new Date() }) {
68
+ if (!TRACKED_STAGE_COMMANDS.has(command)) return null;
69
+ if (!lastRecommendation || !Array.isArray(lastRecommendation.expected_commands)) return null;
70
+ if (lastRecommendation.expected_commands.includes(command)) return null;
71
+ return {
72
+ schema_version: DEVIATION_SCHEMA,
73
+ observed_at: now.toISOString(),
74
+ run_id: runId,
75
+ recommended_stage: lastRecommendation.stage || null,
76
+ recommended_status: lastRecommendation.status || null,
77
+ recommended_commands: lastRecommendation.expected_commands,
78
+ recommendation_issued_at: lastRecommendation.issued_at || null,
79
+ actual_command: command,
80
+ actual_argv_shape: argvShape,
81
+ deviation_reason: typeof deviationReason === "string" && deviationReason.trim() ? deviationReason.trim() : null,
82
+ };
83
+ }
84
+
85
+ export function appendDeviation(journalPath, entry) {
86
+ const path = resolve(journalPath);
87
+ mkdirSync(dirname(path), { recursive: true });
88
+ appendFileSync(path, `${JSON.stringify(entry)}\n`);
89
+ return entry;
90
+ }
91
+
92
+ /** Best-effort read; malformed lines are skipped, a missing journal is empty. */
93
+ export function readDeviations(journalPath) {
94
+ const path = resolve(journalPath);
95
+ if (!existsSync(path)) return [];
96
+ const entries = [];
97
+ for (const line of readFileSync(path, "utf8").split("\n")) {
98
+ if (!line.trim()) continue;
99
+ try {
100
+ const entry = JSON.parse(line);
101
+ if (entry && typeof entry === "object" && entry.schema_version === DEVIATION_SCHEMA) entries.push(entry);
102
+ } catch {
103
+ // tolerate a torn line; telemetry must never block reads
104
+ }
105
+ }
106
+ return entries;
107
+ }
@@ -0,0 +1,49 @@
1
+ function defineDoctorCheck({ id, phase = "doctor", run, when = null }, { registryId = "doctor", index = "unknown" } = {}) {
2
+ const location = `Doctor check registry "${registryId}" check at index ${index}`;
3
+ if (!isNonEmptyString(id)) throw new Error(`${location} needs a non-empty id.`);
4
+ const normalizedId = id.trim();
5
+ const checkLabel = `Doctor check registry "${registryId}" check "${normalizedId}"`;
6
+ if (!isNonEmptyString(phase)) throw new Error(`${checkLabel} needs a phase.`);
7
+ if (typeof run !== "function") throw new Error(`${checkLabel} needs a run function.`);
8
+ if (when != null && typeof when !== "function") throw new Error(`${checkLabel} has a non-function when predicate.`);
9
+
10
+ return Object.freeze({
11
+ id: normalizedId,
12
+ phase: phase.trim(),
13
+ run,
14
+ when,
15
+ });
16
+ }
17
+
18
+ export function createDoctorCheckRegistry(checks, { registryId = "doctor" } = {}) {
19
+ if (!Array.isArray(checks)) throw new Error(`Doctor check registry "${registryId}" must be an array.`);
20
+ const seen = new Map();
21
+ const normalized = checks.map((check, index) => {
22
+ if (!check || typeof check !== "object") {
23
+ throw new Error(`Doctor check registry "${registryId}" has a non-object check at index ${index}.`);
24
+ }
25
+ const doctorCheck = defineDoctorCheck(check, { registryId, index });
26
+ if (seen.has(doctorCheck.id)) {
27
+ throw new Error(`Doctor check registry "${registryId}" has duplicate check id "${doctorCheck.id}".`);
28
+ }
29
+ seen.set(doctorCheck.id, doctorCheck);
30
+ return doctorCheck;
31
+ });
32
+ return Object.freeze(normalized);
33
+ }
34
+
35
+ export function runDoctorCheckRegistry(checks, context, { phase = null } = {}) {
36
+ const phaseFilter = isNonEmptyString(phase) ? phase.trim() : null;
37
+ const executed = [];
38
+ for (const check of checks) {
39
+ if (phaseFilter && check.phase !== phaseFilter) continue;
40
+ if (check.when && !check.when(context)) continue;
41
+ check.run(context);
42
+ executed.push(check.id);
43
+ }
44
+ return executed;
45
+ }
46
+
47
+ function isNonEmptyString(value) {
48
+ return typeof value === "string" && value.trim().length > 0;
49
+ }
@@ -0,0 +1,106 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+
5
+ export const DOCTOR_SIDECAR_REL_PATH = ".campaign-runtime/doctor-output.json";
6
+ export const DOCTOR_SIDECAR_SCHEMA = "campaigns-os-doctor-output/v0";
7
+
8
+ export function doctorSidecarPath(targetBaseDir) {
9
+ return join(targetBaseDir, DOCTOR_SIDECAR_REL_PATH);
10
+ }
11
+
12
+ // Atomic JSON write (tmp + rename) for the artifacts other commands may read
13
+ // concurrently: the assembly report, the doctor sidecar, a run session. A
14
+ // torn report would defeat the gate decision it records, and a torn sidecar
15
+ // would itself break the freshness contract the stale stamp implements.
16
+ export function writeJsonAtomic(path, value) {
17
+ const resolved = resolve(path);
18
+ mkdirSync(dirname(resolved), { recursive: true });
19
+ const tmp = `${resolved}.${randomUUID()}.tmp`;
20
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
21
+ renameSync(tmp, resolved);
22
+ }
23
+
24
+ // #312: every retained sidecar names the command that persisted it, the way
25
+ // a stale stamp already names the command that made it (`stale_marked_by`).
26
+ // `generated_at` alone cannot tell a fresh `doctor --write` from a two-day-old
27
+ // gitignored copy carried along by `cp -R` — which is exactly what got a
28
+ // read-only command accused of writing this file. The name is the producer's
29
+ // own: each producer function states it where it is defined (`doctor`,
30
+ // `next`, `qa run`) or receives it from the dispatch that selected it (the
31
+ // intake body serving `start` / `build`); it is never re-read from argv at
32
+ // the write. A producer that forgets to say who it is fails loudly rather
33
+ // than writing an anonymous artifact, and a name that is not a plain command
34
+ // word (a control character, a stray newline) is refused here, at the seam,
35
+ // so a consumer that greps or re-prints the sidecar reads one producer per
36
+ // line. Placed beside `generated_at` so the two read together.
37
+ const PRODUCER_NAME = /^[a-z0-9][a-z0-9 _-]*$/;
38
+
39
+ export function stampDoctorProducer(doctor, command) {
40
+ if (typeof command !== "string" || !command.trim()) {
41
+ throw new TypeError("stampDoctorProducer requires command: the retained doctor sidecar names the command that produced it (generated_by).");
42
+ }
43
+ if (!PRODUCER_NAME.test(command.trim())) {
44
+ throw new TypeError(`stampDoctorProducer refuses producer name ${JSON.stringify(command)}: generated_by is a plain command word (${PRODUCER_NAME}).`);
45
+ }
46
+ if (!doctor || typeof doctor !== "object" || Array.isArray(doctor)) {
47
+ throw new TypeError("stampDoctorProducer requires a doctor result object.");
48
+ }
49
+ const { schema_version, generated_at, generated_by: _previous, ...rest } = doctor;
50
+ return {
51
+ ...(schema_version !== undefined ? { schema_version } : {}),
52
+ ...(generated_at !== undefined ? { generated_at } : {}),
53
+ generated_by: command.trim(),
54
+ ...rest,
55
+ };
56
+ }
57
+
58
+ // The one writer of the retained doctor sidecar: a wholesale, atomic rewrite
59
+ // of a doctorPacket result stamped with its producer. Every producer —
60
+ // `doctor --write`, `next`, `start`/`build`, the QA stage refresh — goes
61
+ // through this (or through `stampDoctorProducer` when its own transactional
62
+ // writer must do the rename), so `generated_by` cannot be skipped by one of
63
+ // them the way #327's cause labels once were.
64
+ export function writeDoctorSidecar(path, doctor, { command } = {}) {
65
+ writeJsonAtomic(path, stampDoctorProducer(doctor, command));
66
+ return path;
67
+ }
68
+
69
+ // #171 v1 freshness contract for the retained doctor sidecar: commands that
70
+ // mutate doctor inputs WITHOUT recomputing doctor state (theme waive/generate,
71
+ // qa policy set) stamp the retained snapshot stale instead of leaving a green
72
+ // lie on disk, while commands that DO recompute (doctor, prepare-build/start,
73
+ // next) rewrite the sidecar wholesale — which clears any stale stamp.
74
+ export function markDoctorSidecarStale(targetBaseDir, { command = null, reason = null } = {}) {
75
+ const path = doctorSidecarPath(targetBaseDir);
76
+ if (!existsSync(path)) return null;
77
+ let sidecar;
78
+ try {
79
+ sidecar = JSON.parse(readFileSync(path, "utf8"));
80
+ } catch {
81
+ return null;
82
+ }
83
+ if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar)) return null;
84
+ const stamped = {
85
+ ...sidecar,
86
+ stale: true,
87
+ stale_marked_by: command,
88
+ stale_marked_at: new Date().toISOString(),
89
+ stale_reason: reason
90
+ || "A later command changed doctor inputs after this snapshot was written. Re-run campaigns-os doctor (or campaigns-os next) for current state.",
91
+ };
92
+ writeJsonAtomic(path, stamped);
93
+ return path;
94
+ }
95
+
96
+ // The retained doctor sidecar records its own verdict twice: `ok` (boolean)
97
+ // and `status` ("ready", "ready_with_warnings", "ready_with_waivers",
98
+ // "blocked"). A bundle consumer must read that verdict rather than treat the
99
+ // sidecar's presence, schema validity, or freshness as readiness: a blocked
100
+ // doctor run is a perfectly well-formed artifact whose content says the
101
+ // campaign cannot proceed. Either signal blocks; a sidecar that is not an
102
+ // object reports nothing (its shape is the schema check's job, not this one's).
103
+ export function doctorSidecarBlocked(sidecar) {
104
+ if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar)) return false;
105
+ return sidecar.status === "blocked" || sidecar.ok === false;
106
+ }