@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
@@ -303,13 +303,23 @@ export interface ParsedArgs {
303
303
  composites?: boolean;
304
304
  /** `chant workspace graph --intent <path[:start-end]>` (#2651): the region the intent graph is over. */
305
305
  intent?: string;
306
+ /** `chant workspace check --changes <base>..<head>` (#2773): the diff the forward coverage check maps to records. */
307
+ changes?: string;
308
+ /** `chant workspace check --changes ... --severity off|warn|fail` (#2773): in place of the declaration's `changes.severity`. */
309
+ severity?: string;
306
310
  /** `chant workspace records --current` (#2546): leave out records a closed record supersedes. */
307
311
  current?: boolean;
308
312
  /** `chant workspace records amend <id> --set <file|->` (#2670): the JSON fields to set. */
309
313
  set?: string;
310
314
  /** `chant workspace records review <id> --verdict <v>` (#2670): agree, dissent or abstain. */
311
315
  verdict?: string;
312
- /** `chant workspace records review <id> --by <principal>` (#2670): the reviewer, as the caller names them. */
316
+ /**
317
+ * `chant workspace records review <id> --by <principal>` (#2670): the
318
+ * reviewer, as the caller names them. Also `chant workspace records new
319
+ * --by <name>` (#2756): the record's proposer, or its decider when it
320
+ * opens straight into a later state; see {@link ChannelOptions.by} in
321
+ * `records-write.ts`.
322
+ */
313
323
  by?: string;
314
324
  /**
315
325
  * `chant workspace records review <id> --sign [<key file>]` (#2687): the key that seals the verdict, or true for git's user.signingkey.
@@ -436,7 +446,7 @@ export interface ParsedArgs {
436
446
  steward?: string;
437
447
  /** `chant workspace work claim|renew|release <id> --holder <name>` (#2732): who holds, or releases, the work lease. */
438
448
  holder?: string;
439
- /** `chant run <op> --work <id>` (#2748): the work item an Op with a work lease runs under. */
449
+ /** `chant run <op> --work <id>` (#2748): the work item an Op with a work lease runs under. `chant workspace check --changes --work <id>` (#2773): the work item in hand. */
440
450
  work?: string;
441
451
  /** `chant workspace work claim|renew <id> --ttl <seconds|duration>` (#2732): how long the lease lasts unless renewed. */
442
452
  ttl?: string;
@@ -26,6 +26,7 @@ import { join } from "path";
26
26
  import { regenLexicon, type RegenResult } from "./lexicon-regen";
27
27
  import { fetchWithRetry } from "./fetch";
28
28
  import { isLexiconPlugin, type UpstreamPin } from "../lexicon";
29
+ import { importLexiconPackage } from "../lexicon-module";
29
30
 
30
31
  // ── Public types ──────────────────────────────────────────────────────
31
32
 
@@ -200,7 +201,7 @@ export function revertVersionBump(filePath: string, original: string): void {
200
201
  export async function loadUpstreamPin(lexicon: LexiconId): Promise<UpstreamPin | null> {
201
202
  let mod: Record<string, unknown>;
202
203
  try {
203
- mod = (await import(`@intentius/chant-lexicon-${lexicon}`)) as Record<string, unknown>;
204
+ mod = (await importLexiconPackage(`@intentius/chant-lexicon-${lexicon}`)) as Record<string, unknown>;
204
205
  } catch {
205
206
  return null;
206
207
  }
@@ -171,7 +171,54 @@ describe("release wiring: preflight gates both recipes (#1255)", () => {
171
171
  }
172
172
  });
173
173
 
174
- it("the whole-repo release requires main, since it pushes main", () => {
175
- expect(recipeBody("release")).toMatch(/release-preflight\.sh main/);
174
+ });
175
+
176
+ /**
177
+ * #2816 — main's HEAD is rarely green, so a release tags the newest green
178
+ * commit on main (or the one named) and merges the bump back into main.
179
+ * scripts/release-preflight.test.ts runs the mechanics; this pins the wiring.
180
+ */
181
+ describe("release wiring: release the newest green commit (#2816)", () => {
182
+ it("both recipes tag the commit the preflight chose, not HEAD", () => {
183
+ for (const recipe of ["release", "release-lexicon"]) {
184
+ const body = recipeBody(recipe);
185
+ expect(body).toMatch(/sha=\$\(bash scripts\/release-preflight\.sh/);
186
+ expect(body).toMatch(/release_open "\$sha"/);
187
+ }
188
+ });
189
+
190
+ it("both recipes merge the tag back into main instead of pushing HEAD", () => {
191
+ for (const recipe of ["release", "release-lexicon"]) {
192
+ const body = recipeBody(recipe);
193
+ expect(body).toMatch(/release_ship "\$tag"/);
194
+ expect(body).not.toMatch(/git push/);
195
+ expect(body.indexOf("git tag")).toBeLessThan(body.indexOf("release_ship"));
196
+ }
197
+ const lib = readFileSync(join(repoRoot, "scripts", "release-lib.sh"), "utf-8");
198
+ // main and the tag land together or not at all, and nothing is forced.
199
+ expect(lib).toMatch(/git push --quiet --atomic origin "HEAD:refs\/heads\/main" "refs\/tags\/\$tag"/);
200
+ expect(lib).not.toMatch(/git push[^\n]*(--force|\s-f\b)/);
201
+ });
202
+
203
+ it("both recipes offer a dry run that stops before committing", () => {
204
+ for (const recipe of ["release", "release-lexicon"]) {
205
+ const body = recipeBody(recipe);
206
+ expect(body.indexOf("RELEASE_DRY_RUN")).toBeGreaterThan(-1);
207
+ expect(body.indexOf("RELEASE_DRY_RUN")).toBeLessThan(body.indexOf("git commit"));
208
+ }
209
+ });
210
+
211
+ it("the whole-repo floor reads main as well as the green commit", () => {
212
+ expect(recipeBody("release")).toMatch(/release_version_at "\$RELEASE_MAIN"/);
213
+ expect(recipeBody("release-lexicon")).toMatch(/release_version_at "\$RELEASE_MAIN"/);
214
+ });
215
+
216
+ it("a push to main does not cancel main's chant run; PR runs still cancel", () => {
217
+ const workflow = readFileSync(join(repoRoot, ".github", "workflows", "chant.yml"), "utf-8");
218
+ const block = workflow.match(/^concurrency:\n((?:[ ]+.*\n)+)/m)?.[1] ?? "";
219
+ // One group per commit on main, so a later push neither cancels a running
220
+ // run nor replaces a queued one.
221
+ expect(block).toMatch(/group:.*github\.ref == 'refs\/heads\/main' && github\.sha \|\| github\.ref/);
222
+ expect(block).toMatch(/cancel-in-progress: \$\{\{ github\.ref != 'refs\/heads\/main' \}\}/);
176
223
  });
177
224
  });
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { CapabilityRegistry } from "./capability";
25
- import { lexiconModulePath } from "../lexicon-module";
25
+ import { lexiconModulePath, importLexiconPackage } from "../lexicon-module";
26
26
  import type { CapabilityPlugin } from "./capability-plugin";
27
27
  import { isCapabilityPlugin } from "./capability-plugin";
28
28
  import { starterCapabilityPlugin } from "./starter-plugin";
@@ -62,7 +62,7 @@ export async function loadCapabilityPlugin(name: string): Promise<CapabilityPlug
62
62
  const packageName = `@intentius/chant-capability-${name}`;
63
63
  let mod: Record<string, unknown>;
64
64
  try {
65
- mod = (await import(packageName)) as Record<string, unknown>;
65
+ mod = (await importLexiconPackage(packageName)) as Record<string, unknown>;
66
66
  } catch (err) {
67
67
  throw new Error(
68
68
  `capability plugin package "${packageName}" could not be loaded: ${err instanceof Error ? err.message : String(err)}`,
@@ -118,7 +118,7 @@ export async function loadCapabilityPluginFromLexicon(name: string): Promise<Cap
118
118
  const packageName = lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`;
119
119
  let mod: Record<string, unknown>;
120
120
  try {
121
- mod = (await import(packageName)) as Record<string, unknown>;
121
+ mod = (await importLexiconPackage(packageName)) as Record<string, unknown>;
122
122
  } catch {
123
123
  return null; // lexicon not installed / not resolvable here — nothing to contribute.
124
124
  }
@@ -22,7 +22,7 @@
22
22
  * executor, the CLI entrypoint the driver (#556) never had.
23
23
  */
24
24
 
25
- import { lexiconModulePath, lexiconNames } from "../lexicon-module";
25
+ import { lexiconModulePath, lexiconNames, importLexiconPackage } from "../lexicon-module";
26
26
  import { discoverComponents } from "./discover";
27
27
  import type { BuildParamProvenance } from "../provenance";
28
28
  import { inferArchetype, projectToJson, type Archetype } from "./component";
@@ -244,7 +244,7 @@ export interface GenerateComponentsResult {
244
244
  async function loadLexiconPlugin(name: string): Promise<LexiconPlugin | null> {
245
245
  let mod: Record<string, unknown>;
246
246
  try {
247
- mod = (await import(lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
247
+ mod = (await importLexiconPackage(lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
248
248
  } catch {
249
249
  return null;
250
250
  }
package/src/config.ts CHANGED
@@ -11,6 +11,7 @@ import type { BuildParamProvenance } from "./provenance";
11
11
  import { findProjectConfig } from "./project-root";
12
12
  import { evaluateProjectConfig } from "./config-sandbox";
13
13
  import { lexiconNames, pathLexiconRoot, registerLexiconDeclarations, type LexiconDeclaration } from "./lexicon-module";
14
+ import { decideConfigSchema, type DecideConfig } from "./op/decide-config";
14
15
 
15
16
  /**
16
17
  * One project-declared environment (chant #1166). Historically always a bare
@@ -194,6 +195,7 @@ export const ChantConfigSchema = z.object({
194
195
  knowledge: z.object({
195
196
  dir: z.string().min(1).optional(),
196
197
  }).optional(),
198
+ decide: decideConfigSchema.optional(),
197
199
  exclude: z.array(z.string().min(1)).optional(),
198
200
  include: z.array(z.string().min(1)).optional(),
199
201
  rootOnly: z.boolean().optional(),
@@ -449,6 +451,15 @@ export interface ChantConfig {
449
451
  dir?: string;
450
452
  };
451
453
 
454
+ /**
455
+ * The backends the `decide` Op activity calls (#2828): backend name, as a
456
+ * decision point's model decider names it, to `{ url, key?, timeoutMs? }`.
457
+ * A key is `{ env }` or a brokered capability, never a literal (SYS001). A
458
+ * `decide` step's own `backends` take the place of these. See
459
+ * `./op/decide-config.ts`.
460
+ */
461
+ decide?: DecideConfig;
462
+
452
463
  /**
453
464
  * Globs, relative to the directory holding this config, naming files that
454
465
  * source discovery must skip (#2519): `build`, `lint`, `list`, `explain`
@@ -24,7 +24,9 @@
24
24
  * a project that names lexicons by package takes exactly the path it always did.
25
25
  */
26
26
 
27
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
27
+ import { createRequire } from "node:module";
28
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
29
+ import { pathToFileURL } from "node:url";
28
30
 
29
31
  /**
30
32
  * One `lexicons` entry: a package-backed name, or a name plus the module that
@@ -213,3 +215,56 @@ export function lexiconPackagesToInstall(
213
215
  ): string[] {
214
216
  return names.filter((name) => !paths.has(name)).map((name) => `@intentius/chant-lexicon-${name}`);
215
217
  }
218
+
219
+ /**
220
+ * chant#2845 — the npm package a bare specifier names: `@scope/name` for a
221
+ * scoped one, else its first segment. `undefined` for a relative or absolute
222
+ * path, which is not a package.
223
+ */
224
+ export function packageNameOf(spec: string): string | undefined {
225
+ if (spec.startsWith(".") || isAbsolute(spec) || spec.startsWith("file:")) return undefined;
226
+ const parts = spec.split("/");
227
+ return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
228
+ }
229
+
230
+ /** True when `err` says the package `pkg` itself could not be found, not something it imports. */
231
+ function isPackageNotFound(err: unknown, pkg: string): boolean {
232
+ const code = (err as { code?: unknown } | null)?.code;
233
+ if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") return false;
234
+ const message = err instanceof Error ? err.message : String(err);
235
+ return message.includes(`'${pkg}'`) || message.includes(`"${pkg}"`) || message.includes(`${pkg}/`);
236
+ }
237
+
238
+ /**
239
+ * chant#2845 — resolve `spec` (a lexicon package or one of its subpaths) from
240
+ * `fromDir` instead of from chant's own install, or `undefined` when the
241
+ * project doesn't have it either.
242
+ */
243
+ export function resolveFromProject(spec: string, fromDir: string = process.cwd()): string | undefined {
244
+ try {
245
+ return createRequire(join(fromDir, "package.json")).resolve(spec);
246
+ } catch {
247
+ return undefined;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * chant#2845 — import a lexicon package, or one of its subpaths, by its bare
253
+ * specifier. chant's own install is tried first, as a bare `import()` always
254
+ * did, so a lexicon chant can reach loads exactly as before. When the package
255
+ * isn't there, which is the case for a chant installed globally and a project
256
+ * that installs its lexicons in its own node_modules, it is resolved from
257
+ * `fromDir` (the project) and imported from there. Any other failure, and a
258
+ * package the project doesn't have either, rethrows the first error.
259
+ */
260
+ export async function importLexiconPackage(spec: string, fromDir: string = process.cwd()): Promise<Record<string, unknown>> {
261
+ try {
262
+ return (await import(spec)) as Record<string, unknown>;
263
+ } catch (err) {
264
+ const pkg = packageNameOf(spec);
265
+ if (pkg === undefined || !isPackageNotFound(err, pkg)) throw err;
266
+ const resolved = resolveFromProject(spec, fromDir);
267
+ if (resolved === undefined) throw err;
268
+ return (await import(pathToFileURL(resolved).href)) as Record<string, unknown>;
269
+ }
270
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * chant#2845 — a lexicon the project installs in its own node_modules loads
3
+ * when chant's own install can't reach it (a global chant): the plugin, its
4
+ * op activities and its activity contracts, resolved from the working
5
+ * directory after the bare import from chant's location fails.
6
+ */
7
+ import { describe, test, expect, beforeAll, afterAll } from "vitest";
8
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { importLexiconPackage, packageNameOf, resolveFromProject } from "./lexicon-module";
12
+ import { loadActivities } from "./op/activity-registry";
13
+ import { loadActivityContracts } from "./op/activity-contract-registry";
14
+ import { resolveLexiconVersions } from "./cli/plugins";
15
+
16
+ const NAME = "zz2845";
17
+ const PKG = `@intentius/chant-lexicon-${NAME}`;
18
+ let project: string;
19
+
20
+ beforeAll(async () => {
21
+ project = await mkdtemp(join(tmpdir(), "chant-2845-"));
22
+ const dir = join(project, "node_modules", "@intentius", `chant-lexicon-${NAME}`);
23
+ await mkdir(join(dir, "op"), { recursive: true });
24
+ await writeFile(
25
+ join(dir, "package.json"),
26
+ JSON.stringify({
27
+ name: PKG,
28
+ version: "9.9.9",
29
+ type: "module",
30
+ exports: { ".": { default: "./index.js" }, "./op/activities": { default: "./op/activities.js" }, "./op/activity-contracts": { default: "./op/contracts.js" } },
31
+ }),
32
+ );
33
+ await writeFile(join(dir, "index.js"), "export const marker = 'plugin';\n");
34
+ await writeFile(join(dir, "op", "activities.js"), "export async function zz2845Probe() { return { ok: true }; }\n");
35
+ // A contract is branded with a global symbol, so a plain object built here is one.
36
+ await writeFile(
37
+ join(dir, "op", "contracts.js"),
38
+ "export const zz2845ProbeContract = { [Symbol.for('chant.op.activityContract')]: true, name: 'zz2845Probe', args: {}, returns: {} };\n",
39
+ );
40
+ });
41
+
42
+ afterAll(async () => {
43
+ await rm(project, { recursive: true, force: true });
44
+ });
45
+
46
+ describe("importLexiconPackage (#2845)", () => {
47
+ test("a package chant can't reach is imported from the project", async () => {
48
+ await expect(import(PKG)).rejects.toThrow();
49
+ const mod = await importLexiconPackage(PKG, project);
50
+ expect(mod.marker).toBe("plugin");
51
+ const op = await importLexiconPackage(`${PKG}/op/activities`, project);
52
+ expect(typeof op.zz2845Probe).toBe("function");
53
+ });
54
+
55
+ test("a package chant reaches loads from chant's install, whatever the directory", async () => {
56
+ const mod = await importLexiconPackage("zod", project);
57
+ expect(typeof mod.z).toBe("object");
58
+ });
59
+
60
+ test("a package neither has fails with the bare import's own error", async () => {
61
+ await expect(importLexiconPackage("@intentius/chant-lexicon-nope-2845", project)).rejects.toThrow(/chant-lexicon-nope-2845/);
62
+ });
63
+
64
+ test("a path is not looked up in the project", () => {
65
+ expect(packageNameOf("/abs/lexicon.ts")).toBeUndefined();
66
+ expect(packageNameOf("./lexicon.ts")).toBeUndefined();
67
+ expect(packageNameOf(`${PKG}/op/activities`)).toBe(PKG);
68
+ expect(resolveFromProject(PKG, project)).toMatch(/index\.js$/);
69
+ });
70
+ });
71
+
72
+ describe("the loaders take the project's lexicon (#2845)", () => {
73
+ const inProject = async <T>(fn: () => Promise<T> | T): Promise<T> => {
74
+ const cwd = process.cwd();
75
+ process.chdir(project);
76
+ try {
77
+ return await fn();
78
+ } finally {
79
+ process.chdir(cwd);
80
+ }
81
+ };
82
+
83
+ test("loadActivities finds its op activities", async () => {
84
+ const activities = await inProject(() => loadActivities([NAME]));
85
+ expect(activities.has("zz2845Probe")).toBe(true);
86
+ });
87
+
88
+ test("loadActivityContracts finds its contracts", async () => {
89
+ const contracts = await inProject(() => loadActivityContracts([NAME]));
90
+ expect(contracts.has("zz2845Probe")).toBe(true);
91
+ });
92
+
93
+ test("resolveLexiconVersions reads its version", async () => {
94
+ expect(await inProject(() => resolveLexiconVersions([NAME]))).toEqual({ [NAME]: "9.9.9" });
95
+ });
96
+ });
@@ -66,6 +66,12 @@ export interface ConvergeRuleOutcome {
66
66
  action: "ran" | "reported" | "skipped-budget" | "skipped-flap" | "gated";
67
67
  /** The dispatched Op name, for `action: "ran"` or `"gated"`. */
68
68
  op?: string;
69
+ /**
70
+ * The resource the rule fired for, on a tick of a ConvergeOp with an
71
+ * observer step (#2778), which evaluates its rules once per resource. The
72
+ * dispatched Op reads it from `CHANT_CONVERGE_RESOURCE`.
73
+ */
74
+ resource?: string;
69
75
  /** The gate's signal name, for `action: "gated"`. */
70
76
  gateName?: string;
71
77
  /**
@@ -131,6 +137,14 @@ export function componentVerdicts(rows: readonly ComponentStatusRow[]): Converge
131
137
  }));
132
138
  }
133
139
 
140
+ /** One resource an observer step reported on a tick (#2778). */
141
+ export interface ConvergeResourceVerdict {
142
+ name: string;
143
+ status: "in-sync" | "drifted" | "unknown";
144
+ /** The observer's reason, capped to one line. */
145
+ detail?: string;
146
+ }
147
+
134
148
  /** One immutable converge-tick record. */
135
149
  export interface ConvergeTickRecord {
136
150
  /** Schema version, so an incompatible future shape is detected before being misread. */
@@ -168,6 +182,12 @@ export interface ConvergeTickRecord {
168
182
  * records written before #2027 have none; absent is not "no components".
169
183
  */
170
184
  components?: ConvergeComponentVerdict[];
185
+ /**
186
+ * What a ConvergeOp's observer step reported this tick (#2778), one entry
187
+ * per resource: in its place of `components` for resources no lexicon
188
+ * observes. Absent on a tick observed through a lexicon environment.
189
+ */
190
+ resources?: ConvergeResourceVerdict[];
171
191
  /** Aggregate counts backing the tick's one log line. */
172
192
  summary: {
173
193
  drifted: number;
@@ -85,6 +85,8 @@ export function buildRunRecord(
85
85
  status: OpRunRecord["status"];
86
86
  id?: string;
87
87
  gate?: OpRunRecord["gate"];
88
+ point?: OpRunRecord["point"];
89
+ steward?: string;
88
90
  },
89
91
  ): OpRunRecordInput {
90
92
  const phases: OpRunPhaseRecord[] = [];
@@ -106,6 +108,8 @@ export function buildRunRecord(
106
108
  ...(record.approval ? { approval: record.approval } : {}),
107
109
  ...(record.error !== undefined ? { error: record.error } : {}),
108
110
  ...(record.refusal !== undefined ? { refusal: record.refusal } : {}),
111
+ ...(record.point ? { point: record.point } : {}),
112
+ ...(record.gate ? { gate: record.gate } : {}),
109
113
  });
110
114
  if (record.outcome) outcomes[record.outcome.name] = record.outcome.value;
111
115
  }
@@ -122,6 +126,8 @@ export function buildRunRecord(
122
126
  outcomes,
123
127
  phases,
124
128
  ...(times.gate ? { gate: times.gate } : {}),
129
+ ...(times.point ? { point: times.point } : {}),
130
+ ...(times.steward ? { steward: times.steward } : {}),
125
131
  };
126
132
  }
127
133
 
@@ -124,3 +124,98 @@ export const CONVERGE_SYMPTOM_FIELDS: ReadonlySet<string> = new Set([
124
124
  "unobservedReasons",
125
125
  "totalCount",
126
126
  ]);
127
+
128
+ // ── Resources an observer step reports (#2778) ──────────────────────────────
129
+
130
+ /**
131
+ * One resource's verdict, as an observer step reports it: `in-sync` when it
132
+ * is what its declaration says, `drifted` when it isn't, and `unknown` when
133
+ * the observer could not tell.
134
+ */
135
+ export type ResourceStatus = "in-sync" | "drifted" | "unknown";
136
+
137
+ /** One resource an observer step looked at. */
138
+ export interface ObservedResource {
139
+ name: string;
140
+ status: ResourceStatus;
141
+ /** Why the observer gave that verdict, in one line. */
142
+ detail?: string;
143
+ }
144
+
145
+ /**
146
+ * What a `ConvergeOp`'s observer step returns (#2778), for resources no
147
+ * lexicon declares or observes: the processes a supervisor runs in a box,
148
+ * for example. `ConvergeOp({ observe })` evaluates its rules once per
149
+ * resource, against a {@link ResourceSymptom}.
150
+ */
151
+ export interface ResourceObservation {
152
+ resources: ObservedResource[];
153
+ }
154
+
155
+ /**
156
+ * The record a `ConvergeOp` with an observer step evaluates each rule
157
+ * against, once per observed resource (#2778). A rule reads `status` (and
158
+ * may read `resource` to single one out); a `run()` it fires is dispatched
159
+ * for that resource, which the dispatched Op reads from
160
+ * `CHANT_CONVERGE_RESOURCE`.
161
+ */
162
+ export interface ResourceSymptom {
163
+ env: string;
164
+ /** The resource's name, as the observer reported it. */
165
+ resource: string;
166
+ status: ResourceStatus;
167
+ /** The observer's one-line reason, or "". */
168
+ detail: string;
169
+ }
170
+
171
+ /**
172
+ * The environment variable a dispatched Op reads the resource from, when a
173
+ * ConvergeOp with an observer step fired its rule for one resource (#2778).
174
+ */
175
+ export const CONVERGE_RESOURCE_ENV = "CHANT_CONVERGE_RESOURCE";
176
+
177
+ /** Every field a {@link ResourceSymptom} produces: OPS014's whitelist for a ConvergeOp with an observer step. */
178
+ export const RESOURCE_SYMPTOM_FIELDS: ReadonlySet<string> = new Set(["env", "resource", "status", "detail"]);
179
+
180
+ const RESOURCE_STATUSES: ReadonlySet<string> = new Set(["in-sync", "drifted", "unknown"]);
181
+
182
+ /**
183
+ * Read an observer step's result as a {@link ResourceObservation}. The
184
+ * result is the observation itself, or a `shell()` step's result whose
185
+ * stdout is the observation as JSON. Throws, naming what is wrong, on
186
+ * anything else: a tick must not converge from a reading it can't parse.
187
+ */
188
+ export function parseResourceObservation(value: unknown): ResourceObservation {
189
+ let candidate: unknown = value;
190
+ if (candidate && typeof candidate === "object" && !("resources" in candidate) && typeof (candidate as { stdout?: unknown }).stdout === "string") {
191
+ const stdout = (candidate as { stdout: string }).stdout;
192
+ try {
193
+ candidate = JSON.parse(stdout);
194
+ } catch {
195
+ throw new Error(`the observer step's stdout is not JSON: ${stdout.slice(0, 120)}`);
196
+ }
197
+ }
198
+ const resources = (candidate as { resources?: unknown } | null)?.resources;
199
+ if (!Array.isArray(resources)) {
200
+ throw new Error("the observer step returned no `resources` array: it must return { resources: [{ name, status }] }, or print it as JSON");
201
+ }
202
+ const seen = new Set<string>();
203
+ const out: ObservedResource[] = [];
204
+ for (const [i, r] of resources.entries()) {
205
+ const entry = r as { name?: unknown; status?: unknown; detail?: unknown } | null;
206
+ if (!entry || typeof entry.name !== "string" || entry.name === "") {
207
+ throw new Error(`the observer step's resources[${i}] has no name`);
208
+ }
209
+ if (typeof entry.status !== "string" || !RESOURCE_STATUSES.has(entry.status)) {
210
+ throw new Error(`the observer step's resource "${entry.name}" has status ${JSON.stringify(entry.status)}: it must be in-sync, drifted or unknown`);
211
+ }
212
+ if (seen.has(entry.name)) throw new Error(`the observer step reported resource "${entry.name}" twice`);
213
+ seen.add(entry.name);
214
+ out.push({
215
+ name: entry.name,
216
+ status: entry.status as ResourceStatus,
217
+ ...(typeof entry.detail === "string" && entry.detail !== "" ? { detail: entry.detail } : {}),
218
+ });
219
+ }
220
+ return { resources: out };
221
+ }
@@ -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
+ });