@guyghost/swarm-dao-core 0.3.0 → 0.5.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 (304) hide show
  1. package/dist/adapters/attention/fs-attention.store.d.ts +17 -0
  2. package/dist/adapters/attention/fs-attention.store.d.ts.map +1 -0
  3. package/dist/adapters/attention/fs-attention.store.js +59 -0
  4. package/dist/adapters/attention/fs-attention.store.js.map +1 -0
  5. package/dist/adapters/git-workspace.d.ts +26 -0
  6. package/dist/adapters/git-workspace.d.ts.map +1 -0
  7. package/dist/adapters/git-workspace.js +84 -0
  8. package/dist/adapters/git-workspace.js.map +1 -0
  9. package/dist/adapters/index.d.ts +6 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +6 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/persistence/file-dao-state.repository.d.ts +15 -0
  14. package/dist/adapters/persistence/file-dao-state.repository.d.ts.map +1 -0
  15. package/dist/adapters/persistence/file-dao-state.repository.js +85 -0
  16. package/dist/adapters/persistence/file-dao-state.repository.js.map +1 -0
  17. package/dist/adapters/persistence/in-memory-dao-state.repository.d.ts +10 -0
  18. package/dist/adapters/persistence/in-memory-dao-state.repository.d.ts.map +1 -0
  19. package/dist/adapters/persistence/in-memory-dao-state.repository.js +14 -0
  20. package/dist/adapters/persistence/in-memory-dao-state.repository.js.map +1 -0
  21. package/dist/adapters/persistence/legacy-dao-state.repository.d.ts +11 -0
  22. package/dist/adapters/persistence/legacy-dao-state.repository.d.ts.map +1 -0
  23. package/dist/adapters/persistence/legacy-dao-state.repository.js +14 -0
  24. package/dist/adapters/persistence/legacy-dao-state.repository.js.map +1 -0
  25. package/dist/application/index.d.ts +16 -0
  26. package/dist/application/index.d.ts.map +1 -0
  27. package/dist/application/index.js +16 -0
  28. package/dist/application/index.js.map +1 -0
  29. package/dist/application/initialize-dao.use-case.d.ts +20 -0
  30. package/dist/application/initialize-dao.use-case.d.ts.map +1 -0
  31. package/dist/application/initialize-dao.use-case.js +17 -0
  32. package/dist/application/initialize-dao.use-case.js.map +1 -0
  33. package/dist/application/proposals/control-proposal.use-case.d.ts +23 -0
  34. package/dist/application/proposals/control-proposal.use-case.d.ts.map +1 -0
  35. package/dist/application/proposals/control-proposal.use-case.js +53 -0
  36. package/dist/application/proposals/control-proposal.use-case.js.map +1 -0
  37. package/dist/application/proposals/create-amendment-proposal.use-case.d.ts +25 -0
  38. package/dist/application/proposals/create-amendment-proposal.use-case.d.ts.map +1 -0
  39. package/dist/application/proposals/create-amendment-proposal.use-case.js +39 -0
  40. package/dist/application/proposals/create-amendment-proposal.use-case.js.map +1 -0
  41. package/dist/application/proposals/create-proposal.use-case.d.ts +34 -0
  42. package/dist/application/proposals/create-proposal.use-case.d.ts.map +1 -0
  43. package/dist/application/proposals/create-proposal.use-case.js +49 -0
  44. package/dist/application/proposals/create-proposal.use-case.js.map +1 -0
  45. package/dist/application/proposals/deliberate-proposal.use-case.d.ts +35 -0
  46. package/dist/application/proposals/deliberate-proposal.use-case.d.ts.map +1 -0
  47. package/dist/application/proposals/deliberate-proposal.use-case.js +67 -0
  48. package/dist/application/proposals/deliberate-proposal.use-case.js.map +1 -0
  49. package/dist/application/proposals/dry-run-proposal.use-case.d.ts +21 -0
  50. package/dist/application/proposals/dry-run-proposal.use-case.d.ts.map +1 -0
  51. package/dist/application/proposals/dry-run-proposal.use-case.js +20 -0
  52. package/dist/application/proposals/dry-run-proposal.use-case.js.map +1 -0
  53. package/dist/application/proposals/execute-proposal.use-case.d.ts +29 -0
  54. package/dist/application/proposals/execute-proposal.use-case.d.ts.map +1 -0
  55. package/dist/application/proposals/execute-proposal.use-case.js +66 -0
  56. package/dist/application/proposals/execute-proposal.use-case.js.map +1 -0
  57. package/dist/application/proposals/rate-proposal.use-case.d.ts +24 -0
  58. package/dist/application/proposals/rate-proposal.use-case.d.ts.map +1 -0
  59. package/dist/application/proposals/rate-proposal.use-case.js +33 -0
  60. package/dist/application/proposals/rate-proposal.use-case.js.map +1 -0
  61. package/dist/application/proposals/record-deliberation-outputs.use-case.d.ts +21 -0
  62. package/dist/application/proposals/record-deliberation-outputs.use-case.d.ts.map +1 -0
  63. package/dist/application/proposals/record-deliberation-outputs.use-case.js +65 -0
  64. package/dist/application/proposals/record-deliberation-outputs.use-case.js.map +1 -0
  65. package/dist/application/proposals/rollback-proposal.use-case.d.ts +20 -0
  66. package/dist/application/proposals/rollback-proposal.use-case.d.ts.map +1 -0
  67. package/dist/application/proposals/rollback-proposal.use-case.js +17 -0
  68. package/dist/application/proposals/rollback-proposal.use-case.js.map +1 -0
  69. package/dist/application/proposals/round-table.use-case.d.ts +27 -0
  70. package/dist/application/proposals/round-table.use-case.d.ts.map +1 -0
  71. package/dist/application/proposals/round-table.use-case.js +55 -0
  72. package/dist/application/proposals/round-table.use-case.js.map +1 -0
  73. package/dist/application/proposals/ship-proposal.use-case.d.ts +26 -0
  74. package/dist/application/proposals/ship-proposal.use-case.d.ts.map +1 -0
  75. package/dist/application/proposals/ship-proposal.use-case.js +60 -0
  76. package/dist/application/proposals/ship-proposal.use-case.js.map +1 -0
  77. package/dist/application/proposals/start-deliberation.use-case.d.ts +21 -0
  78. package/dist/application/proposals/start-deliberation.use-case.d.ts.map +1 -0
  79. package/dist/application/proposals/start-deliberation.use-case.js +30 -0
  80. package/dist/application/proposals/start-deliberation.use-case.js.map +1 -0
  81. package/dist/application/proposals/transition-proposal.use-case.d.ts +20 -0
  82. package/dist/application/proposals/transition-proposal.use-case.d.ts.map +1 -0
  83. package/dist/application/proposals/transition-proposal.use-case.js +19 -0
  84. package/dist/application/proposals/transition-proposal.use-case.js.map +1 -0
  85. package/dist/application/proposals/update-proposal.use-case.d.ts +26 -0
  86. package/dist/application/proposals/update-proposal.use-case.d.ts.map +1 -0
  87. package/dist/application/proposals/update-proposal.use-case.js +19 -0
  88. package/dist/application/proposals/update-proposal.use-case.js.map +1 -0
  89. package/dist/commands/index.d.ts +2 -0
  90. package/dist/commands/index.d.ts.map +1 -0
  91. package/dist/commands/index.js +2 -0
  92. package/dist/commands/index.js.map +1 -0
  93. package/dist/commands/registry.d.ts +46 -0
  94. package/dist/commands/registry.d.ts.map +1 -0
  95. package/dist/commands/registry.js +367 -0
  96. package/dist/commands/registry.js.map +1 -0
  97. package/dist/config.d.ts +17 -0
  98. package/dist/config.d.ts.map +1 -1
  99. package/dist/config.js +9 -1
  100. package/dist/config.js.map +1 -1
  101. package/dist/control/gates.d.ts +4 -1
  102. package/dist/control/gates.d.ts.map +1 -1
  103. package/dist/control/gates.js +22 -10
  104. package/dist/control/gates.js.map +1 -1
  105. package/dist/delivery/dependencies.d.ts.map +1 -1
  106. package/dist/delivery/dependencies.js +15 -7
  107. package/dist/delivery/dependencies.js.map +1 -1
  108. package/dist/delivery/dry-run.d.ts.map +1 -1
  109. package/dist/delivery/dry-run.js +3 -44
  110. package/dist/delivery/dry-run.js.map +1 -1
  111. package/dist/delivery/execution-isolation.d.ts +34 -0
  112. package/dist/delivery/execution-isolation.d.ts.map +1 -0
  113. package/dist/delivery/execution-isolation.js +97 -0
  114. package/dist/delivery/execution-isolation.js.map +1 -0
  115. package/dist/delivery/execution.d.ts.map +1 -1
  116. package/dist/delivery/execution.js +7 -5
  117. package/dist/delivery/execution.js.map +1 -1
  118. package/dist/delivery/index.d.ts +1 -0
  119. package/dist/delivery/index.d.ts.map +1 -1
  120. package/dist/delivery/index.js +1 -0
  121. package/dist/delivery/index.js.map +1 -1
  122. package/dist/delivery/plans.d.ts +3 -1
  123. package/dist/delivery/plans.d.ts.map +1 -1
  124. package/dist/delivery/plans.js +2 -2
  125. package/dist/delivery/plans.js.map +1 -1
  126. package/dist/domain/dry-run.d.ts +3 -0
  127. package/dist/domain/dry-run.d.ts.map +1 -0
  128. package/dist/domain/dry-run.js +28 -0
  129. package/dist/domain/dry-run.js.map +1 -0
  130. package/dist/domain/index.d.ts +6 -0
  131. package/dist/domain/index.d.ts.map +1 -0
  132. package/dist/domain/index.js +7 -0
  133. package/dist/domain/index.js.map +1 -0
  134. package/dist/governance/agents.d.ts +2 -0
  135. package/dist/governance/agents.d.ts.map +1 -1
  136. package/dist/governance/agents.js +148 -173
  137. package/dist/governance/agents.js.map +1 -1
  138. package/dist/governance/charter.d.ts +14 -0
  139. package/dist/governance/charter.d.ts.map +1 -0
  140. package/dist/governance/charter.js +65 -0
  141. package/dist/governance/charter.js.map +1 -0
  142. package/dist/governance/delegation.machine.d.ts +316 -0
  143. package/dist/governance/delegation.machine.d.ts.map +1 -0
  144. package/dist/governance/delegation.machine.js +387 -0
  145. package/dist/governance/delegation.machine.js.map +1 -0
  146. package/dist/governance/delegation.utils.d.ts +55 -0
  147. package/dist/governance/delegation.utils.d.ts.map +1 -0
  148. package/dist/governance/delegation.utils.js +157 -0
  149. package/dist/governance/delegation.utils.js.map +1 -0
  150. package/dist/governance/edit-gate.d.ts +47 -0
  151. package/dist/governance/edit-gate.d.ts.map +1 -0
  152. package/dist/governance/edit-gate.js +156 -0
  153. package/dist/governance/edit-gate.js.map +1 -0
  154. package/dist/governance/index.d.ts +3 -0
  155. package/dist/governance/index.d.ts.map +1 -1
  156. package/dist/governance/index.js +3 -0
  157. package/dist/governance/index.js.map +1 -1
  158. package/dist/governance/lifecycle.d.ts +0 -6
  159. package/dist/governance/lifecycle.d.ts.map +1 -1
  160. package/dist/governance/lifecycle.js +7 -31
  161. package/dist/governance/lifecycle.js.map +1 -1
  162. package/dist/governance/proposal.machine.d.ts +2 -130
  163. package/dist/governance/proposal.machine.d.ts.map +1 -1
  164. package/dist/governance/proposal.machine.js +2 -377
  165. package/dist/governance/proposal.machine.js.map +1 -1
  166. package/dist/governance/proposal.utils.d.ts +22 -165
  167. package/dist/governance/proposal.utils.d.ts.map +1 -1
  168. package/dist/governance/proposal.utils.js +52 -119
  169. package/dist/governance/proposal.utils.js.map +1 -1
  170. package/dist/governance/scoring.d.ts.map +1 -1
  171. package/dist/governance/scoring.js +36 -16
  172. package/dist/governance/scoring.js.map +1 -1
  173. package/dist/governance/voting.d.ts.map +1 -1
  174. package/dist/governance/voting.js +24 -14
  175. package/dist/governance/voting.js.map +1 -1
  176. package/dist/host-tools/github-config.d.ts.map +1 -1
  177. package/dist/host-tools/github-config.js +17 -5
  178. package/dist/host-tools/github-config.js.map +1 -1
  179. package/dist/host-tools/handlers.d.ts +14 -11
  180. package/dist/host-tools/handlers.d.ts.map +1 -1
  181. package/dist/host-tools/handlers.js +204 -335
  182. package/dist/host-tools/handlers.js.map +1 -1
  183. package/dist/host-tools/messages.d.ts +10 -4
  184. package/dist/host-tools/messages.d.ts.map +1 -1
  185. package/dist/host-tools/messages.js +14 -17
  186. package/dist/host-tools/messages.js.map +1 -1
  187. package/dist/index.d.ts +6 -0
  188. package/dist/index.d.ts.map +1 -1
  189. package/dist/index.js +12 -0
  190. package/dist/index.js.map +1 -1
  191. package/dist/integrations/github.d.ts.map +1 -1
  192. package/dist/integrations/github.js +5 -2
  193. package/dist/integrations/github.js.map +1 -1
  194. package/dist/integrations/http.d.ts +12 -1
  195. package/dist/integrations/http.d.ts.map +1 -1
  196. package/dist/integrations/http.js +114 -17
  197. package/dist/integrations/http.js.map +1 -1
  198. package/dist/intelligence/delegation.d.ts +41 -0
  199. package/dist/intelligence/delegation.d.ts.map +1 -0
  200. package/dist/intelligence/delegation.js +162 -0
  201. package/dist/intelligence/delegation.js.map +1 -0
  202. package/dist/intelligence/index.d.ts +2 -0
  203. package/dist/intelligence/index.d.ts.map +1 -1
  204. package/dist/intelligence/index.js +2 -0
  205. package/dist/intelligence/index.js.map +1 -1
  206. package/dist/intelligence/model.d.ts +28 -2
  207. package/dist/intelligence/model.d.ts.map +1 -1
  208. package/dist/intelligence/model.js +36 -3
  209. package/dist/intelligence/model.js.map +1 -1
  210. package/dist/intelligence/roundtable.d.ts +4 -2
  211. package/dist/intelligence/roundtable.d.ts.map +1 -1
  212. package/dist/intelligence/roundtable.js +2 -2
  213. package/dist/intelligence/roundtable.js.map +1 -1
  214. package/dist/intelligence/sequential.d.ts +36 -0
  215. package/dist/intelligence/sequential.d.ts.map +1 -0
  216. package/dist/intelligence/sequential.js +91 -0
  217. package/dist/intelligence/sequential.js.map +1 -0
  218. package/dist/intelligence/swarm.d.ts +19 -4
  219. package/dist/intelligence/swarm.d.ts.map +1 -1
  220. package/dist/intelligence/swarm.js +107 -53
  221. package/dist/intelligence/swarm.js.map +1 -1
  222. package/dist/intelligence/synthesis.d.ts.map +1 -1
  223. package/dist/intelligence/synthesis.js +16 -4
  224. package/dist/intelligence/synthesis.js.map +1 -1
  225. package/dist/models/graph-engineering.machine.d.ts +219 -0
  226. package/dist/models/graph-engineering.machine.d.ts.map +1 -0
  227. package/dist/models/graph-engineering.machine.js +214 -0
  228. package/dist/models/graph-engineering.machine.js.map +1 -0
  229. package/dist/models/improvement-loop.machine.d.ts +239 -0
  230. package/dist/models/improvement-loop.machine.d.ts.map +1 -0
  231. package/dist/models/improvement-loop.machine.js +254 -0
  232. package/dist/models/improvement-loop.machine.js.map +1 -0
  233. package/dist/models/index.d.ts +5 -0
  234. package/dist/models/index.d.ts.map +1 -0
  235. package/dist/models/index.js +5 -0
  236. package/dist/models/index.js.map +1 -0
  237. package/dist/models/product-loop.machine.d.ts +531 -0
  238. package/dist/models/product-loop.machine.d.ts.map +1 -0
  239. package/dist/models/product-loop.machine.js +699 -0
  240. package/dist/models/product-loop.machine.js.map +1 -0
  241. package/dist/models/proposal.machine.d.ts +82 -0
  242. package/dist/models/proposal.machine.d.ts.map +1 -0
  243. package/dist/models/proposal.machine.js +68 -0
  244. package/dist/models/proposal.machine.js.map +1 -0
  245. package/dist/observability/attention.d.ts +40 -0
  246. package/dist/observability/attention.d.ts.map +1 -0
  247. package/dist/observability/attention.js +119 -0
  248. package/dist/observability/attention.js.map +1 -0
  249. package/dist/observability/index.d.ts +1 -0
  250. package/dist/observability/index.d.ts.map +1 -1
  251. package/dist/observability/index.js +1 -0
  252. package/dist/observability/index.js.map +1 -1
  253. package/dist/observability/logging.d.ts +6 -0
  254. package/dist/observability/logging.d.ts.map +1 -1
  255. package/dist/observability/logging.js +36 -6
  256. package/dist/observability/logging.js.map +1 -1
  257. package/dist/observability/metrics.d.ts +7 -0
  258. package/dist/observability/metrics.d.ts.map +1 -1
  259. package/dist/observability/metrics.js +179 -43
  260. package/dist/observability/metrics.js.map +1 -1
  261. package/dist/persistence.d.ts +5 -6
  262. package/dist/persistence.d.ts.map +1 -1
  263. package/dist/persistence.js +197 -140
  264. package/dist/persistence.js.map +1 -1
  265. package/dist/ports/clock.d.ts +7 -0
  266. package/dist/ports/clock.d.ts.map +1 -0
  267. package/dist/ports/clock.js +5 -0
  268. package/dist/ports/clock.js.map +1 -0
  269. package/dist/ports/host.d.ts +8 -0
  270. package/dist/ports/host.d.ts.map +1 -0
  271. package/dist/ports/host.js +2 -0
  272. package/dist/ports/host.js.map +1 -0
  273. package/dist/ports/index.d.ts +5 -0
  274. package/dist/ports/index.d.ts.map +1 -0
  275. package/dist/ports/index.js +5 -0
  276. package/dist/ports/index.js.map +1 -0
  277. package/dist/ports/repository.d.ts +7 -0
  278. package/dist/ports/repository.d.ts.map +1 -0
  279. package/dist/ports/repository.js +2 -0
  280. package/dist/ports/repository.js.map +1 -0
  281. package/dist/ports/workspace.d.ts +14 -0
  282. package/dist/ports/workspace.d.ts.map +1 -0
  283. package/dist/ports/workspace.js +14 -0
  284. package/dist/ports/workspace.js.map +1 -0
  285. package/dist/presenters/index.d.ts +2 -0
  286. package/dist/presenters/index.d.ts.map +1 -0
  287. package/dist/presenters/index.js +2 -0
  288. package/dist/presenters/index.js.map +1 -0
  289. package/dist/presenters/proposal.presenter.d.ts +30 -0
  290. package/dist/presenters/proposal.presenter.d.ts.map +1 -0
  291. package/dist/presenters/proposal.presenter.js +47 -0
  292. package/dist/presenters/proposal.presenter.js.map +1 -0
  293. package/dist/types/index.d.ts +70 -0
  294. package/dist/types/index.d.ts.map +1 -1
  295. package/dist/types/index.js +12 -0
  296. package/dist/types/index.js.map +1 -1
  297. package/dist/utils/host.d.ts.map +1 -1
  298. package/dist/utils/host.js +77 -6
  299. package/dist/utils/host.js.map +1 -1
  300. package/dist/utils/security.d.ts +7 -1
  301. package/dist/utils/security.d.ts.map +1 -1
  302. package/dist/utils/security.js +18 -2
  303. package/dist/utils/security.js.map +1 -1
  304. package/package.json +13 -1
