@intentius/chant 0.91.0 → 0.93.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 (344) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/graph.d.ts.map +1 -1
  4. package/dist/cli/handlers/operator.d.ts +6 -0
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  10. package/dist/cli/plugins.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +12 -2
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/lexicon-module.d.ts +22 -0
  17. package/dist/lexicon-module.d.ts.map +1 -1
  18. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  19. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  20. package/dist/lifecycle/run-ledger.d.ts +2 -0
  21. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  22. package/dist/lifecycle/symptoms.d.ts +51 -0
  23. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  24. package/dist/lint/rules/index.d.ts +4 -2
  25. package/dist/lint/rules/index.d.ts.map +1 -1
  26. package/dist/lint/rules/op/index.d.ts +14 -3
  27. package/dist/lint/rules/op/index.d.ts.map +1 -1
  28. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  29. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  31. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  33. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  35. package/dist/op/activities/activity-contracts.d.ts +201 -0
  36. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  37. package/dist/op/activities/apply.d.ts.map +1 -1
  38. package/dist/op/activities/change-coverage.d.ts +44 -0
  39. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  40. package/dist/op/activities/converge.d.ts +24 -1
  41. package/dist/op/activities/converge.d.ts.map +1 -1
  42. package/dist/op/activities/decide.d.ts +107 -0
  43. package/dist/op/activities/decide.d.ts.map +1 -0
  44. package/dist/op/activities/index.d.ts +10 -0
  45. package/dist/op/activities/index.d.ts.map +1 -1
  46. package/dist/op/activities/shell.d.ts +32 -0
  47. package/dist/op/activities/shell.d.ts.map +1 -1
  48. package/dist/op/activities/source-release.d.ts +86 -0
  49. package/dist/op/activities/source-release.d.ts.map +1 -0
  50. package/dist/op/activities/source-rollback.d.ts +100 -0
  51. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  52. package/dist/op/activities/work-evidence.d.ts +42 -0
  53. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  54. package/dist/op/activity-contract.d.ts +1 -0
  55. package/dist/op/activity-contract.d.ts.map +1 -1
  56. package/dist/op/activity-profiles.d.ts +16 -0
  57. package/dist/op/activity-profiles.d.ts.map +1 -1
  58. package/dist/op/builders.d.ts +84 -2
  59. package/dist/op/builders.d.ts.map +1 -1
  60. package/dist/op/composites/converge-op.d.ts +31 -4
  61. package/dist/op/composites/converge-op.d.ts.map +1 -1
  62. package/dist/op/decide-backend.d.ts +116 -0
  63. package/dist/op/decide-backend.d.ts.map +1 -0
  64. package/dist/op/decide-config.d.ts +120 -0
  65. package/dist/op/decide-config.d.ts.map +1 -0
  66. package/dist/op/decide-read-inputs.d.ts +40 -0
  67. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  68. package/dist/op/gate-wait.d.ts +43 -0
  69. package/dist/op/gate-wait.d.ts.map +1 -0
  70. package/dist/op/gate.d.ts +5 -2
  71. package/dist/op/gate.d.ts.map +1 -1
  72. package/dist/op/index.d.ts +11 -4
  73. package/dist/op/index.d.ts.map +1 -1
  74. package/dist/op/local-executor.d.ts +29 -4
  75. package/dist/op/local-executor.d.ts.map +1 -1
  76. package/dist/op/local-output.d.ts +2 -0
  77. package/dist/op/local-output.d.ts.map +1 -1
  78. package/dist/op/op-ir.d.ts +2 -0
  79. package/dist/op/op-ir.d.ts.map +1 -1
  80. package/dist/op/op-verb-class.d.ts.map +1 -1
  81. package/dist/op/operator.d.ts +97 -1
  82. package/dist/op/operator.d.ts.map +1 -1
  83. package/dist/op/runtime.d.ts +37 -5
  84. package/dist/op/runtime.d.ts.map +1 -1
  85. package/dist/op/runtimes/local.d.ts.map +1 -1
  86. package/dist/op/source-archive.d.ts +63 -0
  87. package/dist/op/source-archive.d.ts.map +1 -0
  88. package/dist/op/steward-points.d.ts +136 -0
  89. package/dist/op/steward-points.d.ts.map +1 -0
  90. package/dist/op/steward-turn.d.ts +53 -0
  91. package/dist/op/steward-turn.d.ts.map +1 -0
  92. package/dist/op/steward.d.ts +22 -1
  93. package/dist/op/steward.d.ts.map +1 -1
  94. package/dist/op/types.d.ts +7 -0
  95. package/dist/op/types.d.ts.map +1 -1
  96. package/dist/op/work-lease-run.d.ts +1 -1
  97. package/dist/op/work-lease-run.d.ts.map +1 -1
  98. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  100. package/dist/workspace/answers-ledger.d.ts +76 -0
  101. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  102. package/dist/workspace/changes-cli.d.ts +16 -0
  103. package/dist/workspace/changes-cli.d.ts.map +1 -0
  104. package/dist/workspace/changes.d.ts +200 -0
  105. package/dist/workspace/changes.d.ts.map +1 -0
  106. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  107. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  108. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  118. package/dist/workspace/chant-migrations.d.ts +99 -0
  119. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  120. package/dist/workspace/checks/boxes.d.ts +34 -1
  121. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  122. package/dist/workspace/checks/diagrams.d.ts +31 -0
  123. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  124. package/dist/workspace/checks/records.d.ts +7 -0
  125. package/dist/workspace/checks/records.d.ts.map +1 -1
  126. package/dist/workspace/checks.d.ts +3 -0
  127. package/dist/workspace/checks.d.ts.map +1 -1
  128. package/dist/workspace/compose-graph.d.ts +2 -0
  129. package/dist/workspace/compose-graph.d.ts.map +1 -1
  130. package/dist/workspace/conformance/index.d.ts.map +1 -1
  131. package/dist/workspace/decide.d.ts +49 -4
  132. package/dist/workspace/decide.d.ts.map +1 -1
  133. package/dist/workspace/declaration.d.ts +52 -0
  134. package/dist/workspace/declaration.d.ts.map +1 -1
  135. package/dist/workspace/declaration.schema.json +114 -1
  136. package/dist/workspace/declared-kinds.d.ts +20 -0
  137. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  138. package/dist/workspace/generated-files.d.ts +7 -0
  139. package/dist/workspace/generated-files.d.ts.map +1 -1
  140. package/dist/workspace/intent.d.ts.map +1 -1
  141. package/dist/workspace/lineage-check.d.ts +3 -0
  142. package/dist/workspace/lineage-check.d.ts.map +1 -1
  143. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  144. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  145. package/dist/workspace/ls.d.ts +39 -1
  146. package/dist/workspace/ls.d.ts.map +1 -1
  147. package/dist/workspace/point-answer.schema.json +95 -0
  148. package/dist/workspace/points-cli.d.ts.map +1 -1
  149. package/dist/workspace/points.d.ts +1 -1
  150. package/dist/workspace/reason-codes.d.ts +14 -2
  151. package/dist/workspace/reason-codes.d.ts.map +1 -1
  152. package/dist/workspace/records-cli.d.ts +11 -1
  153. package/dist/workspace/records-cli.d.ts.map +1 -1
  154. package/dist/workspace/records-close.d.ts +1 -1
  155. package/dist/workspace/records-close.d.ts.map +1 -1
  156. package/dist/workspace/records-write.d.ts +11 -5
  157. package/dist/workspace/records-write.d.ts.map +1 -1
  158. package/dist/workspace/records.d.ts +34 -4
  159. package/dist/workspace/records.d.ts.map +1 -1
  160. package/dist/workspace/status-gates.d.ts +7 -2
  161. package/dist/workspace/status-gates.d.ts.map +1 -1
  162. package/dist/workspace/status-stewards.d.ts +59 -1
  163. package/dist/workspace/status-stewards.d.ts.map +1 -1
  164. package/dist/workspace/status.d.ts +16 -0
  165. package/dist/workspace/status.d.ts.map +1 -1
  166. package/dist/workspace/work-cli.d.ts +113 -2
  167. package/dist/workspace/work-cli.d.ts.map +1 -1
  168. package/dist/workspace/work-evidence.d.ts +74 -0
  169. package/dist/workspace/work-evidence.d.ts.map +1 -0
  170. package/dist/workspace/work.d.ts +43 -1
  171. package/dist/workspace/work.d.ts.map +1 -1
  172. package/package.json +1 -1
  173. package/src/cli/commands/audit.test.ts +9 -1
  174. package/src/cli/commands/build.ts +3 -2
  175. package/src/cli/commands/lint.ts +7 -1
  176. package/src/cli/handlers/graph.test.ts +20 -0
  177. package/src/cli/handlers/graph.ts +16 -2
  178. package/src/cli/handlers/operator.ts +34 -2
  179. package/src/cli/handlers/run.test.ts +154 -2
  180. package/src/cli/handlers/run.ts +121 -2
  181. package/src/cli/main.test.ts +10 -0
  182. package/src/cli/main.ts +19 -0
  183. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  184. package/src/cli/mcp/workspace-tools.ts +62 -4
  185. package/src/cli/plugins.ts +11 -3
  186. package/src/cli/registry.ts +12 -2
  187. package/src/codegen/pinned-upgrade.ts +2 -1
  188. package/src/codegen/release-wiring.test.ts +49 -2
  189. package/src/components/capability-plugin-loader.ts +3 -3
  190. package/src/components/cli-support.ts +2 -2
  191. package/src/config.ts +11 -0
  192. package/src/lexicon-module.ts +56 -1
  193. package/src/lexicon-package.test.ts +96 -0
  194. package/src/lifecycle/converge-ledger.ts +20 -0
  195. package/src/lifecycle/run-ledger.ts +6 -0
  196. package/src/lifecycle/symptoms.ts +95 -0
  197. package/src/lint/rules/index.ts +6 -2
  198. package/src/lint/rules/op/index.ts +17 -4
  199. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  200. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  201. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  202. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  203. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  204. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  205. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  206. package/src/op/activities/activity-contracts.ts +190 -1
  207. package/src/op/activities/apply.ts +7 -6
  208. package/src/op/activities/change-coverage.ts +60 -0
  209. package/src/op/activities/converge.test.ts +55 -1
  210. package/src/op/activities/converge.ts +208 -29
  211. package/src/op/activities/decide.live.test.ts +78 -0
  212. package/src/op/activities/decide.test.ts +300 -0
  213. package/src/op/activities/decide.ts +267 -0
  214. package/src/op/activities/index.ts +21 -0
  215. package/src/op/activities/shell.test.ts +91 -1
  216. package/src/op/activities/shell.ts +91 -2
  217. package/src/op/activities/source-release.test.ts +118 -0
  218. package/src/op/activities/source-release.ts +168 -0
  219. package/src/op/activities/source-rollback.test.ts +131 -0
  220. package/src/op/activities/source-rollback.ts +239 -0
  221. package/src/op/activities/work-evidence.ts +62 -0
  222. package/src/op/activity-contract-registry.test.ts +2 -0
  223. package/src/op/activity-contract-registry.ts +2 -2
  224. package/src/op/activity-contract.test.ts +12 -0
  225. package/src/op/activity-contract.ts +6 -1
  226. package/src/op/activity-profiles.ts +29 -0
  227. package/src/op/activity-registry.test.ts +1 -1
  228. package/src/op/activity-registry.ts +2 -2
  229. package/src/op/builders.ts +119 -5
  230. package/src/op/command-gate.test.ts +192 -0
  231. package/src/op/composites/converge-op.test.ts +27 -1
  232. package/src/op/composites/converge-op.ts +69 -15
  233. package/src/op/decide-backend.test.ts +44 -0
  234. package/src/op/decide-backend.ts +212 -0
  235. package/src/op/decide-config.test.ts +52 -0
  236. package/src/op/decide-config.ts +80 -0
  237. package/src/op/decide-read-inputs.ts +117 -0
  238. package/src/op/gate-approval.ts +2 -2
  239. package/src/op/gate-wait.ts +57 -0
  240. package/src/op/gate.ts +8 -3
  241. package/src/op/generate-pipeline.ts +2 -2
  242. package/src/op/index.ts +18 -3
  243. package/src/op/local-executor.ts +151 -15
  244. package/src/op/local-output.test.ts +33 -0
  245. package/src/op/local-output.ts +20 -2
  246. package/src/op/op-ir.test.ts +10 -0
  247. package/src/op/op-ir.ts +4 -0
  248. package/src/op/op-verb-class.ts +1 -0
  249. package/src/op/operator.ts +290 -11
  250. package/src/op/runtime.ts +31 -8
  251. package/src/op/runtimes/local.ts +3 -1
  252. package/src/op/source-archive.ts +161 -0
  253. package/src/op/steward-points.test.ts +313 -0
  254. package/src/op/steward-points.ts +194 -0
  255. package/src/op/steward-turn.ts +79 -0
  256. package/src/op/steward.test.ts +87 -3
  257. package/src/op/steward.ts +25 -1
  258. package/src/op/types.ts +7 -0
  259. package/src/op/work-lease-run.test.ts +20 -1
  260. package/src/op/work-lease-run.ts +2 -2
  261. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  262. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  263. package/src/workspace/answers-ledger.ts +118 -0
  264. package/src/workspace/changes-cli.ts +56 -0
  265. package/src/workspace/changes.schema.json +472 -0
  266. package/src/workspace/changes.test.ts +315 -0
  267. package/src/workspace/changes.ts +512 -0
  268. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  269. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  270. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  271. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  272. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  273. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  274. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  275. package/src/workspace/chant-migrations.ts +164 -0
  276. package/src/workspace/check-contract.test.ts +4 -2
  277. package/src/workspace/check.schema.json +15 -5
  278. package/src/workspace/checks/boxes.test.ts +80 -1
  279. package/src/workspace/checks/boxes.ts +136 -1
  280. package/src/workspace/checks/diagrams.test.ts +134 -0
  281. package/src/workspace/checks/diagrams.ts +108 -0
  282. package/src/workspace/checks/records.ts +47 -0
  283. package/src/workspace/checks.ts +8 -1
  284. package/src/workspace/compose-graph.ts +2 -0
  285. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  286. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  287. package/src/workspace/conformance/index.ts +5 -0
  288. package/src/workspace/decide.test.ts +1 -1
  289. package/src/workspace/decide.ts +156 -23
  290. package/src/workspace/declaration.schema.json +114 -1
  291. package/src/workspace/declaration.test.ts +58 -0
  292. package/src/workspace/declaration.ts +104 -0
  293. package/src/workspace/declared-kinds.test.ts +5 -5
  294. package/src/workspace/declared-kinds.ts +46 -1
  295. package/src/workspace/generated-files.ts +14 -0
  296. package/src/workspace/graph-cli.ts +1 -1
  297. package/src/workspace/graph.schema.json +9 -1
  298. package/src/workspace/intent.schema.json +8 -2
  299. package/src/workspace/intent.ts +3 -10
  300. package/src/workspace/lineage-check.ts +16 -1
  301. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  302. package/src/workspace/lineage-upgrade.test.ts +70 -0
  303. package/src/workspace/lineage-upgrade.ts +81 -10
  304. package/src/workspace/ls-contract.test.ts +57 -0
  305. package/src/workspace/ls.schema.json +46 -1
  306. package/src/workspace/ls.ts +62 -3
  307. package/src/workspace/points-cli.ts +14 -3
  308. package/src/workspace/points-write.schema.json +11 -1
  309. package/src/workspace/points.schema.json +33 -0
  310. package/src/workspace/points.test.ts +1 -1
  311. package/src/workspace/points.ts +1 -1
  312. package/src/workspace/read-contract.test.ts +1 -1
  313. package/src/workspace/reason-codes.test.ts +11 -1
  314. package/src/workspace/reason-codes.ts +18 -2
  315. package/src/workspace/records-amend.schema.json +2 -0
  316. package/src/workspace/records-cli.ts +13 -3
  317. package/src/workspace/records-close.schema.json +2 -0
  318. package/src/workspace/records-new.schema.json +2 -0
  319. package/src/workspace/records-review.schema.json +2 -0
  320. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  321. package/src/workspace/records-sessions-write.test.ts +2 -34
  322. package/src/workspace/records-write.e2e.test.ts +108 -0
  323. package/src/workspace/records-write.test.ts +32 -41
  324. package/src/workspace/records-write.ts +33 -15
  325. package/src/workspace/records.schema.json +36 -2
  326. package/src/workspace/records.test.ts +48 -3
  327. package/src/workspace/records.ts +109 -3
  328. package/src/workspace/status-contract.test.ts +13 -8
  329. package/src/workspace/status-gates.ts +9 -4
  330. package/src/workspace/status-stewards.ts +108 -4
  331. package/src/workspace/status.schema.json +111 -7
  332. package/src/workspace/status.ts +36 -1
  333. package/src/workspace/work-acceptance.test.ts +274 -0
  334. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  335. package/src/workspace/work-cli.ts +193 -10
  336. package/src/workspace/work-evidence.schema.json +212 -0
  337. package/src/workspace/work-evidence.ts +169 -0
  338. package/src/workspace/work-history.schema.json +349 -0
  339. package/src/workspace/work-history.test.ts +156 -0
  340. package/src/workspace/work.test.ts +3 -2
  341. package/src/workspace/work.ts +100 -1
  342. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  343. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  344. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -53,7 +53,7 @@
