@intentius/chant 0.90.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 (402) 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/components.d.ts +8 -0
  4. package/dist/cli/handlers/components.d.ts.map +1 -1
  5. package/dist/cli/handlers/graph.d.ts.map +1 -1
  6. package/dist/cli/handlers/operator.d.ts +20 -0
  7. package/dist/cli/handlers/operator.d.ts.map +1 -1
  8. package/dist/cli/handlers/run.d.ts.map +1 -1
  9. package/dist/cli/main.d.ts.map +1 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts +7 -4
  11. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  12. package/dist/cli/plugins.d.ts.map +1 -1
  13. package/dist/cli/registry.d.ts +42 -1
  14. package/dist/cli/registry.d.ts.map +1 -1
  15. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  16. package/dist/components/verbs/vuln-scan.d.ts +72 -0
  17. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  18. package/dist/config.d.ts +22 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/lexicon-module.d.ts +22 -0
  21. package/dist/lexicon-module.d.ts.map +1 -1
  22. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  23. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  24. package/dist/lifecycle/git.d.ts +40 -5
  25. package/dist/lifecycle/git.d.ts.map +1 -1
  26. package/dist/lifecycle/lease.d.ts +72 -16
  27. package/dist/lifecycle/lease.d.ts.map +1 -1
  28. package/dist/lifecycle/member-ledger.d.ts +3 -2
  29. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  30. package/dist/lifecycle/plan-ledger.d.ts +114 -0
  31. package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
  32. package/dist/lifecycle/run-ledger.d.ts +2 -0
  33. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  34. package/dist/lifecycle/symptoms.d.ts +51 -0
  35. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  36. package/dist/lifecycle/work-lease.d.ts +140 -0
  37. package/dist/lifecycle/work-lease.d.ts.map +1 -0
  38. package/dist/lint/rules/index.d.ts +4 -2
  39. package/dist/lint/rules/index.d.ts.map +1 -1
  40. package/dist/lint/rules/op/index.d.ts +14 -3
  41. package/dist/lint/rules/op/index.d.ts.map +1 -1
  42. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  43. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  44. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  45. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  46. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  47. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  48. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  49. package/dist/op/activities/activity-contracts.d.ts +203 -2
  50. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  51. package/dist/op/activities/apply.d.ts.map +1 -1
  52. package/dist/op/activities/change-coverage.d.ts +44 -0
  53. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  54. package/dist/op/activities/converge.d.ts +24 -1
  55. package/dist/op/activities/converge.d.ts.map +1 -1
  56. package/dist/op/activities/decide.d.ts +107 -0
  57. package/dist/op/activities/decide.d.ts.map +1 -0
  58. package/dist/op/activities/index.d.ts +10 -0
  59. package/dist/op/activities/index.d.ts.map +1 -1
  60. package/dist/op/activities/shell.d.ts +32 -0
  61. package/dist/op/activities/shell.d.ts.map +1 -1
  62. package/dist/op/activities/source-release.d.ts +86 -0
  63. package/dist/op/activities/source-release.d.ts.map +1 -0
  64. package/dist/op/activities/source-rollback.d.ts +100 -0
  65. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  66. package/dist/op/activities/work-evidence.d.ts +42 -0
  67. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  68. package/dist/op/activity-contract.d.ts +1 -0
  69. package/dist/op/activity-contract.d.ts.map +1 -1
  70. package/dist/op/activity-profiles.d.ts +16 -0
  71. package/dist/op/activity-profiles.d.ts.map +1 -1
  72. package/dist/op/builders.d.ts +84 -2
  73. package/dist/op/builders.d.ts.map +1 -1
  74. package/dist/op/composites/converge-op.d.ts +31 -4
  75. package/dist/op/composites/converge-op.d.ts.map +1 -1
  76. package/dist/op/decide-backend.d.ts +116 -0
  77. package/dist/op/decide-backend.d.ts.map +1 -0
  78. package/dist/op/decide-config.d.ts +120 -0
  79. package/dist/op/decide-config.d.ts.map +1 -0
  80. package/dist/op/decide-read-inputs.d.ts +40 -0
  81. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  82. package/dist/op/discover.d.ts +25 -0
  83. package/dist/op/discover.d.ts.map +1 -1
  84. package/dist/op/gate-wait.d.ts +43 -0
  85. package/dist/op/gate-wait.d.ts.map +1 -0
  86. package/dist/op/gate.d.ts +5 -2
  87. package/dist/op/gate.d.ts.map +1 -1
  88. package/dist/op/index.d.ts +18 -5
  89. package/dist/op/index.d.ts.map +1 -1
  90. package/dist/op/lifecycle-receipt-store.d.ts +34 -0
  91. package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
  92. package/dist/op/local-executor.d.ts +48 -4
  93. package/dist/op/local-executor.d.ts.map +1 -1
  94. package/dist/op/local-output.d.ts +2 -0
  95. package/dist/op/local-output.d.ts.map +1 -1
  96. package/dist/op/op-ir.d.ts +12 -1
  97. package/dist/op/op-ir.d.ts.map +1 -1
  98. package/dist/op/op-verb-class.d.ts.map +1 -1
  99. package/dist/op/operator.d.ts +126 -1
  100. package/dist/op/operator.d.ts.map +1 -1
  101. package/dist/op/runtime.d.ts +46 -5
  102. package/dist/op/runtime.d.ts.map +1 -1
  103. package/dist/op/runtimes/local.d.ts.map +1 -1
  104. package/dist/op/source-archive.d.ts +63 -0
  105. package/dist/op/source-archive.d.ts.map +1 -0
  106. package/dist/op/step-output-ref.d.ts +2 -2
  107. package/dist/op/step-output-ref.d.ts.map +1 -1
  108. package/dist/op/steward-points.d.ts +136 -0
  109. package/dist/op/steward-points.d.ts.map +1 -0
  110. package/dist/op/steward-turn.d.ts +53 -0
  111. package/dist/op/steward-turn.d.ts.map +1 -0
  112. package/dist/op/steward.d.ts +161 -0
  113. package/dist/op/steward.d.ts.map +1 -0
  114. package/dist/op/types.d.ts +58 -0
  115. package/dist/op/types.d.ts.map +1 -1
  116. package/dist/op/work-lease-decl.d.ts +18 -0
  117. package/dist/op/work-lease-decl.d.ts.map +1 -0
  118. package/dist/op/work-lease-run.d.ts +173 -0
  119. package/dist/op/work-lease-run.d.ts.map +1 -0
  120. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  121. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  122. package/dist/workspace/answers-ledger.d.ts +76 -0
  123. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  124. package/dist/workspace/box-isolation.d.ts +99 -0
  125. package/dist/workspace/box-isolation.d.ts.map +1 -0
  126. package/dist/workspace/changes-cli.d.ts +16 -0
  127. package/dist/workspace/changes-cli.d.ts.map +1 -0
  128. package/dist/workspace/changes.d.ts +200 -0
  129. package/dist/workspace/changes.d.ts.map +1 -0
  130. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  131. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  132. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  133. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  134. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  135. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  136. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  137. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  138. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  139. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  140. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  141. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  142. package/dist/workspace/chant-migrations.d.ts +99 -0
  143. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  144. package/dist/workspace/checks/box-isolation.d.ts +18 -0
  145. package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
  146. package/dist/workspace/checks/boxes.d.ts +104 -0
  147. package/dist/workspace/checks/boxes.d.ts.map +1 -0
  148. package/dist/workspace/checks/diagrams.d.ts +31 -0
  149. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  150. package/dist/workspace/checks/records.d.ts +8 -0
  151. package/dist/workspace/checks/records.d.ts.map +1 -1
  152. package/dist/workspace/checks.d.ts +13 -1
  153. package/dist/workspace/checks.d.ts.map +1 -1
  154. package/dist/workspace/compose-graph.d.ts +2 -0
  155. package/dist/workspace/compose-graph.d.ts.map +1 -1
  156. package/dist/workspace/conformance/index.d.ts.map +1 -1
  157. package/dist/workspace/decide.d.ts +229 -0
  158. package/dist/workspace/decide.d.ts.map +1 -0
  159. package/dist/workspace/decision-points.schema.json +137 -0
  160. package/dist/workspace/declaration.d.ts +103 -0
  161. package/dist/workspace/declaration.d.ts.map +1 -1
  162. package/dist/workspace/declaration.schema.json +285 -0
  163. package/dist/workspace/declared-kinds.d.ts +32 -0
  164. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  165. package/dist/workspace/generated-files.d.ts +7 -0
  166. package/dist/workspace/generated-files.d.ts.map +1 -1
  167. package/dist/workspace/intent.d.ts.map +1 -1
  168. package/dist/workspace/lineage-check.d.ts +3 -0
  169. package/dist/workspace/lineage-check.d.ts.map +1 -1
  170. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  171. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  172. package/dist/workspace/ls.d.ts +39 -1
  173. package/dist/workspace/ls.d.ts.map +1 -1
  174. package/dist/workspace/point-answer.schema.json +95 -0
  175. package/dist/workspace/points-cli.d.ts +113 -0
  176. package/dist/workspace/points-cli.d.ts.map +1 -0
  177. package/dist/workspace/points.d.ts +320 -0
  178. package/dist/workspace/points.d.ts.map +1 -0
  179. package/dist/workspace/reason-codes.d.ts +38 -0
  180. package/dist/workspace/reason-codes.d.ts.map +1 -1
  181. package/dist/workspace/record-assets.d.ts.map +1 -1
  182. package/dist/workspace/records-cli.d.ts +23 -1
  183. package/dist/workspace/records-cli.d.ts.map +1 -1
  184. package/dist/workspace/records-close.d.ts +1 -1
  185. package/dist/workspace/records-close.d.ts.map +1 -1
  186. package/dist/workspace/records-write.d.ts +11 -5
  187. package/dist/workspace/records-write.d.ts.map +1 -1
  188. package/dist/workspace/records.d.ts +42 -6
  189. package/dist/workspace/records.d.ts.map +1 -1
  190. package/dist/workspace/status-gates.d.ts +7 -2
  191. package/dist/workspace/status-gates.d.ts.map +1 -1
  192. package/dist/workspace/status-stewards.d.ts +179 -0
  193. package/dist/workspace/status-stewards.d.ts.map +1 -0
  194. package/dist/workspace/status.d.ts +68 -1
  195. package/dist/workspace/status.d.ts.map +1 -1
  196. package/dist/workspace/work-cli.d.ts +189 -0
  197. package/dist/workspace/work-cli.d.ts.map +1 -0
  198. package/dist/workspace/work-evidence.d.ts +74 -0
  199. package/dist/workspace/work-evidence.d.ts.map +1 -0
  200. package/dist/workspace/work.d.ts +43 -1
  201. package/dist/workspace/work.d.ts.map +1 -1
  202. package/package.json +1 -1
  203. package/src/cli/commands/audit.test.ts +9 -1
  204. package/src/cli/commands/build.ts +3 -2
  205. package/src/cli/commands/lint.ts +7 -1
  206. package/src/cli/handlers/components.test.ts +93 -0
  207. package/src/cli/handlers/components.ts +44 -3
  208. package/src/cli/handlers/graph.test.ts +20 -0
  209. package/src/cli/handlers/graph.ts +16 -2
  210. package/src/cli/handlers/operator.ts +141 -4
  211. package/src/cli/handlers/run.test.ts +173 -2
  212. package/src/cli/handlers/run.ts +174 -3
  213. package/src/cli/main.test.ts +50 -0
  214. package/src/cli/main.ts +97 -2
  215. package/src/cli/mcp/workspace-tools.test.ts +48 -19
  216. package/src/cli/mcp/workspace-tools.ts +110 -8
  217. package/src/cli/plugins.ts +11 -3
  218. package/src/cli/registry.ts +42 -1
  219. package/src/codegen/pinned-upgrade.ts +2 -1
  220. package/src/codegen/release-wiring.test.ts +49 -2
  221. package/src/components/capability-plugin-loader.ts +3 -3
  222. package/src/components/cli-support.ts +2 -2
  223. package/src/components/verbs/vuln-scan.test.ts +124 -1
  224. package/src/components/verbs/vuln-scan.ts +142 -1
  225. package/src/config.ts +11 -0
  226. package/src/lexicon-module.ts +56 -1
  227. package/src/lexicon-package.test.ts +96 -0
  228. package/src/lifecycle/converge-ledger.ts +20 -0
  229. package/src/lifecycle/git.ts +65 -15
  230. package/src/lifecycle/lease.test.ts +22 -0
  231. package/src/lifecycle/lease.ts +133 -29
  232. package/src/lifecycle/member-ledger.ts +3 -2
  233. package/src/lifecycle/plan-ledger.test.ts +148 -0
  234. package/src/lifecycle/plan-ledger.ts +158 -0
  235. package/src/lifecycle/run-ledger.ts +6 -0
  236. package/src/lifecycle/symptoms.ts +95 -0
  237. package/src/lifecycle/work-lease.test.ts +236 -0
  238. package/src/lifecycle/work-lease.ts +426 -0
  239. package/src/lint/rules/index.ts +6 -2
  240. package/src/lint/rules/op/index.ts +17 -4
  241. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  242. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  243. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  244. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  245. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  246. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  247. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  248. package/src/op/activities/activity-contracts.ts +190 -1
  249. package/src/op/activities/apply.ts +7 -6
  250. package/src/op/activities/change-coverage.ts +60 -0
  251. package/src/op/activities/converge.test.ts +55 -1
  252. package/src/op/activities/converge.ts +208 -29
  253. package/src/op/activities/decide.live.test.ts +78 -0
  254. package/src/op/activities/decide.test.ts +300 -0
  255. package/src/op/activities/decide.ts +267 -0
  256. package/src/op/activities/index.ts +21 -0
  257. package/src/op/activities/shell.test.ts +91 -1
  258. package/src/op/activities/shell.ts +91 -2
  259. package/src/op/activities/source-release.test.ts +118 -0
  260. package/src/op/activities/source-release.ts +168 -0
  261. package/src/op/activities/source-rollback.test.ts +131 -0
  262. package/src/op/activities/source-rollback.ts +239 -0
  263. package/src/op/activities/work-evidence.ts +62 -0
  264. package/src/op/activity-contract-registry.test.ts +2 -0
  265. package/src/op/activity-contract-registry.ts +2 -2
  266. package/src/op/activity-contract.test.ts +12 -0
  267. package/src/op/activity-contract.ts +6 -1
  268. package/src/op/activity-profiles.ts +29 -0
  269. package/src/op/activity-registry.test.ts +1 -1
  270. package/src/op/activity-registry.ts +2 -2
  271. package/src/op/builders.ts +124 -5
  272. package/src/op/command-gate.test.ts +192 -0
  273. package/src/op/composites/converge-op.test.ts +27 -1
  274. package/src/op/composites/converge-op.ts +69 -15
  275. package/src/op/decide-backend.test.ts +44 -0
  276. package/src/op/decide-backend.ts +212 -0
  277. package/src/op/decide-config.test.ts +52 -0
  278. package/src/op/decide-config.ts +80 -0
  279. package/src/op/decide-read-inputs.ts +117 -0
  280. package/src/op/discover.ts +71 -0
  281. package/src/op/gate-approval.ts +2 -2
  282. package/src/op/gate-wait.ts +57 -0
  283. package/src/op/gate.ts +8 -3
  284. package/src/op/generate-pipeline.ts +2 -2
  285. package/src/op/index.ts +34 -6
  286. package/src/op/lifecycle-receipt-store.test.ts +60 -0
  287. package/src/op/lifecycle-receipt-store.ts +61 -0
  288. package/src/op/local-executor.ts +367 -33
  289. package/src/op/local-output.test.ts +33 -0
  290. package/src/op/local-output.ts +33 -2
  291. package/src/op/op-ir.test.ts +10 -0
  292. package/src/op/op-ir.ts +18 -0
  293. package/src/op/op-verb-class.ts +1 -0
  294. package/src/op/operator.ts +360 -10
  295. package/src/op/runtime.ts +37 -8
  296. package/src/op/runtimes/local.ts +6 -1
  297. package/src/op/source-archive.ts +161 -0
  298. package/src/op/step-output-ref.ts +6 -2
  299. package/src/op/steward-points.test.ts +313 -0
  300. package/src/op/steward-points.ts +194 -0
  301. package/src/op/steward-turn.ts +79 -0
  302. package/src/op/steward.test.ts +296 -0
  303. package/src/op/steward.ts +277 -0
  304. package/src/op/types.ts +60 -0
  305. package/src/op/work-lease-decl.ts +80 -0
  306. package/src/op/work-lease-run.test.ts +345 -0
  307. package/src/op/work-lease-run.ts +395 -0
  308. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  309. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  310. package/src/workspace/answers-ledger.ts +118 -0
  311. package/src/workspace/box-isolation.test.ts +261 -0
  312. package/src/workspace/box-isolation.ts +205 -0
  313. package/src/workspace/changes-cli.ts +56 -0
  314. package/src/workspace/changes.schema.json +472 -0
  315. package/src/workspace/changes.test.ts +315 -0
  316. package/src/workspace/changes.ts +512 -0
  317. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  318. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  319. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  320. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  321. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  322. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  323. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  324. package/src/workspace/chant-migrations.ts +164 -0
  325. package/src/workspace/check-contract.test.ts +5 -1
  326. package/src/workspace/check.schema.json +25 -7
  327. package/src/workspace/checks/box-isolation.ts +68 -0
  328. package/src/workspace/checks/boxes.test.ts +276 -0
  329. package/src/workspace/checks/boxes.ts +442 -0
  330. package/src/workspace/checks/diagrams.test.ts +134 -0
  331. package/src/workspace/checks/diagrams.ts +108 -0
  332. package/src/workspace/checks/records.ts +72 -0
  333. package/src/workspace/checks.test.ts +7 -0
  334. package/src/workspace/checks.ts +27 -3
  335. package/src/workspace/compose-graph.ts +2 -0
  336. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  337. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  338. package/src/workspace/conformance/index.ts +5 -0
  339. package/src/workspace/decide.test.ts +224 -0
  340. package/src/workspace/decide.ts +709 -0
  341. package/src/workspace/decision-points.schema.json +137 -0
  342. package/src/workspace/declaration.schema.json +285 -0
  343. package/src/workspace/declaration.test.ts +58 -0
  344. package/src/workspace/declaration.ts +241 -0
  345. package/src/workspace/declared-kinds.test.ts +5 -5
  346. package/src/workspace/declared-kinds.ts +71 -3
  347. package/src/workspace/generated-files.ts +14 -0
  348. package/src/workspace/graph-cli.ts +1 -1
  349. package/src/workspace/graph.schema.json +9 -1
  350. package/src/workspace/intent.schema.json +12 -3
  351. package/src/workspace/intent.ts +3 -10
  352. package/src/workspace/lineage-check.ts +16 -1
  353. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  354. package/src/workspace/lineage-upgrade.test.ts +70 -0
  355. package/src/workspace/lineage-upgrade.ts +81 -10
  356. package/src/workspace/ls-contract.test.ts +57 -0
  357. package/src/workspace/ls.schema.json +46 -1
  358. package/src/workspace/ls.ts +62 -3
  359. package/src/workspace/point-answer.schema.json +95 -0
  360. package/src/workspace/points-cli.ts +284 -0
  361. package/src/workspace/points-write.schema.json +499 -0
  362. package/src/workspace/points.schema.json +743 -0
  363. package/src/workspace/points.test.ts +264 -0
  364. package/src/workspace/points.ts +564 -0
  365. package/src/workspace/read-contract.test.ts +18 -1
  366. package/src/workspace/reason-codes.test.ts +24 -1
  367. package/src/workspace/reason-codes.ts +49 -0
  368. package/src/workspace/record-assets.test.ts +3 -2
  369. package/src/workspace/record-assets.ts +4 -1
  370. package/src/workspace/records-amend.schema.json +2 -0
  371. package/src/workspace/records-cli.ts +28 -5
  372. package/src/workspace/records-close.schema.json +2 -0
  373. package/src/workspace/records-contract.test.ts +3 -2
  374. package/src/workspace/records-new.schema.json +2 -0
  375. package/src/workspace/records-review.schema.json +2 -0
  376. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  377. package/src/workspace/records-sessions-write.test.ts +2 -34
  378. package/src/workspace/records-write.e2e.test.ts +108 -0
  379. package/src/workspace/records-write.test.ts +32 -41
  380. package/src/workspace/records-write.ts +33 -15
  381. package/src/workspace/records.schema.json +53 -2
  382. package/src/workspace/records.test.ts +48 -3
  383. package/src/workspace/records.ts +142 -6
  384. package/src/workspace/status-contract.test.ts +189 -6
  385. package/src/workspace/status-gates.ts +9 -4
  386. package/src/workspace/status-stewards.ts +329 -0
  387. package/src/workspace/status.schema.json +335 -5
  388. package/src/workspace/status.ts +142 -6
  389. package/src/workspace/work-acceptance.test.ts +274 -0
  390. package/src/workspace/work-cli.e2e.test.ts +180 -0
  391. package/src/workspace/work-cli.ts +429 -0
  392. package/src/workspace/work-evidence.schema.json +212 -0
  393. package/src/workspace/work-evidence.ts +169 -0
  394. package/src/workspace/work-history.schema.json +349 -0
  395. package/src/workspace/work-history.test.ts +156 -0
  396. package/src/workspace/work-lease.schema.json +233 -0
  397. package/src/workspace/work-readiness-chud.test.ts +145 -0
  398. package/src/workspace/work.test.ts +3 -2
  399. package/src/workspace/work.ts +100 -1
  400. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  401. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  402. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * The work lease (#2732, ws-055 option a): who is working on which work item,
3
+ * as a lease beside the operator lease.
4
+ *
5
+ * A work item's lease is the operator lease (./lease.ts) under the key
6
+ * `work/<id>`, so its ref is `refs/chant/lease/work/<id>`, or
7
+ * `refs/chant/lease/_members/<member>/work/<id>` in a workspace member's
8
+ * ledger (#2524 D7). It keeps every property the operator lease has: a claim
9
+ * is one compare-and-set of that ref, the record carries a fencing token that
10
+ * changes only when the lease changes hands, it expires after its time to
11
+ * live, and it is pushed to and fetched from the project's remote. Unlike the
12
+ * operator's, a work lease counts only once the remote has taken it
13
+ * (`requirePush`), so two workers in separate clones cannot both hold one
14
+ * item.
15
+ *
16
+ * The ref holds only the live lease. Every claim, renew and release also
17
+ * appends a line to `_leases/<id>.jsonl` on `chant/lifecycle`, the same
18
+ * append-only shape as `_gates/<op>.jsonl` (./gate-ledger.ts), so the history
19
+ * of an item's leases is a ledger like the others.
20
+ *
21
+ * The lease is coordination, not a record of work: it never touches the
22
+ * working branch, so it stays out of a release's nothing-to-ship test and out
23
+ * of the write-scope checks (ws-055, and chud's own leases.mjs before it).
24
+ *
25
+ * Which ledger an item's lease lives in is decided by the caller through
26
+ * `cwd`: `chant workspace work` passes the directory of the work kind file,
27
+ * so the member owning the kind owns its items' leases wherever the command
28
+ * runs from.
29
+ */
30
+ import { sortedJsonReplacer } from "../utils";
31
+ import { getRuntime } from "../runtime-adapter";
32
+ import {
33
+ acquireLease,
34
+ leaseRef,
35
+ LEASE_REF_PREFIX,
36
+ LEASE_REMOTE_TRACKING_PREFIX,
37
+ readLease,
38
+ releaseLease,
39
+ type LeaseRecord,
40
+ } from "./lease";
41
+ import {
42
+ fetchLifecycleStatus,
43
+ pushLifecycle,
44
+ readBlobBySha,
45
+ readBlobFromPath,
46
+ readPathSha,
47
+ RefCASConflictError,
48
+ writeBlobToPath,
49
+ } from "./git";
50
+ import { dirname } from "node:path";
51
+ import type { ReasonCode } from "../workspace/reason-codes";
52
+ import { resolveMemberLedger } from "./member-ledger";
53
+
54
+ /** The key prefix of a work lease under the operator lease's ref namespace. */
55
+ export const WORK_LEASE_KEY_PREFIX = "work/";
56
+
57
+ /** The directory on `chant/lifecycle` that holds lease histories, under a member's prefix when it has one. */
58
+ export const LEASES_DIR = "_leases";
59
+
60
+ /** How long a claim lasts unless renewed: chud's default, which its dispatcher renews at a third of. */
61
+ export const DEFAULT_WORK_LEASE_TTL_MS = 10 * 60_000;
62
+
63
+ /** A work item id a lease can be keyed by: one ref path segment and one file name. */
64
+ export const WORK_ITEM_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
65
+
66
+ /** How long a claim waits out another process's lock on the ref before calling it stale. */
67
+ const LOCK_WAIT_MS = 2_000;
68
+ const APPEND_RETRY_ATTEMPTS = 5;
69
+
70
+ /** The lease key of work item `id`. */
71
+ export function workLeaseKey(id: string): string {
72
+ return `${WORK_LEASE_KEY_PREFIX}${id}`;
73
+ }
74
+
75
+ /** The lease ref of work item `id` in the ledger with `memberPrefix`. */
76
+ export function workLeaseRef(id: string, memberPrefix = ""): string {
77
+ return leaseRef(workLeaseKey(id), memberPrefix);
78
+ }
79
+
80
+ /** The path of work item `id`'s lease history on `chant/lifecycle`. */
81
+ export function leaseHistoryPath(id: string, memberPrefix = ""): string {
82
+ return `${memberPrefix}${LEASES_DIR}/${id}.jsonl`;
83
+ }
84
+
85
+ /** One work item's lease, as the read contract reports it. */
86
+ export interface WorkLease {
87
+ /** The work item's id. */
88
+ item: string;
89
+ holder: string;
90
+ /** The fencing token: new on every claim, kept by every renew. */
91
+ token: string;
92
+ acquiredAt: string;
93
+ expiresAt: string;
94
+ }
95
+
96
+ /** A lease with whether it is still live at the time it was read. */
97
+ export interface WorkLeaseState extends WorkLease {
98
+ state: "active" | "expired";
99
+ /** The lease ref. */
100
+ ref: string;
101
+ }
102
+
103
+ /** One line of `_leases/<id>.jsonl`. */
104
+ export interface LeaseHistoryRecord {
105
+ version: 1;
106
+ event: "claim" | "renew" | "release";
107
+ item: string;
108
+ /** The lease's holder. */
109
+ holder: string;
110
+ /** Who wrote this line: the holder, or, for the release of an expired lease, whoever closed it out. */
111
+ by: string;
112
+ token: string;
113
+ acquiredAt: string;
114
+ /** The lease's expiry after this event; a release keeps the expiry it had. */
115
+ expiresAt: string;
116
+ /** When the event happened. */
117
+ timestamp: string;
118
+ /** How the work ended, on a release. Free text such as done, not_done or skipped. */
119
+ outcome?: string;
120
+ note?: string;
121
+ }
122
+
123
+ /** Why a claim, renew or release was refused. Closed: a new code is a contract change. */
124
+ export const WORK_LEASE_REFUSALS = [
125
+ /** Someone holds the lease live: another worker, or, for a claim, the same one. */
126
+ "lease-held",
127
+ /** Nobody holds it live: it expired, was released or was never claimed. */
128
+ "lease-not-held",
129
+ /** The live lease has another token than the one given. */
130
+ "lease-token-mismatch",
131
+ /** Another writer changed the lease between the read and the write. */
132
+ "lease-race",
133
+ /** The remote refused the push: another clone got there first, or it could not be reached. */
134
+ "lease-push-rejected",
135
+ ] as const satisfies readonly ReasonCode[];
136
+ export type WorkLeaseRefusal = (typeof WORK_LEASE_REFUSALS)[number];
137
+
138
+ /** Where the history line went. */
139
+ export interface LeaseHistoryWrite {
140
+ path: string;
141
+ commit: string;
142
+ /** Whether `chant/lifecycle` reached the remote; false with no remote too. */
143
+ pushed: boolean;
144
+ }
145
+
146
+ export type WorkLeaseResult =
147
+ | { ok: true; lease: WorkLease; history: LeaseHistoryWrite }
148
+ | { ok: false; reason: WorkLeaseRefusal; message: string; heldBy?: WorkLease };
149
+
150
+ export interface WorkLeaseOptions {
151
+ /** A directory in the repository, inside the member whose ledger holds the lease. */
152
+ cwd: string;
153
+ now?: () => Date;
154
+ }
155
+
156
+ function toWorkLease(item: string, r: LeaseRecord): WorkLease {
157
+ return { item, holder: r.holder, token: r.token, acquiredAt: r.acquiredAt, expiresAt: r.expiresAt };
158
+ }
159
+
160
+ function checkId(id: string): void {
161
+ if (!WORK_ITEM_ID_PATTERN.test(id) || id.includes("..")) {
162
+ throw new Error(`${JSON.stringify(id)} can't key a work lease: use letters, digits, ".", "_" and "-", starting with a letter or digit`);
163
+ }
164
+ }
165
+
166
+ const REASONS: Record<string, WorkLeaseRefusal> = {
167
+ held: "lease-held",
168
+ "not-held": "lease-not-held",
169
+ "token-mismatch": "lease-token-mismatch",
170
+ race: "lease-race",
171
+ "push-rejected": "lease-push-rejected",
172
+ };
173
+
174
+ function refusalMessage(id: string, reason: WorkLeaseRefusal, heldBy: WorkLease | undefined): string {
175
+ const by = heldBy ? `${heldBy.holder} until ${heldBy.expiresAt}` : undefined;
176
+ switch (reason) {
177
+ case "lease-held":
178
+ return `${id} is held by ${by}`;
179
+ case "lease-not-held":
180
+ return `${id} is not held${heldBy ? ` by the caller: ${by}` : ""}; claim it again`;
181
+ case "lease-token-mismatch":
182
+ return `${id} is held with another token (${heldBy?.token}) by ${by}`;
183
+ case "lease-race":
184
+ return `${id} changed while it was being written${by ? `: it is held by ${by}` : ""}`;
185
+ case "lease-push-rejected":
186
+ return `the remote refused the lease on ${id}${by ? `: it is held there by ${by}` : ", or could not be reached"}`;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Claim work item `id` for `holder` for `ttlMs`. Refused with `lease-held`,
192
+ * naming the holder, when anyone holds it live, `holder` included; an expired
193
+ * lease does not refuse a claim, and the new claim gets a new token.
194
+ */
195
+ export async function claimWorkLease(
196
+ id: string,
197
+ holder: string,
198
+ opts: WorkLeaseOptions & { ttlMs?: number; note?: string },
199
+ ): Promise<WorkLeaseResult> {
200
+ checkId(id);
201
+ const result = await acquireLease(workLeaseKey(id), holder, {
202
+ cwd: opts.cwd,
203
+ ttlMs: opts.ttlMs ?? DEFAULT_WORK_LEASE_TTL_MS,
204
+ now: opts.now,
205
+ mode: "claim",
206
+ requirePush: true,
207
+ lockWaitMs: LOCK_WAIT_MS,
208
+ });
209
+ return finish(id, "claim", holder, result, opts);
210
+ }
211
+
212
+ /**
213
+ * Move the expiry of `holder`'s live lease on `id` to `ttlMs` from now,
214
+ * keeping its token. With `token`, the live lease must carry it. Refused with
215
+ * `lease-not-held` once the lease has expired or was released: the holder
216
+ * claims it again, and gets a new token.
217
+ */
218
+ export async function renewWorkLease(
219
+ id: string,
220
+ holder: string,
221
+ opts: WorkLeaseOptions & { ttlMs?: number; token?: string; note?: string },
222
+ ): Promise<WorkLeaseResult> {
223
+ checkId(id);
224
+ const result = await acquireLease(workLeaseKey(id), holder, {
225
+ cwd: opts.cwd,
226
+ ttlMs: opts.ttlMs ?? DEFAULT_WORK_LEASE_TTL_MS,
227
+ now: opts.now,
228
+ mode: "renew",
229
+ token: opts.token,
230
+ requirePush: true,
231
+ lockWaitMs: LOCK_WAIT_MS,
232
+ });
233
+ return finish(id, "renew", holder, result, opts);
234
+ }
235
+
236
+ async function finish(
237
+ id: string,
238
+ event: "claim" | "renew",
239
+ holder: string,
240
+ result: Awaited<ReturnType<typeof acquireLease>>,
241
+ opts: WorkLeaseOptions & { note?: string },
242
+ ): Promise<WorkLeaseResult> {
243
+ if (!result.acquired || !result.lease) {
244
+ const reason = REASONS[result.reason ?? "race"];
245
+ const heldBy = result.heldBy ? toWorkLease(id, result.heldBy) : undefined;
246
+ return { ok: false, reason, message: refusalMessage(id, reason, heldBy), ...(heldBy ? { heldBy } : {}) };
247
+ }
248
+ const lease = toWorkLease(id, result.lease);
249
+ const now = opts.now?.() ?? new Date();
250
+ const history = await appendLeaseHistory(
251
+ { version: 1, event, item: id, holder, by: holder, token: lease.token, acquiredAt: lease.acquiredAt, expiresAt: lease.expiresAt, timestamp: now.toISOString(), ...(opts.note ? { note: opts.note } : {}) },
252
+ opts,
253
+ );
254
+ return { ok: true, lease, history };
255
+ }
256
+
257
+ /**
258
+ * Give the lease on `id` back. The holder releases a live lease (with
259
+ * `token`, only that one); anyone may release an expired one, which is how an
260
+ * abandoned claim is closed out. `outcome` says how the work ended. Refused
261
+ * with `lease-not-held` when there is no lease, and `lease-held` when someone
262
+ * else holds it live.
263
+ */
264
+ export async function releaseWorkLease(
265
+ id: string,
266
+ by: string,
267
+ opts: WorkLeaseOptions & { token?: string; outcome?: string; note?: string },
268
+ ): Promise<WorkLeaseResult> {
269
+ checkId(id);
270
+ const key = workLeaseKey(id);
271
+ const now = opts.now?.() ?? new Date();
272
+ const { record } = await readLease(key, { cwd: opts.cwd });
273
+ if (!record) return { ok: false, reason: "lease-not-held", message: refusalMessage(id, "lease-not-held", undefined) };
274
+ const current = toWorkLease(id, record);
275
+ const live = new Date(record.expiresAt).getTime() > now.getTime();
276
+ if (live && record.holder !== by) return { ok: false, reason: "lease-held", message: refusalMessage(id, "lease-held", current), heldBy: current };
277
+ if (opts.token !== undefined && record.token !== opts.token) {
278
+ return { ok: false, reason: "lease-token-mismatch", message: refusalMessage(id, "lease-token-mismatch", current), heldBy: current };
279
+ }
280
+ if (!(await releaseLease(key, record.holder, record.token, { cwd: opts.cwd }))) {
281
+ const after = (await readLease(key, { cwd: opts.cwd })).record;
282
+ const heldBy = after ? toWorkLease(id, after) : undefined;
283
+ return { ok: false, reason: "lease-race", message: refusalMessage(id, "lease-race", heldBy), ...(heldBy ? { heldBy } : {}) };
284
+ }
285
+ const history = await appendLeaseHistory(
286
+ {
287
+ version: 1,
288
+ event: "release",
289
+ item: id,
290
+ holder: record.holder,
291
+ by,
292
+ token: record.token,
293
+ acquiredAt: record.acquiredAt,
294
+ expiresAt: record.expiresAt,
295
+ timestamp: now.toISOString(),
296
+ ...(opts.outcome ? { outcome: opts.outcome } : {}),
297
+ ...(opts.note ? { note: opts.note } : {}),
298
+ },
299
+ opts,
300
+ );
301
+ return { ok: true, lease: current, history };
302
+ }
303
+
304
+ /**
305
+ * Append one line to `_leases/<id>.jsonl`, with the same read-then-CAS retry
306
+ * the gate ledger's appends use, then push `chant/lifecycle`. The branch is
307
+ * fetched first when it fast-forwards, and the push is best-effort: the lease
308
+ * ref is what coordinates, and a history that did not reach the remote is
309
+ * reported as `pushed: false` rather than failing a lease that was taken.
310
+ */
311
+ async function appendLeaseHistory(record: LeaseHistoryRecord, opts: { cwd: string }): Promise<LeaseHistoryWrite> {
312
+ await fetchLifecycleStatus(opts).catch(() => undefined);
313
+ const file = `${record.item}.jsonl`;
314
+ const json = JSON.stringify(record, sortedJsonReplacer);
315
+ let commit: string | undefined;
316
+ let lastErr: unknown;
317
+ for (let attempt = 1; attempt <= APPEND_RETRY_ATTEMPTS && commit === undefined; attempt++) {
318
+ try {
319
+ const priorSha = await readPathSha(LEASES_DIR, file, opts);
320
+ const existing = priorSha ? await readBlobBySha(priorSha, opts) : null;
321
+ const content = existing ? `${existing.replace(/\n$/, "")}\n${json}` : json;
322
+ commit = await writeBlobToPath(LEASES_DIR, file, content, `Work lease ${record.event}: ${record.item} (${record.holder})`, {
323
+ ...opts,
324
+ expectPriorPathSha: priorSha,
325
+ });
326
+ } catch (err) {
327
+ if (!(err instanceof RefCASConflictError)) throw err;
328
+ lastErr = err;
329
+ }
330
+ }
331
+ if (commit === undefined) throw lastErr;
332
+ const pushed = await pushLifecycle(opts).catch(() => false);
333
+ const { prefix } = await resolveMemberLedger(opts.cwd);
334
+ return { path: leaseHistoryPath(record.item, prefix), commit, pushed };
335
+ }
336
+
337
+ /** Parse the text of a lease history file, oldest first. Malformed lines are counted and skipped. */
338
+ export function parseLeaseHistory(content: string): { records: LeaseHistoryRecord[]; malformed: number } {
339
+ const records: LeaseHistoryRecord[] = [];
340
+ let malformed = 0;
341
+ for (const line of content.split("\n").map((l) => l.trim()).filter(Boolean)) {
342
+ try {
343
+ const r = JSON.parse(line) as Partial<LeaseHistoryRecord>;
344
+ const ok =
345
+ r.version === 1 &&
346
+ (r.event === "claim" || r.event === "renew" || r.event === "release") &&
347
+ [r.item, r.holder, r.by, r.token, r.acquiredAt, r.expiresAt, r.timestamp].every((v) => typeof v === "string");
348
+ if (ok) records.push(r as LeaseHistoryRecord);
349
+ else malformed++;
350
+ } catch {
351
+ malformed++;
352
+ }
353
+ }
354
+ return { records, malformed };
355
+ }
356
+
357
+ /** Read work item `id`'s lease history in the ledger of the project at `opts.cwd`. Empty when it has none. */
358
+ export async function readLeaseHistory(id: string, opts: { cwd: string }): Promise<{ records: LeaseHistoryRecord[]; malformed: number }> {
359
+ checkId(id);
360
+ const content = await readBlobFromPath(LEASES_DIR, `${id}.jsonl`, opts);
361
+ return content ? parseLeaseHistory(content) : { records: [], malformed: 0 };
362
+ }
363
+
364
+ function parseRecord(raw: string): LeaseRecord | undefined {
365
+ try {
366
+ const v = JSON.parse(raw) as Partial<LeaseRecord>;
367
+ return typeof v.holder === "string" && typeof v.token === "string" && typeof v.acquiredAt === "string" && typeof v.expiresAt === "string"
368
+ ? (v as LeaseRecord)
369
+ : undefined;
370
+ } catch {
371
+ return undefined;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Every work lease in the ledger with `memberPrefix`, read from the local
377
+ * refs only: the lease refs this clone wrote, and the remote's as last
378
+ * fetched (`refs/chant/lease-remote/`), the fresher of the two per item, as
379
+ * `readLease` picks. It never fetches, so a read-only report stays off the
380
+ * network; a claim or renew fetches before it decides. Released leases have
381
+ * no ref and are not listed; expired ones are, with `state: "expired"`.
382
+ * Sorted by item.
383
+ */
384
+ export async function listWorkLeases(opts: { cwd: string; memberPrefix: string; now?: Date }): Promise<WorkLeaseState[]> {
385
+ const rt = getRuntime();
386
+ const now = (opts.now ?? new Date()).getTime();
387
+ const local = `${LEASE_REF_PREFIX}${opts.memberPrefix}${WORK_LEASE_KEY_PREFIX}`;
388
+ const remote = `${LEASE_REMOTE_TRACKING_PREFIX}${opts.memberPrefix}${WORK_LEASE_KEY_PREFIX}`;
389
+ const out = await rt.spawn(["git", "for-each-ref", "--format=%(refname) %(objectname)", local, remote], { cwd: opts.cwd });
390
+ if (out.exitCode !== 0) return [];
391
+ const best = new Map<string, LeaseRecord>();
392
+ for (const line of out.stdout.split("\n").filter(Boolean)) {
393
+ const [ref, sha] = line.split(" ");
394
+ const id = ref.startsWith(local) ? ref.slice(local.length) : ref.slice(remote.length);
395
+ if (!WORK_ITEM_ID_PATTERN.test(id)) continue;
396
+ const record = parseRecord((await readBlobBySha(sha, opts)) ?? "");
397
+ if (!record) continue;
398
+ const prior = best.get(id);
399
+ const key = (r: LeaseRecord) => `${r.acquiredAt} ${r.expiresAt}`;
400
+ if (!prior || key(record) > key(prior) || (key(record) === key(prior) && ref.startsWith(local))) best.set(id, record);
401
+ }
402
+ return [...best.entries()]
403
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
404
+ .map(([id, r]) => ({
405
+ ...toWorkLease(id, r),
406
+ state: new Date(r.expiresAt).getTime() > now ? ("active" as const) : ("expired" as const),
407
+ ref: workLeaseRef(id, opts.memberPrefix),
408
+ }));
409
+ }
410
+
411
+ /**
412
+ * The active lease of each work item of the kind at `kindFile`, by item id,
413
+ * for `chant workspace records --json`. Read from the local refs of the
414
+ * ledger owning the kind file's directory, without fetching. Empty when
415
+ * nothing can be read.
416
+ */
417
+ export async function activeWorkLeases(kindFile: string, now = new Date()): Promise<Map<string, Omit<WorkLease, "item">>> {
418
+ const cwd = dirname(kindFile);
419
+ try {
420
+ const { prefix } = await resolveMemberLedger(cwd);
421
+ const leases = await listWorkLeases({ cwd, memberPrefix: prefix, now });
422
+ return new Map(leases.filter((l) => l.state === "active").map((l) => [l.item, { holder: l.holder, token: l.token, acquiredAt: l.acquiredAt, expiresAt: l.expiresAt }]));
423
+ } catch {
424
+ return new Map();
425
+ }
426
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Core lint rules for chant projects (COR + EVL).
2
+ * Core lint rules for chant projects (COR + EVL, and SYS001 for the `decide`
3
+ * activity's backends, #2828).
3
4
  */
4
5
 
5
6
  import type { LintRule } from "../rule";
@@ -29,6 +30,7 @@ export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-
29
30
  export { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
30
31
  export { cor022ReceiptLeafRule, collectReceiptVariables, receiptFactoryCall } from "./cor022-receipt-leaf";
31
32
  export { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
33
+ export { noLiteralKeyRule } from "./sys001-no-literal-key";
32
34
  export { isInsideCompositeFactory } from "./composite-scope";
33
35
 
34
36
  import { flatDeclarationsRule } from "./flat-declarations";
@@ -56,9 +58,10 @@ import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-
56
58
  import { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
57
59
  import { cor022ReceiptLeafRule } from "./cor022-receipt-leaf";
58
60
  import { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
61
+ import { noLiteralKeyRule } from "./sys001-no-literal-key";
59
62
 
60
63
  /**
61
- * Load all 24 core lint rules (COR + EVL).
64
+ * Load all core lint rules (COR + EVL + SYS001).
62
65
  */
63
66
  export function loadCoreRules(): LintRule[] {
64
67
  return [
@@ -87,5 +90,6 @@ export function loadCoreRules(): LintRule[] {
87
90
  cor021EnvLiteralNameRule,
88
91
  cor022ReceiptLeafRule,
89
92
  cor024ReceiptSecretPointerRule,
93
+ noLiteralKeyRule,
90
94
  ];
91
95
  }
@@ -5,7 +5,8 @@
5
5
  * `coreReceiptChecks`) so they run over the FULL build result regardless of
6
6
  * which lexicons are configured — an Op is recognized by entity type
7
7
  * (`../../../op/resource.ts`'s `OpResource`), not by which lexicon declared
8
- * it.
8
+ * it. SYS010 (#2828) joined them when the `decide` activity moved into core:
9
+ * it reads the decide steps, and the `decide.backends` the caller passes.
9
10
  */
10
11
 
11
12
  import type { PostSynthCheck } from "../../post-synth";
@@ -13,13 +14,25 @@ import { ops012 } from "./ops012-activity-contract";
13
14
  import { ops013 } from "./ops013-step-output-ref";
14
15
  import { ops014 } from "./ops014-converge-rule-refusals";
15
16
  import { ops015 } from "./ops015-gate-approval";
17
+ import { sys010Check } from "./sys010-key-over-plain-http";
18
+ import type { DecideBackend } from "../../../op/decide-config";
16
19
 
17
20
  export { ops012 } from "./ops012-activity-contract";
18
21
  export { ops013 } from "./ops013-step-output-ref";
19
22
  export { ops014 } from "./ops014-converge-rule-refusals";
20
23
  export { ops015 } from "./ops015-gate-approval";
24
+ export { sys010, sys010Check } from "./sys010-key-over-plain-http";
21
25
 
22
- /** Core's own post-synth checks over the Op model — OPS012, OPS013, OPS014, OPS015. */
23
- export function coreOpChecks(): PostSynthCheck[] {
24
- return [ops012, ops013, ops014, ops015];
26
+ export interface CoreOpCheckOptions {
27
+ /** `decide.backends` from the project's chant.config, for SYS010 (#2828). */
28
+ decideBackends?: Record<string, Pick<DecideBackend, "url" | "key">>;
29
+ }
30
+
31
+ /**
32
+ * Core's own post-synth checks over the Op model: OPS012, OPS013, OPS014,
33
+ * OPS015, and SYS010 over the `decide` steps and the configured decide
34
+ * backends.
35
+ */
36
+ export function coreOpChecks(opts: CoreOpCheckOptions = {}): PostSynthCheck[] {
37
+ return [ops012, ops013, ops014, ops015, sys010Check(opts.decideBackends)];
25
38
  }
@@ -8,7 +8,7 @@ import type { PostSynthContext } from "../../post-synth";
8
8
  import { DECLARABLE_MARKER } from "../../../declarable";
9
9
  import { when, eq, gt, allOf, run, report } from "../../../op";
10
10
  import type { ConvergeRule } from "../../../op";
11
- import type { ConvergeSymptom } from "../../../lifecycle/symptoms";
11
+ import type { ConvergeSymptom, ResourceSymptom } from "../../../lifecycle/symptoms";
12
12
  import { ops014 } from "./ops014-converge-rule-refusals";
13
13
 
14
14
  function makeEntity(entityType: string, props: Record<string, unknown>) {
@@ -261,3 +261,25 @@ describe("OPS014: converge-rule-refusals", () => {
261
261
  expect(ops014.check(ctx)).toHaveLength(0);
262
262
  });
263
263
  });
264
+
265
+ describe("OPS014 over a ConvergeOp with an observer step (#2778)", () => {
266
+ test("its rules are checked against ResourceSymptom's fields", () => {
267
+ const observed = (rules: unknown[]) =>
268
+ makeEntity("Chant::Op", {
269
+ name: "box-converge",
270
+ overview: "test",
271
+ labels: { Converge: "true", Env: "box", Dial: "apply", Observe: "step" },
272
+ phases: [
273
+ { name: "Observe", steps: [{ kind: "activity", fn: "spriteServicesObserve", args: {}, id: "observe" }] },
274
+ { name: "Converge", steps: [{ kind: "activity", fn: "convergeTick", args: { rules, observed: { kind: "step-output-ref", step: "observe" } } }] },
275
+ ],
276
+ });
277
+ const restartOp = opEntity("restart-service", [{ kind: "activity", fn: "spriteServiceRestart", args: {} }]);
278
+ const good = when<ResourceSymptom>(eq("status", "drifted"), run("restart-service"), { id: "restart", why: "x" });
279
+ const wrongField = { ...good, id: "adopt", when: { kind: "field-comparison", field: "adoptCount", op: "gt", value: 0 } };
280
+ const ok = ops014.check(makeCtxFromEntities(new Map<string, unknown>([["c", observed([good])], ["r", restartOp]])));
281
+ expect(ok).toEqual([]);
282
+ const bad = ops014.check(makeCtxFromEntities(new Map<string, unknown>([["c", observed([wrongField])], ["r", restartOp]])));
283
+ expect(bad.map((d) => d.message)).toEqual([expect.stringMatching(/rule "adopt": predicate is outside the evaluable subset/)]);
284
+ });
285
+ });
@@ -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
+ });