@@ -0,0 +1,699 @@
1
+ import { assign, createActor, setup } from "xstate";
2
+ export const REQUIRED_PRODUCT_ANCHORS = [
3
+ "qualification-passed",
4
+ "vote-quorum",
5
+ "budget-envelope",
6
+ "controls-passed",
7
+ "auto-ship-policy",
8
+ "observation-window",
9
+ "rollback-path-exists",
10
+ "frozen-set-intact",
11
+ "regression",
12
+ ];
13
+ export const PRODUCT_TERMINAL_STATES = ["validated", "rejected"];
14
+ export const PRODUCT_VOTE_EXPIRY_HOURS = { standard: 72, criticalSecurity: 12 };
15
+ export const PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS = 3;
16
+ export const PRODUCT_MEMBER_QUOTA_RENEWAL_MINUTES = 300;
17
+ export const PRODUCT_ALLOWED_CATEGORIES = ["performance", "refactor", "tooling", "docs", "security"];
18
+ // Anchors auto-sealed by machine actions (the tool never records these).
19
+ // `rollback-path-exists` is NOT auto-sealed: it is an external, tool-recorded
20
+ // anchor so the ship gate trusts a verified artifact rather than a bare draft
21
+ // string (the verifier records it via ANCHOR_RECORDED only after the rollback
22
+ // command confirms the artifact is genuine).
23
+ export const PRODUCT_AUTO_SEALED_ANCHORS = [
24
+ "qualification-passed",
25
+ "vote-quorum",
26
+ "budget-envelope",
27
+ "controls-passed",
28
+ "auto-ship-policy",
29
+ "observation-window",
30
+ ];
31
+ // Anchors the tool records via ANCHOR_RECORDED (the machine never auto-seals these).
32
+ export const PRODUCT_EXTERNAL_ANCHORS = ["frozen-set-intact", "regression", "rollback-path-exists"];
33
+ /**
34
+ * Subset of required anchors that must ALREADY be passed at the VERIFY_EVALUATE
35
+ * ship gate. It deliberately EXCLUDES `observation-window` (sealed only when the
36
+ * observation window completes) and the anchors `sealVerificationAnchors` itself
37
+ * seals when it transitions to `ship` (`controls-passed`, `auto-ship-policy`) —
38
+ * those decisions are re-derived from draft/controls/budget by `canAutoShip`,
39
+ * never read back as their own gate. `rollback-path-exists` IS included because
40
+ * it is now an external, tool-recorded anchor (not sealed by the ship action),
41
+ * so there is no chicken-and-egg: the verifier must have already recorded it.
42
+ */
43
+ export const PRODUCT_SHIP_GATE_ANCHORS = [
44
+ "qualification-passed",
45
+ "vote-quorum",
46
+ "budget-envelope",
47
+ "frozen-set-intact",
48
+ "regression",
49
+ "rollback-path-exists",
50
+ ];
51
+ const isNonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
52
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
53
+ const isProposalDraft = (value) => {
54
+ if (!isRecord(value))
55
+ return false;
56
+ const deps = value.dependencies;
57
+ return (isNonEmpty(value.scope) &&
58
+ PRODUCT_ALLOWED_CATEGORIES.includes(value.category) &&
59
+ typeof value.touchesSensitive === "boolean" &&
60
+ Array.isArray(deps) &&
61
+ deps.every((dep) => isNonEmpty(dep)) &&
62
+ typeof value.budgetAllocation === "number" &&
63
+ value.budgetAllocation >= 0 &&
64
+ isNonEmpty(value.rollbackArtifact) &&
65
+ isNonEmpty(value.evidence));
66
+ };
67
+ export const isRequiredProductAnchor = (value) => typeof value === "string" && REQUIRED_PRODUCT_ANCHORS.includes(value);
68
+ // --- Deterministic policies. Pure. The model — not the tool adapter — decides. ---
69
+ /**
70
+ * Qualification: scope, dependencies, allowed category, affirmative permission
71
+ * clearance, budget presence, and rollback artifact. Permission clearance is an
72
+ * explicit, evidence-backed boolean carried by QUALIFICATION_RUN — qualification
73
+ * never passes on the mere absence of a denial. Sensitive security work may pass
74
+ * (it is auto-votable) but is flagged, which later forbids auto-ship.
75
+ */
76
+ export const qualifyProposal = (draft, permissionsCleared) => {
77
+ if (!draft)
78
+ return { qualified: false, reason: "missing draft" };
79
+ if (!isNonEmpty(draft.scope))
80
+ return { qualified: false, reason: "empty scope" };
81
+ if (!PRODUCT_ALLOWED_CATEGORIES.includes(draft.category)) {
82
+ return { qualified: false, reason: "category not allowed" };
83
+ }
84
+ if (!Array.isArray(draft.dependencies) || draft.dependencies.some((dep) => !isNonEmpty(dep))) {
85
+ return { qualified: false, reason: "unresolvable dependencies" };
86
+ }
87
+ if (!permissionsCleared)
88
+ return { qualified: false, reason: "permissions not cleared" };
89
+ if (draft.budgetAllocation <= 0)
90
+ return { qualified: false, reason: "no budget allocated" };
91
+ if (!isNonEmpty(draft.rollbackArtifact))
92
+ return { qualified: false, reason: "no rollback artifact" };
93
+ return { qualified: true, reason: "scope, dependencies, category, permissions, and budget validated" };
94
+ };
95
+ /** Vote tally: quorum is a minimum favorable-vote threshold, not participation. */
96
+ export const tallyProductVotes = (favorable, config) => {
97
+ if (!config)
98
+ return { quorumReached: false };
99
+ return { quorumReached: config.quorum > 0 && favorable >= config.quorum };
100
+ };
101
+ /** Budget ledger: remaining = initial - consumed; never negative. */
102
+ export const applyBudgetCharge = (envelope, action) => {
103
+ if (!envelope)
104
+ return { initial: 0, consumed: 0, history: [] };
105
+ const consumed = envelope.consumed + Math.max(0, action.amount);
106
+ return {
107
+ initial: envelope.initial,
108
+ consumed,
109
+ history: [...envelope.history, action],
110
+ };
111
+ };
112
+ export const budgetRemaining = (envelope) => {
113
+ if (!envelope)
114
+ return 0;
115
+ return Math.max(0, envelope.initial - envelope.consumed);
116
+ };
117
+ /**
118
+ * Auto-ship gate. Decides deterministically from the draft + controls + budget.
119
+ * Does NOT depend on an anchor it would itself set (no chicken-and-egg).
120
+ * Sensitive security work is forbidden from auto-ship even if reversible.
121
+ */
122
+ export const canAutoShip = (context) => {
123
+ const { draft, controls, budget } = context;
124
+ if (!draft)
125
+ return { allowed: false, reason: "missing draft" };
126
+ const controlValues = Object.values(controls);
127
+ if (controlValues.length === 0)
128
+ return { allowed: false, reason: "no controls recorded" };
129
+ if (controlValues.some((c) => c.status !== "passed"))
130
+ return { allowed: false, reason: "a control failed" };
131
+ if (!PRODUCT_ALLOWED_CATEGORIES.includes(draft.category)) {
132
+ return { allowed: false, reason: "category not allowed for auto-ship" };
133
+ }
134
+ if (draft.category === "security" || draft.touchesSensitive) {
135
+ return { allowed: false, reason: "sensitive change requires human review before deploy" };
136
+ }
137
+ if (!isNonEmpty(draft.rollbackArtifact))
138
+ return { allowed: false, reason: "no rollback artifact" };
139
+ if (budgetRemaining(budget) <= 0)
140
+ return { allowed: false, reason: "budget exhausted" };
141
+ return { allowed: true, reason: "reversible, allowed, rollback exists, budget remains, not sensitive" };
142
+ };
143
+ const isSensitiveDraft = (draft) => draft !== null && (draft.category === "security" || draft.touchesSensitive === true);
144
+ /**
145
+ * Human deploy-authorization gate for SENSITIVE changes only. Auto-ship forbids
146
+ * sensitive work; such a change that otherwise passes verification routes to
147
+ * Review, where the human owner may authorize deployment. This gate requires:
148
+ * the draft is sensitive, every control passed, budget remains, and the
149
+ * ship-gate anchors (including the externally verified rollback path) hold.
150
+ * It can never be exercised by an AI source — REVIEW_RESOLVED is human-only.
151
+ */
152
+ export const canHumanAuthorizeSensitiveDeploy = (context) => {
153
+ const { draft, controls, budget, anchors } = context;
154
+ if (!draft)
155
+ return { allowed: false, reason: "missing draft" };
156
+ if (!isSensitiveDraft(draft))
157
+ return { allowed: false, reason: "deploy-authorized is only for sensitive changes" };
158
+ const controlValues = Object.values(controls);
159
+ if (controlValues.length === 0 || controlValues.some((c) => c.status !== "passed")) {
160
+ return { allowed: false, reason: "every control must pass before deploy" };
161
+ }
162
+ if (budgetRemaining(budget) <= 0)
163
+ return { allowed: false, reason: "budget exhausted" };
164
+ if (!shipGateAnchorsPassed(anchors))
165
+ return { allowed: false, reason: "ship-gate anchors not passed" };
166
+ return { allowed: true, reason: "human-authorized deploy of sensitive, verified, reversible change" };
167
+ };
168
+ /**
169
+ * Observation gate. Priorities: errors > aiCost > latency. Satisfaction is a
170
+ * signal only and can NEVER confirm degradation. A single measurement never
171
+ * confirms; require N consecutive threshold-crossing measurements on the
172
+ * highest-priority breached metric.
173
+ */
174
+ export const evaluateObservation = (samples, _windowElapsed) => {
175
+ const priorities = ["errors", "aiCost", "latency"];
176
+ for (const metric of priorities) {
177
+ const forMetric = samples.filter((s) => s.metric === metric);
178
+ if (forMetric.length < PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS)
179
+ continue;
180
+ const tail = forMetric.slice(-PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS);
181
+ if (tail.every((s) => s.exceeded))
182
+ return { confirmed: true, metric };
183
+ }
184
+ return { confirmed: false, metric: null };
185
+ };
186
+ /** True only when every required anchor is passed. Terminal invariant at `validated`. */
187
+ export const allRequiredAnchorsPassed = (anchors) => REQUIRED_PRODUCT_ANCHORS.every((anchor) => anchors[anchor]?.status === "passed" && isNonEmpty(anchors[anchor]?.evidence));
188
+ /** True only when every ship-gate anchor is passed. Used at the VERIFY_EVALUATE ship gate. */
189
+ export const shipGateAnchorsPassed = (anchors) => PRODUCT_SHIP_GATE_ANCHORS.every((anchor) => anchors[anchor]?.status === "passed" && isNonEmpty(anchors[anchor]?.evidence));
190
+ const initialContext = (input) => ({
191
+ runId: input.runId,
192
+ proposalId: input.proposalId ?? null,
193
+ improvementCycleId: input.improvementCycleId ?? null,
194
+ draft: null,
195
+ voteConfig: null,
196
+ favorableVotes: 0,
197
+ budget: null,
198
+ controls: {},
199
+ observationSamples: [],
200
+ contactVoteOpen: false,
201
+ contactVoteQuorumReached: false,
202
+ contactRelayAuthorized: false,
203
+ reviewReason: null,
204
+ permissionsCleared: false,
205
+ permissionEvidence: null,
206
+ signalLog: [],
207
+ anchors: {},
208
+ terminalReason: null,
209
+ });
210
+ const productSetup = setup({
211
+ types: {
212
+ context: {},
213
+ events: {},
214
+ input: {},
215
+ },
216
+ guards: {
217
+ isAiAgentSignal: ({ event }) => event.type === "AGENT_SIGNAL" && event.source === "ai" && isNonEmpty(event.note),
218
+ isAiFeedbackAggregated: ({ event }) => event.type === "FEEDBACK_AGGREGATED" && event.source === "ai" && isNonEmpty(event.summary),
219
+ isAiProposalDrafted: ({ event }) => event.type === "PROPOSAL_DRAFTED" && event.source === "ai" && isProposalDraft(event.draft),
220
+ isToolOpenProposition: ({ context, event }) => event.type === "OPEN_PROPOSITION" && event.source === "tool" && context.draft !== null,
221
+ isToolQualificationRun: ({ event }) => event.type === "QUALIFICATION_RUN" &&
222
+ event.source === "tool" &&
223
+ event.permissionCleared === true &&
224
+ isNonEmpty(event.permissionEvidence),
225
+ isToolVoteOpenedFirstTime: ({ context }) => context.voteConfig === null,
226
+ isToolVoteOpened: ({ event }) => event.type === "VOTE_OPENED" &&
227
+ event.source === "tool" &&
228
+ event.config.quorum > 0 &&
229
+ (event.config.kind === "standard" || event.config.kind === "criticalSecurity") &&
230
+ event.config.expiryHours === PRODUCT_VOTE_EXPIRY_HOURS[event.config.kind],
231
+ isToolVoteCast: ({ event }) => event.type === "VOTE_CAST" && event.source === "tool" && event.favorable > 0,
232
+ isSystemVoteEvaluateWithQuorum: ({ context, event }) => event.type === "VOTE_EVALUATE" &&
233
+ event.source === "system" &&
234
+ tallyProductVotes(context.favorableVotes, context.voteConfig).quorumReached,
235
+ isToolVoteExpiredWithQuorum: ({ context, event }) => event.type === "VOTE_EXPIRED" &&
236
+ event.source === "tool" &&
237
+ tallyProductVotes(context.favorableVotes, context.voteConfig).quorumReached,
238
+ isToolVoteExpiredWithoutQuorum: ({ context, event }) => event.type === "VOTE_EXPIRED" &&
239
+ event.source === "tool" &&
240
+ !tallyProductVotes(context.favorableVotes, context.voteConfig).quorumReached,
241
+ isToolBudgetCharge: ({ event }) => event.type === "BUDGET_CHARGE" &&
242
+ event.source === "tool" &&
243
+ isNonEmpty(event.action.description) &&
244
+ isNonEmpty(event.action.evidence) &&
245
+ event.action.amount >= 0,
246
+ chargeExhaustsBudget: ({ context, event }) => event.type === "BUDGET_CHARGE" &&
247
+ event.source === "tool" &&
248
+ budgetRemaining(applyBudgetCharge(context.budget, event.action)) <= 0,
249
+ isToolExecutionDone: ({ context, event }) => event.type === "EXECUTION_DONE" && event.source === "tool" && budgetRemaining(context.budget) > 0,
250
+ isToolVerifyRun: ({ event }) => event.type === "VERIFY_RUN" &&
251
+ event.source === "tool" &&
252
+ isNonEmpty(event.control.name) &&
253
+ (event.control.status === "passed" || event.control.status === "failed") &&
254
+ isNonEmpty(event.control.evidence),
255
+ canShip: ({ context }) => canAutoShip({ draft: context.draft, controls: context.controls, budget: context.budget }).allowed &&
256
+ shipGateAnchorsPassed(context.anchors),
257
+ isSystemVerifyEvaluate: ({ event }) => event.type === "VERIFY_EVALUATE" && event.source === "system",
258
+ isToolObservationSample: ({ event }) => event.type === "OBSERVATION_SAMPLE" &&
259
+ event.source === "tool" &&
260
+ ["errors", "aiCost", "latency", "satisfaction"].includes(event.sample.metric) &&
261
+ typeof event.sample.value === "number" &&
262
+ typeof event.sample.threshold === "number" &&
263
+ typeof event.sample.exceeded === "boolean" &&
264
+ isNonEmpty(event.sample.evidence),
265
+ observationDegraded: ({ context, event }) => event.type === "OBSERVATION_EVALUATE" &&
266
+ evaluateObservation(context.observationSamples, event.windowElapsed).confirmed,
267
+ observationClean: ({ context, event }) => event.type === "OBSERVATION_EVALUATE" &&
268
+ context.observationSamples.length >= PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS &&
269
+ !evaluateObservation(context.observationSamples, event.windowElapsed).confirmed &&
270
+ event.windowElapsed,
271
+ isValidExternalAnchor: ({ event }) => event.type === "ANCHOR_RECORDED" &&
272
+ event.source === "tool" &&
273
+ PRODUCT_EXTERNAL_ANCHORS.includes(event.anchor) &&
274
+ (event.status === "passed" || event.status === "failed") &&
275
+ isNonEmpty(event.evidence),
276
+ isToolCorrectivePropositionOpened: ({ event }) => event.type === "CORRECTIVE_PROPOSITION_OPENED" && event.source === "tool",
277
+ isHumanReviewResolved: ({ event }) => event.type === "REVIEW_RESOLVED" &&
278
+ event.source === "human" &&
279
+ (event.resolution === "scope-reduced" ||
280
+ event.resolution === "budget-expanded" ||
281
+ event.resolution === "abandoned"),
282
+ isHumanDeployAuthorized: ({ context, event }) => event.type === "REVIEW_RESOLVED" &&
283
+ event.source === "human" &&
284
+ event.resolution === "deploy-authorized" &&
285
+ canHumanAuthorizeSensitiveDeploy({
286
+ draft: context.draft,
287
+ controls: context.controls,
288
+ budget: context.budget,
289
+ anchors: context.anchors,
290
+ }).allowed,
291
+ isHumanRetryVerification: ({ event }) => event.type === "RETRY_VERIFICATION_AUTHORIZED" && event.source === "human",
292
+ isToolContactVoteOpened: ({ event }) => event.type === "CONTACT_VOTE_OPENED" && event.source === "tool",
293
+ isToolContactVoteQuorum: ({ event }) => event.type === "CONTACT_VOTE_QUORUM" && event.source === "tool" && typeof event.reached === "boolean",
294
+ isHumanContactRelayAuthorized: ({ context, event }) => event.type === "CONTACT_RELAY_AUTHORIZED" &&
295
+ event.source === "human" &&
296
+ context.contactVoteQuorumReached === true,
297
+ isToolPermissionDenial: ({ event }) => event.type === "PERMISSION_DENIED" && event.source === "tool" && isNonEmpty(event.reason),
298
+ isHumanCancellation: ({ event }) => event.type === "CANCEL" && event.source === "human" && isNonEmpty(event.reason),
299
+ },
300
+ actions: {
301
+ recordDraft: assign(({ context, event }) => event.type === "PROPOSAL_DRAFTED" ? { ...context, draft: event.draft } : context),
302
+ recordSignal: assign(({ context, event }) => {
303
+ // Signal-only events (AGENT_SIGNAL, FEEDBACK_AGGREGATED) carry no state
304
+ // target; they are recorded as traceable product signal evidence so the
305
+ // persisted snapshot reflects that the machine accepted them. This keeps
306
+ // feedback/signal history queryable and makes acceptance observable to the
307
+ // journal-based runner without coupling it to guard internals.
308
+ if (event.type !== "AGENT_SIGNAL" && event.type !== "FEEDBACK_AGGREGATED")
309
+ return context;
310
+ const entry = event.type === "AGENT_SIGNAL" ? `agent-signal: ${event.note}` : `feedback-aggregated: ${event.summary}`;
311
+ return { ...context, signalLog: [...context.signalLog, entry] };
312
+ }),
313
+ recordPermission: assign(({ context, event }) => event.type === "QUALIFICATION_RUN"
314
+ ? {
315
+ ...context,
316
+ permissionsCleared: event.permissionCleared,
317
+ permissionEvidence: event.permissionEvidence,
318
+ }
319
+ : context),
320
+ sealQualificationPassed: assign(({ context }) => ({
321
+ ...context,
322
+ anchors: {
323
+ ...context.anchors,
324
+ "qualification-passed": {
325
+ status: "passed",
326
+ evidence: `qualifier: scope, dependencies, category, permissions, budget validated (${context.permissionEvidence ?? "none"})`,
327
+ },
328
+ },
329
+ })),
330
+ recordVoteConfig: assign(({ context, event }) => event.type === "VOTE_OPENED" ? { ...context, voteConfig: event.config, favorableVotes: 0 } : context),
331
+ recordVote: assign(({ context, event }) => event.type === "VOTE_CAST" ? { ...context, favorableVotes: context.favorableVotes + event.favorable } : context),
332
+ sealVoteQuorum: assign(({ context }) => ({
333
+ ...context,
334
+ anchors: {
335
+ ...context.anchors,
336
+ "vote-quorum": {
337
+ status: "passed",
338
+ evidence: `quorum ${context.voteConfig?.quorum} reached with ${context.favorableVotes} favorable`,
339
+ },
340
+ },
341
+ })),
342
+ openBudget: assign(({ context }) => {
343
+ const allocation = context.draft?.budgetAllocation ?? 0;
344
+ return {
345
+ ...context,
346
+ budget: { initial: allocation, consumed: 0, history: [] },
347
+ anchors: {
348
+ ...context.anchors,
349
+ "budget-envelope": { status: "passed", evidence: `envelope opened at ${allocation}` },
350
+ },
351
+ };
352
+ }),
353
+ chargeBudget: assign(({ context, event }) => {
354
+ if (event.type !== "BUDGET_CHARGE")
355
+ return context;
356
+ return { ...context, budget: applyBudgetCharge(context.budget, event.action) };
357
+ }),
358
+ recordControl: assign(({ context, event }) => {
359
+ if (event.type !== "VERIFY_RUN")
360
+ return context;
361
+ return { ...context, controls: { ...context.controls, [event.control.name]: event.control } };
362
+ }),
363
+ sealVerificationAnchors: assign(({ context }) => {
364
+ const decision = canAutoShip({ draft: context.draft, controls: context.controls, budget: context.budget });
365
+ return {
366
+ ...context,
367
+ anchors: {
368
+ ...context.anchors,
369
+ "controls-passed": {
370
+ status: "passed",
371
+ evidence: `${Object.keys(context.controls).length} controls passed`,
372
+ },
373
+ "auto-ship-policy": {
374
+ status: decision.allowed ? "passed" : "failed",
375
+ evidence: decision.reason,
376
+ },
377
+ // NOTE: `rollback-path-exists` is NOT sealed here. It is an external
378
+ // anchor recorded by the verifier tool (product:anchors) via
379
+ // ANCHOR_RECORDED after the rollback artifact is confirmed genuine.
380
+ },
381
+ };
382
+ }),
383
+ sealHumanAuthorizedDeploy: assign(({ context }) => ({
384
+ ...context,
385
+ anchors: {
386
+ ...context.anchors,
387
+ "controls-passed": {
388
+ status: "passed",
389
+ evidence: `${Object.keys(context.controls).length} controls passed (human deploy review)`,
390
+ },
391
+ "auto-ship-policy": {
392
+ status: "passed",
393
+ evidence: "sensitive change deploy authorized by human owner review; auto-ship does not apply",
394
+ },
395
+ },
396
+ })),
397
+ recordObservationSample: assign(({ context, event }) => {
398
+ if (event.type !== "OBSERVATION_SAMPLE")
399
+ return context;
400
+ // Satisfaction is recorded as signal only; it can never confirm degradation.
401
+ return { ...context, observationSamples: [...context.observationSamples, event.sample] };
402
+ }),
403
+ sealObservationWindow: assign(({ context }) => ({
404
+ ...context,
405
+ anchors: {
406
+ ...context.anchors,
407
+ "observation-window": {
408
+ status: "passed",
409
+ evidence: `${context.observationSamples.length} samples recorded`,
410
+ },
411
+ },
412
+ })),
413
+ recordExternalAnchorOnce: assign(({ context, event }) => {
414
+ if (event.type !== "ANCHOR_RECORDED")
415
+ return context;
416
+ if (context.anchors[event.anchor] !== undefined)
417
+ return context; // failed anchors are immutable per run
418
+ return {
419
+ ...context,
420
+ anchors: {
421
+ ...context.anchors,
422
+ [event.anchor]: { status: event.status, evidence: event.evidence },
423
+ },
424
+ };
425
+ }),
426
+ prepareCorrectiveProposition: assign(({ context }) => ({
427
+ ...context,
428
+ // A corrective/re-scope proposal starts fresh in proposition; prior
429
+ // attempt-scoped evidence is cleared. Draft must be re-submitted and
430
+ // re-qualified; no anchor carries over.
431
+ draft: null,
432
+ voteConfig: null,
433
+ favorableVotes: 0,
434
+ controls: {},
435
+ observationSamples: [],
436
+ anchors: {},
437
+ })),
438
+ recordReviewResolution: assign(({ context }) => ({ ...context })),
439
+ recordBudgetBlockedEntry: assign(({ context }) => ({
440
+ ...context,
441
+ reviewReason: "budget-exhausted",
442
+ })),
443
+ expandBudget: assign(({ context, event }) => {
444
+ if (event.type !== "REVIEW_RESOLVED" || typeof event.expandedBudget !== "number")
445
+ return context;
446
+ const consumed = context.budget?.consumed ?? 0;
447
+ const initial = Math.max(event.expandedBudget, consumed);
448
+ return {
449
+ ...context,
450
+ budget: context.budget ? { ...context.budget, initial } : { initial, consumed: 0, history: [] },
451
+ };
452
+ }),
453
+ resetControlsForRetry: assign(({ context }) => ({
454
+ ...context,
455
+ controls: {},
456
+ anchors: { ...context.anchors },
457
+ })),
458
+ recordContactVoteOpened: assign(({ context }) => ({ ...context, contactVoteOpen: true })),
459
+ recordContactVoteQuorum: assign(({ context, event }) => event.type === "CONTACT_VOTE_QUORUM" ? { ...context, contactVoteQuorumReached: event.reached } : context),
460
+ recordContactRelay: assign(({ context }) => ({ ...context, contactRelayAuthorized: true })),
461
+ recordTerminalRejected: assign(({ context }) => ({
462
+ ...context,
463
+ terminalReason: "vote expired without quorum",
464
+ })),
465
+ recordTerminalCancelled: assign(({ context }) => ({
466
+ ...context,
467
+ terminalReason: "run cancelled by owner",
468
+ })),
469
+ recordTerminalValidated: assign(({ context }) => ({
470
+ ...context,
471
+ terminalReason: "observation completed without confirmed degradation",
472
+ })),
473
+ },
474
+ });
475
+ export const productMachine = productSetup.createMachine({
476
+ id: "swarm-dao-product-loop",
477
+ initial: "exploration",
478
+ context: ({ input }) => initialContext(input),
479
+ on: {
480
+ PERMISSION_DENIED: { guard: "isToolPermissionDenial", target: ".review" },
481
+ CANCEL: { guard: "isHumanCancellation", target: ".rejected", actions: "recordTerminalCancelled" },
482
+ CONTACT_VOTE_OPENED: { guard: "isToolContactVoteOpened", actions: "recordContactVoteOpened" },
483
+ CONTACT_VOTE_QUORUM: { guard: "isToolContactVoteQuorum", actions: "recordContactVoteQuorum" },
484
+ CONTACT_RELAY_AUTHORIZED: {
485
+ guard: "isHumanContactRelayAuthorized",
486
+ actions: "recordContactRelay",
487
+ },
488
+ // NOTE: ANCHOR_RECORDED is intentionally NOT global. External anchors
489
+ // (frozen-set-intact, regression, rollback-path-exists) are only meaningful
490
+ // and only accepted while the machine is in the `verification` state, where
491
+ // the verifier tool records them. Accepting them globally would let a
492
+ // verifier seal regression/frozen-set before any implementation exists.
493
+ },
494
+ states: {
495
+ exploration: {
496
+ on: {
497
+ AGENT_SIGNAL: { guard: "isAiAgentSignal", actions: "recordSignal" },
498
+ FEEDBACK_AGGREGATED: { guard: "isAiFeedbackAggregated", actions: "recordSignal" },
499
+ PROPOSAL_DRAFTED: { guard: "isAiProposalDrafted", actions: "recordDraft" },
500
+ OPEN_PROPOSITION: { guard: "isToolOpenProposition", target: "proposition" },
501
+ },
502
+ },
503
+ proposition: {
504
+ on: {
505
+ // A corrective/re-scope flow clears the draft on entry to proposition
506
+ // (prepareCorrectiveProposition). The replacement draft must be recorded
507
+ // here before qualification can run, exactly as in exploration.
508
+ PROPOSAL_DRAFTED: { guard: "isAiProposalDrafted", actions: "recordDraft" },
509
+ QUALIFICATION_RUN: {
510
+ guard: "isToolQualificationRun",
511
+ target: "qualification",
512
+ actions: "recordPermission",
513
+ },
514
+ },
515
+ },
516
+ qualification: {
517
+ always: [
518
+ {
519
+ guard: ({ context }) => qualifyProposal(context.draft, context.permissionsCleared).qualified,
520
+ target: "vote",
521
+ actions: "sealQualificationPassed",
522
+ },
523
+ { target: "review" },
524
+ ],
525
+ },
526
+ vote: {
527
+ on: {
528
+ VOTE_OPENED: {
529
+ guard: ({ context, event }) => context.voteConfig === null &&
530
+ event.type === "VOTE_OPENED" &&
531
+ event.source === "tool" &&
532
+ event.config.quorum > 0 &&
533
+ (event.config.kind === "standard" || event.config.kind === "criticalSecurity") &&
534
+ event.config.expiryHours === PRODUCT_VOTE_EXPIRY_HOURS[event.config.kind],
535
+ actions: "recordVoteConfig",
536
+ },
537
+ VOTE_CAST: { guard: "isToolVoteCast", actions: "recordVote" },
538
+ VOTE_EVALUATE: {
539
+ guard: "isSystemVoteEvaluateWithQuorum",
540
+ target: "adopted",
541
+ actions: "sealVoteQuorum",
542
+ },
543
+ // Expiry is deterministic: if quorum was already reached when the expiry
544
+ // event arrives, the task is adopted (and the quorum anchor sealed); only
545
+ // expiry WITHOUT quorum rejects. This prevents a late expiry from killing
546
+ // an already-quorate task whose EVALUATE had not yet been emitted.
547
+ VOTE_EXPIRED: [
548
+ {
549
+ guard: "isToolVoteExpiredWithQuorum",
550
+ target: "adopted",
551
+ actions: "sealVoteQuorum",
552
+ },
553
+ {
554
+ guard: "isToolVoteExpiredWithoutQuorum",
555
+ target: "rejected",
556
+ actions: "recordTerminalRejected",
557
+ },
558
+ ],
559
+ },
560
+ },
561
+ adopted: {
562
+ always: [{ target: "execution", actions: "openBudget" }],
563
+ },
564
+ execution: {
565
+ on: {
566
+ BUDGET_CHARGE: [
567
+ {
568
+ guard: "chargeExhaustsBudget",
569
+ target: "budgetBlocked",
570
+ actions: "chargeBudget",
571
+ },
572
+ { guard: "isToolBudgetCharge", actions: "chargeBudget" },
573
+ ],
574
+ EXECUTION_DONE: { guard: "isToolExecutionDone", target: "verification" },
575
+ },
576
+ },
577
+ budgetBlocked: {
578
+ // Budget exhaustion has only one deterministic escape: escalate to human
579
+ // review. This is a model-mandated transition (not an agent decision): no
580
+ // contributor swap, retry, or bypass may skip review. The human owner then
581
+ // resolves via REVIEW_RESOLVED (expand budget / reduce scope / abandon) in
582
+ // the review state. The `always` makes the escalation immediate so the
583
+ // persisted snapshot records the budget block as the cause entering review.
584
+ always: [{ target: "review", actions: "recordBudgetBlockedEntry" }],
585
+ },
586
+ verification: {
587
+ on: {
588
+ VERIFY_RUN: { guard: "isToolVerifyRun", actions: "recordControl" },
589
+ // External anchors (frozen-set-intact, regression, rollback-path-exists)
590
+ // are recorded ONLY in verification by the verifier tool. `rollback-path-
591
+ // exists` is a ship-gate prerequisite, so it must be recorded before
592
+ // VERIFY_EVALUATE selects ship.
593
+ ANCHOR_RECORDED: { guard: "isValidExternalAnchor", actions: "recordExternalAnchorOnce" },
594
+ VERIFY_EVALUATE: [
595
+ {
596
+ guard: ({ context, event }) => event.type === "VERIFY_EVALUATE" &&
597
+ event.source === "system" &&
598
+ canAutoShip({ draft: context.draft, controls: context.controls, budget: context.budget }).allowed &&
599
+ shipGateAnchorsPassed(context.anchors),
600
+ target: "ship",
601
+ actions: "sealVerificationAnchors",
602
+ },
603
+ {
604
+ guard: "isSystemVerifyEvaluate",
605
+ target: "review",
606
+ actions: "sealVerificationAnchors",
607
+ },
608
+ ],
609
+ },
610
+ },
611
+ ship: {
612
+ always: [{ target: "observation" }],
613
+ },
614
+ observation: {
615
+ on: {
616
+ OBSERVATION_SAMPLE: { guard: "isToolObservationSample", actions: "recordObservationSample" },
617
+ OBSERVATION_EVALUATE: [
618
+ {
619
+ guard: "observationDegraded",
620
+ target: "rollback",
621
+ },
622
+ {
623
+ guard: "observationClean",
624
+ target: "validated",
625
+ actions: ["sealObservationWindow", "recordTerminalValidated"],
626
+ },
627
+ ],
628
+ },
629
+ },
630
+ rollback: {
631
+ on: {
632
+ CORRECTIVE_PROPOSITION_OPENED: {
633
+ guard: "isToolCorrectivePropositionOpened",
634
+ target: "proposition",
635
+ actions: "prepareCorrectiveProposition",
636
+ },
637
+ },
638
+ },
639
+ review: {
640
+ on: {
641
+ REVIEW_RESOLVED: [
642
+ {
643
+ guard: ({ event }) => event.type === "REVIEW_RESOLVED" && event.source === "human" && event.resolution === "scope-reduced",
644
+ target: "proposition",
645
+ actions: "prepareCorrectiveProposition",
646
+ },
647
+ {
648
+ // Budget expansion is the ONLY escape from BudgetBlocked, and only
649
+ // when the review was actually entered because of budget exhaustion.
650
+ // This prevents any other review cause (qualification failure,
651
+ // permission denial, verification failure) from using budget-expanded
652
+ // to jump straight to execution, bypassing qualification and vote. The
653
+ // human review must declare a strictly larger envelope than what has
654
+ // already been consumed; otherwise the task would immediately
655
+ // re-block. Consumed spend and history are preserved.
656
+ guard: ({ context, event }) => event.type === "REVIEW_RESOLVED" &&
657
+ event.source === "human" &&
658
+ event.resolution === "budget-expanded" &&
659
+ context.reviewReason === "budget-exhausted" &&
660
+ typeof event.expandedBudget === "number" &&
661
+ Number.isFinite(event.expandedBudget) &&
662
+ event.expandedBudget > (context.budget?.consumed ?? 0),
663
+ target: "execution",
664
+ actions: "expandBudget",
665
+ },
666
+ {
667
+ // Human deploy-authorization: the ONLY path to ship for sensitive
668
+ // changes (security/permissions/secrets/payments/sensitive-data) that
669
+ // passed verification but are forbidden from auto-ship. The
670
+ // isHumanDeployAuthorized guard re-checks controls, budget, and the
671
+ // ship-gate anchors (including the externally verified rollback
672
+ // path). No AI source may emit this.
673
+ guard: "isHumanDeployAuthorized",
674
+ target: "ship",
675
+ actions: "sealHumanAuthorizedDeploy",
676
+ },
677
+ {
678
+ guard: ({ event }) => event.type === "REVIEW_RESOLVED" && event.source === "human" && event.resolution === "abandoned",
679
+ target: "rejected",
680
+ actions: "recordReviewResolution",
681
+ },
682
+ ],
683
+ RETRY_VERIFICATION_AUTHORIZED: {
684
+ guard: "isHumanRetryVerification",
685
+ target: "verification",
686
+ actions: "resetControlsForRetry",
687
+ },
688
+ },
689
+ },
690
+ validated: { type: "final" },
691
+ rejected: { type: "final" },
692
+ },
693
+ });
694
+ export const createProductActor = (input) => {
695
+ const actor = createActor(productMachine, { input });
696
+ actor.start();
697
+ return actor;
698
+ };
699
+ //# sourceMappingURL=product-loop.machine.js.map