@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,229 @@
1
+ // Run Record closeout recognition.
2
+ //
3
+ // `next` at stage "done" used to demand a Run Record unconditionally, because
4
+ // nothing in the CLI ever read `.campaign-runtime/run-records/`. A run that had
5
+ // already assembled, closed and remitted its record was still told to make one.
6
+ // The fix is not "go quiet whenever any record exists" — that would re-open the
7
+ // #171 failure the required action was added to prevent. This module decides,
8
+ // from already-read JSON, whether a MATCHING, CURRENT, SUCCESSFULLY CLOSED
9
+ // record exists for this exact packet, and names the reason when it does not.
10
+ //
11
+ // Pure: no filesystem, no clock, no network. The caller reads the records and
12
+ // computes the current QA verdict digest; this module only judges.
13
+ //
14
+ // FAIL OPEN. Every doubt — an unreadable record, an unrecognized remit state, a
15
+ // record that cannot be tied to the verdict the report currently points at —
16
+ // resolves to "not satisfied", which emits the closeout action. A false demand
17
+ // costs one idempotent command; a false silence loses the run's durable record.
18
+
19
+ /*
20
+ * Closed reason vocabulary (`reason_code` on assessRunRecordCloseout's
21
+ * outcome). `satisfied` is the only value that suppresses the required
22
+ * closeout action; every other value names why it must still fire.
23
+ *
24
+ * - `satisfied` a matching, current, closed record exists
25
+ * - `no_record` no readable record for this packet at all
26
+ * - `foreign_campaign` records exist, none belongs to this campaign
27
+ * - `stale_predates_evidence` the newest matching record is older than the
28
+ * producer evidence currently in the report
29
+ * - `outdated_artifacts` the record does not reference the QA verdict the
30
+ * report's qa stage currently points at
31
+ * - `remit_failed` remit was attempted and failed
32
+ * - `remit_incomplete` remit never reached a terminal state (pending, or
33
+ * a state this version does not recognize)
34
+ */
35
+
36
+ // Remit outcomes that mean "this record is closed". `ok` is a completed remit.
37
+ // `skipped` is the consent-off / --no-remit / local-only path: a deliberate
38
+ // non-remit is a closed record, not a failure, and nagging it forever would
39
+ // punish every offline operator.
40
+ const CLOSED_REMIT_STATES = new Set(["ok", "skipped"]);
41
+
42
+ function isObject(value) {
43
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
44
+ }
45
+
46
+ function text(value) {
47
+ return typeof value === "string" && value.trim() ? value.trim() : null;
48
+ }
49
+
50
+ function parseTime(value) {
51
+ const raw = text(value);
52
+ if (!raw) return null;
53
+ const ms = Date.parse(raw);
54
+ return Number.isFinite(ms) ? ms : null;
55
+ }
56
+
57
+ /**
58
+ * The newest producer timestamp already recorded in the report. A record minted
59
+ * before this cannot describe the evidence the report now carries.
60
+ *
61
+ * Old-format reports carry stages with no `checked_at` at all; those simply
62
+ * contribute nothing, so an old report yields no freshness floor rather than a
63
+ * fabricated one.
64
+ */
65
+ export function latestProducerTimestamp(report) {
66
+ const stages = isObject(report?.stages) ? report.stages : {};
67
+ let latest = null;
68
+ for (const key of ["doctor", "qa"]) {
69
+ const stage = isObject(stages[key]) ? stages[key] : null;
70
+ if (!stage) continue;
71
+ for (const field of ["checked_at", "completed_at"]) {
72
+ const ms = parseTime(stage[field]);
73
+ if (ms !== null && (latest === null || ms > latest)) latest = ms;
74
+ }
75
+ }
76
+ return latest;
77
+ }
78
+
79
+ // A record can reference several QA verdicts: a run session retains each blocked
80
+ // repair attempt alongside the attempt that finally passed.
81
+ function qaVerdictDigests(record) {
82
+ const artifacts = Array.isArray(record?.artifacts) ? record.artifacts : [];
83
+ return artifacts
84
+ .filter((artifact) => isObject(artifact) && artifact.kind === "qa_verdict")
85
+ .map((artifact) => text(artifact.sha256))
86
+ .filter(Boolean);
87
+ }
88
+
89
+ /**
90
+ * Whether `record` was written for the campaign `packet` describes: same map id
91
+ * and same public route slug. Both sides must actually assert an identity. An
92
+ * identity-less record is not evidence about THIS campaign, so it can never
93
+ * satisfy closeout — and `run-record` never re-emits under its id.
94
+ */
95
+ export function identityMatches(record, packet) {
96
+ const mapId = text(packet?.spec?.map_id);
97
+ const slug = text(packet?.campaign?.public_route_slug);
98
+ const identity = isObject(record?.identity) ? record.identity : {};
99
+ const recordMapId = text(identity.map_id);
100
+ const recordSlug = text(identity.campaign_slug);
101
+ if (!mapId || !slug || !recordMapId || !recordSlug) return false;
102
+ return recordMapId === mapId && recordSlug === slug;
103
+ }
104
+
105
+ function outcome(reason_code, detail, entry = null) {
106
+ return {
107
+ satisfied: reason_code === "satisfied",
108
+ reason_code,
109
+ detail,
110
+ record_id: entry ? text(entry.record?.run_id) : null,
111
+ record_path: entry ? text(entry.path) : null,
112
+ remit_state: entry ? text(entry.record?.remit_state) : null,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Decide whether this packet's closeout is already satisfied by a durable Run
118
+ * Record on disk.
119
+ *
120
+ * @param {object} input
121
+ * @param {Array} input.records `{ path, record }` entries already read from
122
+ * the target's run-records directory. Entries
123
+ * whose `record` is not an object are ignored
124
+ * rather than fatal: one corrupt file must not
125
+ * hide a good record beside it.
126
+ * @param {object} input.packet the Build Packet `next` was called with.
127
+ * @param {object} input.report the Assembly Report on disk.
128
+ * @param {string[]} input.currentQaVerdictDigests SHA-256 of every QA verdict
129
+ * the report's qa stage currently points at.
130
+ * Empty when the caller could not compute one.
131
+ * @param {boolean} input.qaVerdictRecorded whether the qa stage declares a
132
+ * verdict output at all. Old-format reports do
133
+ * not, and must not be punished for it.
134
+ */
135
+ export function assessRunRecordCloseout({
136
+ records = [],
137
+ packet = null,
138
+ report = null,
139
+ currentQaVerdictDigests = [],
140
+ qaVerdictRecorded = false,
141
+ } = {}) {
142
+ const readable = (Array.isArray(records) ? records : []).filter((entry) => isObject(entry?.record));
143
+ if (!readable.length) {
144
+ return outcome("no_record", "No readable Run Record exists for this target; the run has no durable record yet.");
145
+ }
146
+
147
+ const entry = latestMatchingRunRecord(readable, packet);
148
+ if (!entry) {
149
+ return outcome("foreign_campaign", "Run Records exist under this target, but none carries this packet's map id and public route slug.");
150
+ }
151
+ const record = entry.record;
152
+
153
+ const createdAt = parseTime(record.created_at);
154
+ if (createdAt === null) {
155
+ return outcome("stale_predates_evidence", "The newest matching Run Record has no parseable created_at, so it cannot be shown to cover the current evidence.", entry);
156
+ }
157
+ const floor = latestProducerTimestamp(report);
158
+ if (floor !== null && createdAt < floor) {
159
+ return outcome("stale_predates_evidence", "The newest matching Run Record predates the doctor/QA evidence currently recorded in the assembly report.", entry);
160
+ }
161
+
162
+ // Materially outdated = the record does not reference the QA verdict the
163
+ // report points at RIGHT NOW. Deliberately verdict-only: the assembly report's
164
+ // own hash drifts the instant a producer writes a stage, so including it would
165
+ // make every record instantly outdated.
166
+ if (qaVerdictRecorded) {
167
+ const recorded = qaVerdictDigests(record);
168
+ if (!recorded.length) {
169
+ return outcome("outdated_artifacts", "The assembly report records a QA verdict, but the Run Record references no qa_verdict artifact.", entry);
170
+ }
171
+ const current = (Array.isArray(currentQaVerdictDigests) ? currentQaVerdictDigests : []).map(text).filter(Boolean);
172
+ if (!current.length) {
173
+ return outcome("outdated_artifacts", "The QA verdict the assembly report points at could not be read, so it cannot be matched against the Run Record's qa_verdict reference.", entry);
174
+ }
175
+ if (!current.some((digest) => recorded.includes(digest))) {
176
+ return outcome("outdated_artifacts", "The Run Record references a different QA verdict than the one the assembly report's qa stage points at.", entry);
177
+ }
178
+ }
179
+
180
+ const remitState = text(record.remit_state);
181
+ if (remitState === "failed") {
182
+ return outcome("remit_failed", "The Run Record exists locally but its remit failed; recover the existing record rather than minting a second one.", entry);
183
+ }
184
+ if (!remitState || !CLOSED_REMIT_STATES.has(remitState)) {
185
+ return outcome("remit_incomplete", `The Run Record's remit never reached a terminal state (remit_state=${remitState || "absent"}); finish the existing record rather than minting a second one.`, entry);
186
+ }
187
+
188
+ return outcome("satisfied", remitState === "skipped"
189
+ ? "A matching, current Run Record is closed locally (remit skipped: consent off or --no-remit)."
190
+ : "A matching, current Run Record is closed and remitted.", entry);
191
+ }
192
+
193
+ /**
194
+ * The newest Run Record among `records` that belongs to `packet`'s campaign
195
+ * (identityMatches) and names a `run_id`, or null when none does. Newest
196
+ * matching record wins. A record with an unparseable created_at sorts last:
197
+ * it cannot prove it is current, so it is never preferred over one that can.
198
+ * Equal or unparseable timestamps break the tie on the file path, descending,
199
+ * the way orderRunRecordFileNames orders the directory, so the pick is
200
+ * deterministic and never left to the sort's handling of a 0 comparison. The
201
+ * assessor then judges only this entry — so an older good record cannot mask
202
+ * a newer broken one — and `run-record` re-emits under its id when no session
203
+ * names one. A record without a `run_id` is not a candidate for either: it
204
+ * could not have been written by writeRunRecord, and letting it win would
205
+ * hand run-record nothing to re-emit under, silently minting a second record
206
+ * for a campaign that has one.
207
+ *
208
+ * @param {Array} records `{ path, record }` entries; non-object records are
209
+ * ignored.
210
+ * @param {object} packet the Build Packet whose campaign the record must match.
211
+ */
212
+ export function latestMatchingRunRecord(records, packet) {
213
+ const matching = (Array.isArray(records) ? records : [])
214
+ .filter((entry) => isObject(entry?.record) && identityMatches(entry.record, packet) && text(entry.record.run_id) !== null);
215
+ if (!matching.length) return null;
216
+ const sorted = [...matching].sort((a, b) => {
217
+ const byTime = (parseTime(b.record.created_at) ?? -1) - (parseTime(a.record.created_at) ?? -1);
218
+ if (byTime !== 0) return byTime;
219
+ const left = text(a.path) || "";
220
+ const right = text(b.path) || "";
221
+ return left < right ? 1 : left > right ? -1 : 0;
222
+ });
223
+ return sorted[0];
224
+ }
225
+
226
+ /** Reason codes whose remedy is recovering the EXISTING record, not a new one. */
227
+ export function reasonIsRemitRecovery(reasonCode) {
228
+ return reasonCode === "remit_failed" || reasonCode === "remit_incomplete";
229
+ }