53
53
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "../../post-synth";
54
54
  import { classifyOpVerbClass, isWellFormedPredicate, predicateReferencesField } from "../../../op";
55
55
  import type { ConvergeRule, OpConfig } from "../../../op";
56
- import { CONVERGE_SYMPTOM_FIELDS, type ConvergeSymptom } from "../../../lifecycle/symptoms";
56
+ import { CONVERGE_SYMPTOM_FIELDS, RESOURCE_SYMPTOM_FIELDS, type ConvergeSymptom } from "../../../lifecycle/symptoms";
57
57
  import { isOpEntity } from "./support";
58
58
 
59
59
  function looksLikeOpProps(props: Record<string, unknown>): boolean {
@@ -61,12 +61,15 @@ function looksLikeOpProps(props: Record<string, unknown>): boolean {
61
61
  }
62
62
 
63
63
  /** Read a ConvergeOp's rule table back off its `convergeTick` step's `args.rules`. `undefined` if this doesn't look like a ConvergeOp-shaped Op (defensive — never crashes the build over an unexpected shape). */
64
- function findConvergeRules(props: OpConfig): ConvergeRule<ConvergeSymptom>[] | undefined {
64
+ function findConvergeRules(props: OpConfig): { rules: ConvergeRule<ConvergeSymptom>[]; fields: ReadonlySet<string> } | undefined {
65
65
  for (const phase of props.phases) {
66
66
  for (const step of phase.steps) {
67
67
  if (step.kind === "activity" && step.fn === "convergeTick") {
68
68
  const rules = step.args?.rules;
69
- return Array.isArray(rules) ? (rules as ConvergeRule<ConvergeSymptom>[]) : undefined;
69
+ // #2778: a ConvergeOp with an observer step passes its result as
70
+ // `observed`, and its rules read a ResourceSymptom.
71
+ const fields = step.args?.observed !== undefined ? RESOURCE_SYMPTOM_FIELDS : CONVERGE_SYMPTOM_FIELDS;
72
+ return Array.isArray(rules) ? { rules: rules as ConvergeRule<ConvergeSymptom>[], fields } : undefined;
70
73
  }
71
74
  }
72
75
  }
@@ -100,8 +103,9 @@ export const ops014: PostSynthCheck = {
100
103
  if (props.labels?.Converge !== "true") continue;
101
104
 
102
105
  const dial = props.labels?.Dial ?? "observe";
103
- const rules = findConvergeRules(props);
104
- if (!rules) continue;
106
+ const found = findConvergeRules(props);
107
+ if (!found) continue;
108
+ const { rules, fields } = found;
105
109
 
106
110
  for (const rule of rules) {
107
111
  const label = typeof rule?.id === "string" ? rule.id : "(unnamed rule)";
@@ -119,7 +123,7 @@ export const ops014: PostSynthCheck = {
119
123
  push("every rule must carry its why — refused at build");
120
124
  continue;
121
125
  }
122
- if (!isWellFormedPredicate(rule.when, CONVERGE_SYMPTOM_FIELDS)) {
126
+ if (!isWellFormedPredicate(rule.when, fields)) {
123
127
  push("predicate is outside the evaluable subset (build it from eq/neq/gt/gte/lt/lte/truthy/falsy/allOf/anyOf over a known symptom field)");
124
128
  continue;
125
129
  }
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Op, phase, decide } from "../../../op";
3
+ import type { PostSynthContext } from "../../post-synth";
4
+ import { plainRemoteHost, sys010, sys010Check } from "./sys010-key-over-plain-http";
5
+ import { coreOpChecks } from "./index";
6
+
7
+ const ctx = (op?: unknown): PostSynthContext => ({ outputs: new Map(), entities: new Map(op ? [["op", op as never]] : []), buildResult: {} as never }) as unknown as PostSynthContext;
8
+
9
+ describe("SYS010", () => {
10
+ it("fires when a decide step sends a key over plain http to a remote host", () => {
11
+ const op = Op({ name: "ask", overview: "ask", phases: [phase("Ask", [decide("triage", { backends: { s: { url: "http://jev.example.com", key: { env: "K" } } } })])] });
12
+ const d = sys010.check(ctx(op));
13
+ expect(d).toHaveLength(1);
14
+ expect(d[0]).toMatchObject({ checkId: "SYS010", severity: "error" });
15
+ expect(d[0].message).toContain("jev.example.com");
16
+ });
17
+
18
+ it("is quiet for https, for loopback, and for a backend with no key", () => {
19
+ const op = Op({
20
+ name: "ask",
21
+ overview: "ask",
22
+ phases: [
23
+ phase("Ask", [
24
+ decide("a", { backends: { s: { url: "https://api.typesafe.ai", key: { env: "K" } } } }),
25
+ decide("b", { backends: { s: { url: "http://127.0.0.1:8080", key: { env: "K" } } } }),
26
+ decide("c", { backends: { s: { url: "http://jev.example.com" } } }),
27
+ decide("d"),
28
+ ]),
29
+ ],
30
+ });
31
+ expect(sys010.check(ctx(op))).toEqual([]);
32
+ });
33
+
34
+ it("fires on a backend configured in decide.backends, and is quiet for https", () => {
35
+ const check = sys010Check({ remote: { url: "http://jev.internal:8080", key: { env: "K" } }, ok: { url: "https://api.typesafe.ai", key: { env: "K" } } });
36
+ const d = check.check(ctx());
37
+ expect(d.map((x) => x.entity)).toEqual(["decide.backends.remote"]);
38
+ });
39
+
40
+ it("runs with core's Op checks, with the configured backends when given", () => {
41
+ expect(coreOpChecks().map((c) => c.id)).toContain("SYS010");
42
+ const configured = coreOpChecks({ decideBackends: { remote: { url: "http://10.0.0.2", key: { env: "K" } } } }).find((c) => c.id === "SYS010")!;
43
+ expect(configured.check(ctx())).toHaveLength(1);
44
+ });
45
+
46
+ it("reads hosts", () => {
47
+ expect(plainRemoteHost("http://localhost:1")).toBeUndefined();
48
+ expect(plainRemoteHost("http://[::1]:1")).toBeUndefined();
49
+ expect(plainRemoteHost("http://10.0.0.2")).toBe("10.0.0.2");
50
+ expect(plainRemoteHost("not a url")).toBeUndefined();
51
+ });
52
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * SYS010: a `decide` backend sends its key over plain HTTP (#2740, core's
3
+ * since #2828).
4
+ *
5
+ * A backend in `decide.backends` in chant.config, or in a `decide` step's own
6
+ * `backends`, may have a key and an `http://` URL. The bearer key then crosses
7
+ * the network in clear text. A loopback address (127.0.0.1, ::1, localhost)
8
+ * is exempt, since that is where a local server such as `von serve` or the
9
+ * stub runs, and where a broker that is itself the endpoint listens.
10
+ *
11
+ * The steps are read off the Op entities, which covers a step written with
12
+ * the `decide` builder and one written with `activity("decide", ...)`. The
13
+ * configured backends are not entities: `chant build` and `chant lint` read
14
+ * them from the project's config and hand them to {@link sys010Check}. The id
15
+ * keeps the SYS prefix it had in the systemone lexicon.
16
+ */
17
+
18
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "../../post-synth";
19
+ import type { OpConfig, PhaseDefinition, ActivityStep } from "../../../op/types";
20
+ import type { DecideBackend } from "../../../op/decide-config";
21
+ import { isOpEntity } from "./support";
22
+
23
+ const LOOPBACK = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
24
+
25
+ function* decideSteps(phases: PhaseDefinition[] | undefined): Generator<ActivityStep> {
26
+ for (const phase of phases ?? []) {
27
+ for (const step of phase.steps ?? []) {
28
+ if (step.kind === "activity" && step.fn === "decide") yield step;
29
+ else if (step.kind === "effect") {
30
+ for (const nested of step.steps ?? []) if (nested.kind === "activity" && nested.fn === "decide") yield nested;
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ /** The host of an http:// URL that is not loopback, or undefined. */
37
+ export function plainRemoteHost(url: unknown): string | undefined {
38
+ if (typeof url !== "string") return undefined;
39
+ let parsed: URL;
40
+ try {
41
+ parsed = new URL(url);
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ if (parsed.protocol !== "http:") return undefined;
46
+ return LOOPBACK.has(parsed.hostname) ? undefined : parsed.hostname;
47
+ }
48
+
49
+ const DESCRIPTION = "A decide backend sends its key over plain HTTP to a host that is not loopback";
50
+
51
+ /**
52
+ * SYS010 over the Op entities and, when given, the backends configured in
53
+ * `decide.backends`. A configured backend's diagnostic names it as the entity
54
+ * `decide.backends.<name>`.
55
+ */
56
+ export function sys010Check(configured?: Record<string, Pick<DecideBackend, "url" | "key">>): PostSynthCheck {
57
+ return {
58
+ id: "SYS010",
59
+ description: DESCRIPTION,
60
+
61
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
62
+ const diagnostics: PostSynthDiagnostic[] = [];
63
+ for (const [name, b] of Object.entries(configured ?? {})) {
64
+ const host = b?.key !== undefined ? plainRemoteHost(b.url) : undefined;
65
+ if (!host) continue;
66
+ diagnostics.push({
67
+ checkId: "SYS010",
68
+ severity: "error",
69
+ message: `decide.backends.${name} sends its key over http:// to ${host}; use https://, or a loopback broker`,
70
+ entity: `decide.backends.${name}`,
71
+ });
72
+ }
73
+ for (const [entityKey, entity] of ctx.entities) {
74
+ if (!isOpEntity(entity)) continue;
75
+ const rec = entity as unknown as Record<string, unknown>;
76
+ const props = ((entity as { props?: Record<string, unknown> }).props ?? {}) as unknown as OpConfig;
77
+ for (const step of [...decideSteps(props.phases), ...decideSteps(props.onFailure)]) {
78
+ const backends = (step.args as { backends?: unknown } | undefined)?.backends;
79
+ if (backends === null || typeof backends !== "object") continue;
80
+ for (const [name, b] of Object.entries(backends as Record<string, { url?: unknown; key?: unknown }>)) {
81
+ if (b?.key === undefined) continue;
82
+ const host = plainRemoteHost(b.url);
83
+ if (!host) continue;
84
+ diagnostics.push({
85
+ checkId: "SYS010",
86
+ severity: "error",
87
+ message: `Op "${props.name}", decide ${JSON.stringify((step.args as { point?: unknown }).point)}: backend ${name} sends its key over http:// to ${host}; use https://, or a loopback broker`,
88
+ entity: entityKey,
89
+ lexicon: typeof rec.lexicon === "string" ? rec.lexicon : undefined,
90
+ });
91
+ }
92
+ }
93
+ }
94
+ return diagnostics;
95
+ },
96
+ };
97
+ }
98
+
99
+ /** SYS010 over the Op entities alone, with no configured backends. */
100
+ export const sys010: PostSynthCheck = sys010Check();
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import ts from "typescript";
3
+ import { noLiteralKeyRule } from "./sys001-no-literal-key";
4
+ import { loadCoreRules } from "./index";
5
+
6
+ const run = (code: string) =>
7
+ noLiteralKeyRule.check({ sourceFile: ts.createSourceFile("x.ts", code, ts.ScriptTarget.Latest, true), filePath: "x.ts", entities: [] } as never);
8
+
9
+ describe("SYS001", () => {
10
+ it("is one of core's rules", () => {
11
+ expect(loadCoreRules().map((r) => r.id)).toContain("SYS001");
12
+ });
13
+
14
+ it("fires on a literal key in the config's decide block", () => {
15
+ const d = run(`export default { decide: { backends: { s: { url: "https://x", key: "sk-abc" } } } };`);
16
+ expect(d.map((x) => x.ruleId)).toEqual(["SYS001"]);
17
+ });
18
+
19
+ it("fires on a literal key in a decide step's backends", () => {
20
+ expect(run('decide("p", { backends: { s: { url: "https://x", key: `k` } } });')).toHaveLength(1);
21
+ });
22
+
23
+ it("is quiet for { env } and a brokered capability", () => {
24
+ expect(run(`export default { decide: { backends: { s: { url: "https://x", key: { env: "K" } }, b: { url: "http://127.0.0.1", key: { capability: "inference" } } } } };`)).toEqual([]);
25
+ });
26
+
27
+ it("is quiet for a key property outside decide, and for another tool's backends", () => {
28
+ expect(run(`const x = { key: "not a backend" };`)).toEqual([]);
29
+ expect(run(`const tf = { backends: { s3: { bucket: "b", key: "state/terraform.tfstate" } } };`)).toEqual([]);
30
+ });
31
+ });
@@ -0,0 +1,67 @@
1
+ import ts from "typescript";
2
+ import type { LintRule, LintDiagnostic, LintContext } from "../rule";
3
+
4
+ /**
5
+ * SYS001: a `decide` backend's key is never a literal (#2740, core's since
6
+ * #2828).
7
+ *
8
+ * A key is `{ env: "VARIABLE" }` or a brokered capability
9
+ * (`{ capability: "inference", member: "box" }`, #2726). A string written
10
+ * where a key goes is a credential in git history, and a box that holds one
11
+ * fails `workspace check` with WSP121 as well.
12
+ *
13
+ * Fires on a `key` property whose value is a string or template literal,
14
+ * inside a backend of a `backends` object that belongs to `decide`: the
15
+ * `decide` block of a config object (`decide: { backends: ... }`) or the
16
+ * options of a `decide(...)` step. A `backends` object anywhere else, such as
17
+ * a Terraform backend's `key`, is not a decide backend and is left alone.
18
+ * The id keeps the SYS prefix it had in the systemone lexicon.
19
+ */
20
+
21
+ function keyText(name: ts.PropertyName): string | undefined {
22
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
23
+ return undefined;
24
+ }
25
+
26
+ const isLiteral = (node: ts.Expression): boolean => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node);
27
+
28
+ const isDecideCall = (node: ts.Node): node is ts.CallExpression =>
29
+ ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "decide";
30
+
31
+ /** Where the walk is: outside decide, inside decide's options or config block, or inside its `backends`. */
32
+ type Scope = "outside" | "decide" | "backends";
33
+
34
+ export const noLiteralKeyRule: LintRule = {
35
+ id: "SYS001",
36
+ severity: "error",
37
+ category: "security",
38
+ description: "A decide backend key is { env } or a brokered capability, never a literal string",
39
+
40
+ check(context: LintContext): LintDiagnostic[] {
41
+ const diagnostics: LintDiagnostic[] = [];
42
+ const source = context.sourceFile;
43
+ const visit = (node: ts.Node, scope: Scope) => {
44
+ let within = scope;
45
+ if (isDecideCall(node)) within = "decide";
46
+ else if (ts.isPropertyAssignment(node)) {
47
+ const key = keyText(node.name);
48
+ if (key === "decide") within = "decide";
49
+ else if (key === "backends" && scope === "decide") within = "backends";
50
+ if (scope === "backends" && key === "key" && isLiteral(node.initializer)) {
51
+ const { line, character } = source.getLineAndCharacterOfPosition(node.initializer.getStart(source));
52
+ diagnostics.push({
53
+ ruleId: "SYS001",
54
+ severity: "error",
55
+ message: 'Literal backend key: use { env: "VARIABLE" } or a brokered capability ({ capability, member }) instead',
56
+ file: context.filePath,
57
+ line: line + 1,
58
+ column: character + 1,
59
+ });
60
+ }
61
+ }
62
+ ts.forEachChild(node, (child) => visit(child, within));
63
+ };
64
+ visit(source, "outside");
65
+ return diagnostics;
66
+ },
67
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * A stub `POST /v1/systemone` server for tests of the `decide` activity
3
+ * (`@intentius/chant/op/__fixtures__/decide-stub-backend`): a real HTTP server on
4
+ * 127.0.0.1 that checks a request the way a Jev-compatible server would and
5
+ * answers with what the test scripts. No model runs.
6
+ *
7
+ * ```ts
8
+ * const stub = await startStubBackend({ key: "k", answers: { slice_tier: { type: "choice", choice: "large", probabilities: {...}, confidence: 0.9 } } });
9
+ * // decide.backends, or a decide step's backends: { systemone: { url: stub.url, key: { env: "STUB_KEY" } } }
10
+ * await stub.close();
11
+ * ```
12
+ *
13
+ * It answers 401 when a key is set and the request's bearer differs, 422 when
14
+ * the body is not a request in the wire format, and `status` when the test
15
+ * forces one (429 or 529, say). Every request it read is kept in `requests`.
16
+ *
17
+ * It lives under `__fixtures__` because it listens on a port, and no module
18
+ * chant runs does (#2657, `test/no-listener.test.ts`): only tests start it.
19
+ */
20
+
21
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
22
+ import type { AddressInfo } from "node:net";
23
+ import type { WireAnswer } from "../../workspace/points";
24
+ import { SYSTEMONE_PATH, type BackendRequest } from "../decide-backend";
25
+
26
+ export interface StubQuestion {
27
+ name: string;
28
+ type: string;
29
+ instructions: string;
30
+ criteria: Record<string, string> | string[];
31
+ }
32
+
33
+ export interface StubBackendOptions {
34
+ /** The bearer key the stub requires. Without it, any request is taken. */
35
+ key?: string;
36
+ /** The model id the response reports. Defaults to the id the request sent. */
37
+ model?: string;
38
+ /** The answer to a question, by question name, or computed from the question. Defaults to {@link defaultAnswer}. */
39
+ answers?: Record<string, WireAnswer> | ((question: StubQuestion, request: BackendRequest) => WireAnswer);
40
+ /** Answer every request with this status and no answers. */
41
+ status?: number;
42
+ }
43
+
44
+ export interface StubBackend {
45
+ /** The base URL, without `/v1/systemone`. */
46
+ url: string;
47
+ /** Each request body the stub read, in order. */
48
+ requests: BackendRequest[];
49
+ /** The Authorization header of each request, or null. */
50
+ authorizations: (string | null)[];
51
+ close(): Promise<void>;
52
+ }
53
+
54
+ /** A confident answer of the question's own type: true, the first option, or the last level. */
55
+ export function defaultAnswer(question: StubQuestion): WireAnswer {
56
+ if (question.type === "noul") return { type: "noul", noul: 0.95 };
57
+ if (question.type === "choice") {
58
+ const options = Object.keys(question.criteria);
59
+ const rest = options.length > 1 ? 0.05 / (options.length - 1) : 0;
60
+ const probabilities = Object.fromEntries(options.map((o, i) => [o, i === 0 ? 0.95 : rest]));
61
+ return { type: "choice", choice: options[0], probabilities, confidence: 0.925 };
62
+ }
63
+ if (question.type === "score") {
64
+ const levels = question.criteria as string[];
65
+ const probabilities = Object.fromEntries(levels.map((l, i) => [l, i === levels.length - 1 ? 0.95 : 0.05 / Math.max(1, levels.length - 1)]));
66
+ return { type: "score", score: levels.length - 0.05, probabilities, confidence: 0.925 };
67
+ }
68
+ return { type: "unsupported" };
69
+ }
70
+
71
+ /** Why a body is not a request in the wire format, or null when it is one. */
72
+ export function requestProblem(body: unknown): string | null {
73
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return "the body is not a JSON object";
74
+ const b = body as Record<string, unknown>;
75
+ if (typeof b.model !== "string" || b.model === "") return "model is required";
76
+ if (b.state === undefined) return "state is required";
77
+ if (b.questions === null || typeof b.questions !== "object" || Array.isArray(b.questions) || Object.keys(b.questions).length === 0) return "questions must name at least one question";
78
+ for (const [name, q] of Object.entries(b.questions as Record<string, unknown>)) {
79
+ const question = q as Record<string, unknown> | null;
80
+ if (question === null || typeof question !== "object") return `questions.${name} is not an object`;
81
+ if (typeof question.instructions !== "string") return `questions.${name}.instructions is required`;
82
+ if (question.type === "noul" || question.type === "choice") {
83
+ if (question.criteria === null || typeof question.criteria !== "object" || Array.isArray(question.criteria)) return `questions.${name}.criteria must be an object`;
84
+ if (question.type === "noul" && !("true" in question.criteria && "false" in question.criteria)) return `questions.${name}.criteria must describe true and false`;
85
+ } else if (question.type === "score") {
86
+ if (!Array.isArray(question.criteria) || question.criteria.length < 2 || question.criteria.length > 10) return `questions.${name}.criteria must be 2 to 10 levels`;
87
+ } else {
88
+ return `questions.${name}.type must be noul, choice or score`;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function send(res: ServerResponse, status: number, body: unknown): void {
95
+ res.writeHead(status, { "content-type": "application/json" });
96
+ res.end(JSON.stringify(body));
97
+ }
98
+
99
+ async function readBody(req: IncomingMessage): Promise<string> {
100
+ const chunks: Buffer[] = [];
101
+ for await (const chunk of req) chunks.push(chunk as Buffer);
102
+ return Buffer.concat(chunks).toString("utf-8");
103
+ }
104
+
105
+ /** Start the stub on a free port on 127.0.0.1. */
106
+ export async function startStubBackend(opts: StubBackendOptions = {}): Promise<StubBackend> {
107
+ const requests: BackendRequest[] = [];
108
+ const authorizations: (string | null)[] = [];
109
+ const server = createServer((req, res) => {
110
+ void (async () => {
111
+ if (req.method !== "POST" || req.url !== SYSTEMONE_PATH) return send(res, 404, { error: { type: "not_found", message: `${req.method} ${req.url}` } });
112
+ const auth = req.headers.authorization ?? null;
113
+ authorizations.push(auth);
114
+ if (opts.key !== undefined && auth !== `Bearer ${opts.key}`) return send(res, 401, { error: { type: "authentication_error", message: "invalid key" } });
115
+ let body: unknown;
116
+ try {
117
+ body = JSON.parse(await readBody(req));
118
+ } catch {
119
+ return send(res, 422, { error: { type: "invalid_request", message: "the body is not JSON" } });
120
+ }
121
+ const problem = requestProblem(body);
122
+ if (problem) return send(res, 422, { error: { type: "invalid_request", message: problem } });
123
+ const request = body as BackendRequest;
124
+ requests.push(request);
125
+ if (opts.status !== undefined) return send(res, opts.status, { error: { type: "stub", message: `the stub answers ${opts.status}` } });
126
+ const answers: Record<string, WireAnswer> = {};
127
+ for (const [name, q] of Object.entries(request.questions)) {
128
+ const question: StubQuestion = { name, ...q };
129
+ const scripted = typeof opts.answers === "function" ? opts.answers(question, request) : opts.answers?.[name];
130
+ answers[name] = scripted ?? defaultAnswer(question);
131
+ }
132
+ send(res, 200, { model: opts.model ?? request.model, answers, usage: { input_tokens: JSON.stringify(request).length, output_tokens: 0 } });
133
+ })().catch((err: unknown) => send(res, 500, { error: { type: "stub", message: err instanceof Error ? err.message : String(err) } }));
134
+ });
135
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
136
+ const { port } = server.address() as AddressInfo;
137
+ return {
138
+ url: `http://127.0.0.1:${port}`,
139
+ requests,
140
+ authorizations,
141
+ close: () => new Promise<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))),
142
+ };
143
+ }
@@ -25,6 +25,7 @@
25
25
 
26
26
  import { z } from "zod";
27
27
  import { activityContract } from "../activity-contract";
28
+ import { backendSchema } from "../decide-config";
28
29
 
29
30
  export const lifecycleSnapshotContract = activityContract(
30
31
  "lifecycleSnapshot",
@@ -54,8 +55,14 @@ export const shellCmdContract = activityContract(
54
55
  env: z.record(z.string(), z.string()).optional(),
55
56
  cwd: z.string().optional(),
56
57
  okExit: z.array(z.number()).optional(),
58
+ gatedExit: z.number().optional(),
59
+ gate: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
60
+ json: z.boolean().optional(),
57
61
  }),
58
- z.object({ stdout: z.string(), stderr: z.string(), exitCode: z.number() }),
62
+ // `json` is stdout parsed, when the step sets `json` (#2787): whatever the
63
+ // command printed, so its shape is the author's and only the whole value is
64
+ // a reference target (`sh.out.json`).
65
+ z.object({ stdout: z.string(), stderr: z.string(), exitCode: z.number(), json: z.unknown().optional() }),
59
66
  );
60
67
 
61
68
  export const httpCheckContract = activityContract(
@@ -166,3 +173,185 @@ export const proposeWorkspaceUpgradeContract = activityContract(
166
173
  summary: z.string(),
167
174
  }),
168
175
  );
176
+
177
+ /**
178
+ * The forward coverage check over an Op's own diff (#2773). `args` mirrors
179
+ * `ChangeCoverageArgs`. `returns` names what a step reads: whether the check
180
+ * passed, the range and work item checked, and each finding with the
181
+ * `triage` a work item seeded from it takes as its `source`.
182
+ */
183
+ export const changeCoverageContract = activityContract(
184
+ "changeCoverage",
185
+ z.strictObject({
186
+ cwd: z.string().optional(),
187
+ range: z.string().optional(),
188
+ work: z.string().optional(),
189
+ severity: z.enum(["off", "warn", "fail"]).optional(),
190
+ }),
191
+ z.object({
192
+ ok: z.boolean(),
193
+ range: z.object({ spec: z.string(), base: z.string(), head: z.string() }),
194
+ work: z.string().nullable(),
195
+ severity: z.enum(["off", "warn", "fail"]),
196
+ findings: z.array(
197
+ z.object({
198
+ code: z.enum(["change-uncovered", "change-out-of-scope"]),
199
+ path: z.string(),
200
+ message: z.string(),
201
+ severity: z.enum(["warn", "fail"]),
202
+ records: z.array(z.string()),
203
+ triage: z.object({ finding: z.enum(["change-uncovered", "change-out-of-scope"]), region: z.string() }),
204
+ }),
205
+ ),
206
+ summary: z.object({
207
+ paths: z.number(),
208
+ covered: z.number(),
209
+ uncovered: z.number(),
210
+ outOfScope: z.number(),
211
+ ignored: z.number(),
212
+ records: z.number(),
213
+ }),
214
+ }),
215
+ );
216
+
217
+ /**
218
+ * Evidence for an acceptance criterion, under the run's work lease (#2772).
219
+ * `lease` is the run's lease, a reference the builder fills in, so it is not
220
+ * checked here. `returns` names what a step reads: the item, the record's
221
+ * path, the entry appended and the criteria counted with it.
222
+ */
223
+ export const workEvidenceContract = activityContract(
224
+ "workEvidence",
225
+ z.strictObject({
226
+ lease: z.unknown(),
227
+ criterion: z.string(),
228
+ result: z.enum(["pass", "fail"]),
229
+ title: z.string(),
230
+ url: z.string().optional(),
231
+ path: z.string().optional(),
232
+ kind: z.string().optional(),
233
+ cwd: z.string().optional(),
234
+ }),
235
+ z.object({
236
+ item: z.string(),
237
+ path: z.string(),
238
+ evidence: z.record(z.string(), z.unknown()),
239
+ acceptance: z.object({ met: z.number(), total: z.number(), criteria: z.array(z.object({ id: z.string(), verification: z.string(), met: z.boolean() })) }),
240
+ }),
241
+ );
242
+
243
+ /**
244
+ * The source-release activities (#2782, ./source-release.ts). A release Op
245
+ * reads `archive.out.digest` into its plan, `plan.out.digest` into its gate
246
+ * and its ship step, and `plan.out.file` into its record step.
247
+ */
248
+ export const sourceArchiveContract = activityContract(
249
+ "sourceArchive",
250
+ z.strictObject({ path: z.string(), ref: z.string().optional(), out: z.string().optional(), cwd: z.string().optional() }),
251
+ z.object({ digest: z.string(), archive: z.string(), commit: z.string(), dir: z.string(), files: z.number(), bytes: z.number() }),
252
+ );
253
+
254
+ export const releasePlanContract = activityContract(
255
+ "releasePlan",
256
+ z.strictObject({
257
+ component: z.string(),
258
+ env: z.string(),
259
+ gitSha: z.string(),
260
+ content: z.record(z.string(), z.unknown()),
261
+ dir: z.string().optional(),
262
+ cwd: z.string().optional(),
263
+ }),
264
+ z.object({ digest: z.string(), file: z.string(), gitSha: z.string() }),
265
+ );
266
+
267
+ export const releaseRecordContract = activityContract(
268
+ "releaseRecord",
269
+ z.strictObject({
270
+ plan: z.string(),
271
+ digest: z.string().optional(),
272
+ approval: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
273
+ actor: z.string().optional(),
274
+ runId: z.string().optional(),
275
+ cwd: z.string().optional(),
276
+ }),
277
+ z.object({ recorded: z.boolean(), digest: z.string(), env: z.string(), component: z.string(), approver: z.string().nullable() }),
278
+ );
279
+
280
+ /**
281
+ * `decide` (#2740, core's since #2828). OPS012 checks a step's args against
282
+ * it and OPS013 a later step's reference into its result, so a misspelled
283
+ * `point` or a key written as a literal string fails `chant build` rather
284
+ * than a run.
285
+ */
286
+ const decideEscalation = z.object({ kind: z.enum(["table", "model"]), reason: z.string() }).loose();
287
+
288
+ export const decideContract = activityContract(
289
+ "decide",
290
+ z.strictObject({
291
+ point: z.string().min(1),
292
+ inputs: z.record(z.string(), z.unknown()).optional(),
293
+ read: z.record(z.string(), z.string()).optional(),
294
+ subject: z.string().optional(),
295
+ kind: z.string().optional(),
296
+ cwd: z.string().optional(),
297
+ backends: z.record(z.string(), backendSchema).optional(),
298
+ dryRun: z.boolean().optional(),
299
+ }),
300
+ z.strictObject({
301
+ id: z.string(),
302
+ path: z.string(),
303
+ state: z.enum(["escalated", "proposed", "answered"]),
304
+ open: z.boolean(),
305
+ answer: z.union([z.string(), z.boolean(), z.null()]),
306
+ decider: z.string(),
307
+ model: z.string().nullable(),
308
+ backend: z.string().nullable(),
309
+ confidence: z.number().nullable(),
310
+ threshold: z.number().nullable(),
311
+ answeredBy: z.array(z.string()),
312
+ escalations: z.array(decideEscalation),
313
+ missing: z.array(z.string()),
314
+ }),
315
+ { entities: ["point"] },
316
+ );
317
+
318
+ /**
319
+ * The rollback activities for a source release (#2800, ./source-rollback.ts).
320
+ * A rollback Op reads `plan.out.digest` into its gate, `plan.out.to` and the
321
+ * archive fields into the step that restores the Machine, and
322
+ * `plan.out.file` into its record step.
323
+ */
324
+ export const releaseRollbackPlanContract = activityContract(
325
+ "releaseRollbackPlan",
326
+ z.strictObject({
327
+ component: z.string(),
328
+ env: z.string(),
329
+ to: z.string().optional(),
330
+ path: z.string().optional(),
331
+ dir: z.string().optional(),
332
+ cwd: z.string().optional(),
333
+ }),
334
+ z.object({
335
+ digest: z.string(),
336
+ file: z.string(),
337
+ to: z.string(),
338
+ from: z.string(),
339
+ gitSha: z.string(),
340
+ archive: z.string(),
341
+ archiveDigest: z.string(),
342
+ dir: z.string(),
343
+ }),
344
+ );
345
+
346
+ export const releaseRollbackRecordContract = activityContract(
347
+ "releaseRollbackRecord",
348
+ z.strictObject({
349
+ plan: z.string(),
350
+ digest: z.string().optional(),
351
+ approval: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
352
+ actor: z.string().optional(),
353
+ runId: z.string().optional(),
354
+ cwd: z.string().optional(),
355
+ }),
356
+ z.object({ recorded: z.boolean(), digest: z.string(), env: z.string(), component: z.string(), approver: z.string().nullable() }),
357
+ );